diff --git a/.github/workflows/aurpublish.yml b/.github/workflows/aurpublish.yml deleted file mode 100644 index c2ede44..0000000 --- a/.github/workflows/aurpublish.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Upload AUR Package - -on: - release: - types: [created] - -jobs: - aur-publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install requests - - name: Create PKGBUILD - run: | - python ./create-pkgbuild.py > ./PKGBUILD - - name: Publish AUR package - uses: KSXGitHub/github-actions-deploy-aur@v2.5.0 - with: - pkgname: bumblebee-status - pkgbuild: ./PKGBUILD - commit_username: ${{ secrets.AUR_USERNAME }} - commit_email: ${{ secrets.AUR_EMAIL }} - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: Update AUR package - ssh_keyscan_types: rsa,dsa,ecdsa,ed25519 diff --git a/.github/workflows/autotest.yml b/.github/workflows/autotest.yml deleted file mode 100644 index ec02237..0000000 --- a/.github/workflows/autotest.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Tests - -on: - pull_request: - types: [ opened, reopened, edited ] - push: - -env: - CC_TEST_REPORTER_ID: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - name: Install Ubuntu dependencies - run: sudo apt-get install -y libdbus-1-dev libgit2-dev libvirt-dev taskwarrior - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install -U coverage pytest pytest-mock freezegun - pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true - pip install $(cat requirements/modules/*.txt | grep -v power | cut -d ' ' -f 1 | sort -u) - - name: Install Code Climate dependency - run: | - 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 - - name: Run tests - run: | - coverage run --source=. -m pytest tests -v - - name: Report coverage - uses: paambaati/codeclimate-action@v3.2.0 - with: - coverageCommand: coverage3 xml - debug: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 5e39422..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,70 +0,0 @@ -# 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 diff --git a/.gitignore b/.gitignore index c21fe43..2bf1ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -*.o - # Vim swap files *swp *~ diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 15f1c60..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 - -python: - install: - - requirements: docs/requirements.txt -build: - os: ubuntu-22.04 - tools: - python: "3" diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b730e3e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: python +python: + - "3.4" + - "3.5" + - "3.6" + - "3.7" +before_install: + - sudo apt-get -qq update +install: + - pip install -U coverage==4.3 pytest pytest-mock + - pip install codeclimate-test-reporter +script: + - coverage run --source=. -m pytest tests -v + - CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter +addons: + code_climate: + repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ee8d31..099a62e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,5 +12,6 @@ But even if you can't provide those, any indicator that something is not working ### Adding a new module or theme If you want to add a new module, please have a look at [how to write a new module](docs/development/module.rst) and [how to write a new theme](docs/development/theme.rst). Then simply create a Pull Request and I will review the changes as soon as possible. +If you want to do me a *big* favour, check the Travis status for any failing unit tests. Oh - and if you happen to add unit tests, that's also something I am very grateful for! Thanks for reading until here! :) diff --git a/PKGBUILD.template b/PKGBUILD.template deleted file mode 100644 index 08b511d..0000000 --- a/PKGBUILD.template +++ /dev/null @@ -1,45 +0,0 @@ -# Maintainer: Tobias Witek -# Contributor: Daniel M. Capella -# Contributor: spookykidmm - -pkgname=bumblebee-status -pkgver= -pkgrel=1 -pkgdesc='Modular, theme-able status line generator for the i3 window manager' -arch=('any') -url=https://github.com/tobi-wan-kenobi/bumblebee-status -license=('MIT') -depends=('python' 'python-netifaces' 'python-psutil' 'python-requests') -optdepends=('xorg-xbacklight: to display a displays brightness' - 'xorg-xset: enable/disable automatic screen locking' - 'libnotify: enable/disable automatic screen locking' - 'dnf: display DNF package update information' - 'xorg-setxkbmap: display/change the current keyboard layout' - 'redshift: display the redshifts current color' - 'pulseaudio: control pulseaudio sink/sources' - 'xorg-xrandr: enable/disable screen outputs' - 'pacman: display current status of pacman' - 'iputils: display a ping' - 'python-i3ipc: display titlebar' - 'fakeroot: dependency of the pacman module' - 'python-pytz: timezone conversion for datetimetz module' - 'python-tzlocal: retrieve system timezone for datetimetz module' -) -source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") -sha512sums=('') - -package() { - install -d "$pkgdir"/usr/bin \ - "$pkgdir"/usr/share/$pkgname/bumblebee_status/{core,util} \ - "$pkgdir"/usr/share/$pkgname/bumblebee_status/modules/{core,contrib} \ - "$pkgdir"/usr/share/$pkgname/themes/icons - ln -s /usr/share/$pkgname/$pkgname "$pkgdir"/usr/bin/$pkgname - ln -s /usr/share/$pkgname/bumblebee-ctl "$pkgdir"/usr/bin/bumblebee-ctl - - cd $pkgname-$pkgver - cp -a --parents $pkgname bumblebee_status/{,core/,util/,modules/core/,modules/contrib/}*.py \ - themes/{,icons/}*.json $pkgdir/usr/share/$pkgname - cp -r bin $pkgdir/usr/share/$pkgname/ - - install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE -} diff --git a/README.md b/README.md index 2f41627..6663f4d 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,12 @@ -bumblebee-status -===================================================== - -logo courtesy of [kellya](https://github.com/kellya) - thank you! +# 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) [![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main) -![Commits since release](https://img.shields.io/github/commits-since/tobi-wan-kenobi/bumblebee-status/latest) -![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status) -![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git) -![PyPI version](https://img.shields.io/pypi/v/bumblebee-status) -![Contributors](https://img.shields.io/github/contributors-anon/tobi-wan-kenobi/bumblebee-status) -[![Tests](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml/badge.svg?branch=main)](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/autotest.yml) - +![AUR version](https://img.shields.io/aur/version/bumblebee-status) +[![PyPI version](https://badge.fury.io/py/bumblebee-status.svg)](https://badge.fury.io/py/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) [![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.** @@ -36,14 +27,16 @@ Thanks a lot! Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors) -Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 +Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8 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`. If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/) +The default branch for this project is `main` - I'm keeping `master` around for backwards compatibility (I do not want to break anybody's setup), but the default branch is now `main`! + +If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/) --- @@ -82,16 +75,10 @@ makepkg -sicr pip install --user bumblebee-status ``` -There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)! - -An ebuild, for Gentoo Linux, is available on [gallifrey overlay](https://github.com/fedeliallalinea/gallifrey/tree/master/x11-misc/bumblebee-status). Instructions for adding the overlay can be found [here](https://github.com/fedeliallalinea/gallifrey/blob/master/README.md). - # Dependencies [Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables) for each module. If you are not using a module, you don't need the dependencies. -Some themes (e.g. all ‘powerline’ themes) require Font Awesome http://fontawesome.io/ and a powerline-compatible font (powerline-fonts) https://github.com/powerline/fonts - # Usage ## Normal usage In your i3wm configuration, modify the *status_command* for your i3bar like this: @@ -145,6 +132,14 @@ bar { Restart i3wm and - that's it! +# User contributions + +[@somospocos:bumblebee-status-contrib](https://github.com/somospocos/bumblebee-status-contrib): Collected resources and useful tricks by @somospocos + +[@somospocos:bumblebee-bridge-dwm](https://github.com/somospocos/bumblebee-bridge-dwm): Bridge bumblebee-status output into dwm status bar + +[@somospocos:bumblebee-bridge-dzen2](https://github.com/somospocos/bumblebee-bridge-dzen2): Bridge bumblebee-status output into dzen2 + # Examples [List of themes](https://bumblebee-status.readthedocs.io/en/main/themes.html) diff --git a/bumblebee-ctl b/bumblebee-ctl index 435162d..b1c0f37 100755 --- a/bumblebee-ctl +++ b/bumblebee-ctl @@ -12,7 +12,6 @@ button = { "right-mouse": 3, "wheel-up": 4, "wheel-down": 5, - "update": -1, } @@ -21,7 +20,7 @@ def main(): parser.add_argument( "-b", "--button", - choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down", "update"], + choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down"], help="button to emulate", default="left-mouse", ) diff --git a/bumblebee-status b/bumblebee-status index a16c747..f995850 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -1,11 +1,11 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python import os import sys import json import time -import signal import socket +import select import logging import threading @@ -19,7 +19,6 @@ import core.module import core.input import core.event -import util.format started = False @@ -38,48 +37,42 @@ class CommandSocket(object): self.__socket.close() os.unlink(self.__name) -def process_event(event_line, config, update_lock): - modules = {} - try: - event = json.loads(event_line) - core.input.trigger(event) - if "name" in event: - modules[event["name"]] = True - except ValueError: - pass - delay = float(config.get("engine.input_delay", 0.0)) - if delay > 0: - time.sleep(delay) - if update_lock.acquire(blocking=False) == True: - core.event.trigger("update", modules.keys(), force=True) - core.event.trigger("draw") - update_lock.release() - -def handle_commands(config, update_lock): +def handle_input(output): with CommandSocket() as cmdsocket: + poll = select.poll() + poll.register(sys.stdin.fileno(), select.POLLIN) + poll.register(cmdsocket, select.POLLIN) + while True: - tmp, _ = cmdsocket.accept() - line = tmp.recv(4096).decode() - tmp.close() - logging.debug("socket event {}".format(line)) - process_event(line, config, update_lock) + events = poll.poll() + modules = {} + for fileno, event in events: + if fileno == cmdsocket.fileno(): + tmp, _ = cmdsocket.accept() + line = tmp.recv(4096).decode() + tmp.close() + logging.debug("socket event {}".format(line)) + else: + line = "[" + while line.startswith("["): + line = sys.stdin.readline().strip(",").strip() + logging.info("input event: {}".format(line)) + try: + event = json.loads(line) + core.input.trigger(event) + if "name" in event: + modules[event["name"]] = True + except ValueError: + pass + core.event.trigger("update", modules.keys()) + core.event.trigger("draw") -def handle_events(config, update_lock): - while True: - try: - line = sys.stdin.readline().strip(",").strip() - if line == "[": continue - logging.info("input event: {}".format(line)) - process_event(line, config, update_lock) - except Exception as e: - logging.error(e) + poll.unregister(sys.stdin.fileno()) def main(): - global started - config = core.config.Config(sys.argv[1:]) level = logging.DEBUG if config.debug() else logging.ERROR if config.logfile(): @@ -102,23 +95,9 @@ def main(): core.input.register(None, core.input.WHEEL_UP, "i3-msg workspace prev_on_output") core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output") - core.event.trigger("start") - started = True - - update_lock = threading.Lock() - event_thread = threading.Thread(target=handle_events, args=(config, update_lock, )) - event_thread.daemon = True - event_thread.start() - - cmd_thread = threading.Thread(target=handle_commands, args=(config, update_lock, )) - cmd_thread.daemon = True - cmd_thread.start() - - def sig_USR1_handler(signum,stack): - if update_lock.acquire(blocking=False) == True: - core.event.trigger("update", force=True) - core.event.trigger("draw") - update_lock.release() + input_thread = threading.Thread(target=handle_input, args=(output,)) + input_thread.daemon = True + input_thread.start() if config.debug(): modules.append(core.module.load("debug", config, theme)) @@ -131,16 +110,11 @@ def main(): modules.reverse() output.modules(modules) - - if util.format.asbool(config.get("engine.collapsible", True)) == True: - core.input.register(None, core.input.MIDDLE_MOUSE, output.toggle_minimize) - - signal.signal(10, sig_USR1_handler) + core.event.trigger("start") + started = True while True: - if update_lock.acquire(blocking=False) == True: - core.event.trigger("update") - core.event.trigger("draw") - update_lock.release() + core.event.trigger("update") + core.event.trigger("draw") output.wait(config.interval()) core.event.trigger("stop") @@ -152,7 +126,6 @@ if __name__ == "__main__": main() except Exception as e: # really basic errors -> make sure these are shown in the status bar by minimal config - logging.exception(e) if not started: print("{\"version\":1}") print("[") diff --git a/bumblebee_status/_version.py b/bumblebee_status/_version.py index 247364e..154e077 100644 --- a/bumblebee_status/_version.py +++ b/bumblebee_status/_version.py @@ -292,7 +292,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: - # unparsable. Maybe git-describe is misbehaving? + # unparseable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 0b564b3..67aba5f 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -71,11 +71,6 @@ class print_usage(argparse.Action): ) rst = {} - - if self._format == "rst": - print(".. THIS DOCUMENT IS AUTO-GENERATED, DO NOT MODIFY") - print(".. To change this document, please update the docstrings in the individual modules") - for m in all_modules(): try: module_type = "core" @@ -147,13 +142,6 @@ class Config(util.store.Store): parser = argparse.ArgumentParser( description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki" ) - parser.add_argument( - "-c", - "--config-file", - action="store", - default=None, - help="Specify a configuration file to use" - ) parser.add_argument( "-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP ) @@ -165,7 +153,7 @@ class Config(util.store.Store): default=[], help=PARAMETER_HELP, ) - parser.add_argument("-t", "--theme", default=None, help=THEME_HELP) + parser.add_argument("-t", "--theme", default="default", help=THEME_HELP) parser.add_argument( "-i", "--iconset", @@ -179,13 +167,6 @@ class Config(util.store.Store): default=[], help="Specify a list of modules to hide when not in warning/error state", ) - parser.add_argument( - "-e", - "--errorhide", - nargs="+", - default=[], - help="Specify a list of modules that are hidden when in state error" - ) parser.add_argument( "-d", "--debug", action="store_true", help="Add debug fields to i3 output" ) @@ -210,18 +191,13 @@ class Config(util.store.Store): self.__args = parser.parse_args(args) - if self.__args.config_file: - cfg = self.__args.config_file + for cfg in [ + "~/.bumblebee-status.conf", + "~/.config/bumblebee-status.conf", + "~/.config/bumblebee-status/config", + ]: cfg = os.path.expanduser(cfg) self.load_config(cfg) - else: - for cfg in [ - "~/.bumblebee-status.conf", - "~/.config/bumblebee-status.conf", - "~/.config/bumblebee-status/config", - ]: - cfg = os.path.expanduser(cfg) - self.load_config(cfg) parameters = [item for sub in self.__args.parameters for item in sub] for param in parameters: @@ -240,24 +216,15 @@ class Config(util.store.Store): :param filename: path to the file to load """ - def load_config(self, filename, content=None): - if os.path.exists(filename) or content != None: + def load_config(self, filename): + if os.path.exists(filename): log.info("loading {}".format(filename)) tmp = RawConfigParser() - tmp.optionxform = str - - if content: - tmp.read_string(content) - else: - tmp.read(u"{}".format(filename)) + tmp.read(u"{}".format(filename)) if tmp.has_section("module-parameters"): for key, value in tmp.items("module-parameters"): self.set(key, value) - if tmp.has_section("core"): - for key, value in tmp.items("core"): - self.set(key, value) - """Returns a list of configured modules @@ -266,11 +233,7 @@ class Config(util.store.Store): """ def modules(self): - list_of_modules = [item for sub in self.__args.modules for item in sub] - - if list_of_modules == []: - list_of_modules = util.format.aslist(self.get('modules', [])) - return list_of_modules + return [item for sub in self.__args.modules for item in sub] """Returns the global update interval @@ -281,15 +244,6 @@ class Config(util.store.Store): def interval(self, default=1): return util.format.seconds(self.get("interval", default)) - """Returns the global popup menu font size - - :return: popup menu font size - :rtype: int - """ - - def popup_font_size(self, default=12): - return util.format.asint(self.get("popup_font_size", default)) - """Returns whether debug mode is enabled :return: True if debug is enabled, False otherwise @@ -324,7 +278,7 @@ class Config(util.store.Store): """ def theme(self): - return self.__args.theme or self.get("theme") or "default" + return self.__args.theme """Returns the configured iconset name @@ -335,21 +289,14 @@ class Config(util.store.Store): def iconset(self): return self.__args.iconset - """Returns whether a module should be hidden if their state is not warning/critical + """Returns which modules should be hidden if their state is not warning/critical - :return: True if module should be hidden automatically, False otherwise - :rtype: bool + :return: list of modules to hide automatically + :rtype: list of strings """ def autohide(self, name): - return name in self.__args.autohide or name in util.format.aslist(self.get("autohide", [])) + return name in self.__args.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 diff --git a/bumblebee_status/core/event.py b/bumblebee_status/core/event.py index 70b6b0c..88096a5 100644 --- a/bumblebee_status/core/event.py +++ b/bumblebee_status/core/event.py @@ -8,16 +8,6 @@ def register(event, callback, *args, **kwargs): __callbacks.setdefault(event, []).append(cb) -def register_exclusive(event, callback, *args, **kwargs): - cb = callback - if args or kwargs: - cb = lambda: callback(*args, **kwargs) - - __callbacks[event] = [cb] - -def unregister(event): - if event in __callbacks: - del __callbacks[event] def clear(): __callbacks.clear() diff --git a/bumblebee_status/core/input.py b/bumblebee_status/core/input.py index 5752dd8..a2ebaa8 100644 --- a/bumblebee_status/core/input.py +++ b/bumblebee_status/core/input.py @@ -10,7 +10,6 @@ MIDDLE_MOUSE = 2 RIGHT_MOUSE = 3 WHEEL_UP = 4 WHEEL_DOWN = 5 -UPDATE = -1 def button_name(button): @@ -24,8 +23,6 @@ def button_name(button): return "wheel-up" if button == WHEEL_DOWN: return "wheel-down" - if button == UPDATE: - return "update" return "n/a" @@ -53,14 +50,10 @@ def __execute(event, cmd, wait=False): def register(obj, button=None, cmd=None, wait=False): event_id = __event_id(obj.id if obj is not None else "", button) logging.debug("registering callback {}".format(event_id)) - core.event.unregister(event_id) # make sure there's always only one input event - if callable(cmd): - core.event.register_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)) + core.event.register(event_id, cmd) else: - core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait)) + core.event.register(event_id, lambda event: __execute(event, cmd, wait)) def trigger(event): diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 37a3143..cba01ac 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -1,6 +1,5 @@ import os import importlib -import importlib.util import logging import threading @@ -18,27 +17,6 @@ except Exception as e: log = logging.getLogger(__name__) -def import_user(module_short, config, theme): - usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short)) - if os.path.exists(usermod): - if hasattr(importlib, "machinery"): - log.debug("importing {} from user via machinery".format(module_short)) - mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), - os.path.expanduser(usermod)).load_module() - return getattr(mod, "Module")(config, theme) - else: - log.debug("importing {} from user via importlib.util".format(module_short)) - try: - spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod.Module(config, theme) - except Exception as e: - spec = importlib.util.find_spec("modules.{}".format(module_short), usermod) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod.Module(config, theme) - raise ImportError("not found") """Loads a module by name @@ -55,25 +33,20 @@ def load(module_name, config=core.config.Config([]), theme=None): error = None module_short, alias = (module_name.split(":") + [module_name])[0:2] config.set("__alias__", alias) - - try: - mod = importlib.import_module("modules.core.{}".format(module_short)) - log.debug("importing {} from core".format(module_short)) - return getattr(mod, "Module")(config, theme) - except ImportError as e: + for namespace in ["core", "contrib"]: try: - log.warning("failed to import {} from core: {}".format(module_short, e)) - mod = importlib.import_module("modules.contrib.{}".format(module_short)) - log.debug("importing {} from contrib".format(module_short)) + mod = importlib.import_module( + "modules.{}.{}".format(namespace, module_short) + ) + log.debug( + "importing {} from {}.{}".format(module_short, namespace, 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") + log.debug("failed to import {}: {}".format(module_name, e)) + error = e + log.fatal("failed to import {}: {}".format(module_name, error)) + return Error(config=config, module=module_name, error=error) class Module(core.input.Object): @@ -96,8 +69,6 @@ class Module(core.input.Object): self.alias = self.__config.get("__alias__", None) self.id = self.alias if self.alias else self.name self.next_update = None - self.minimized = False - self.minimized = self.parameter("start-minimized", False) self.theme = theme @@ -113,15 +84,6 @@ class Module(core.input.Object): def hidden(self): return False - """Override this to show the module even if it normally would be scrolled away - - :return: True if the module should be hidden, False otherwise - :rtype: boolean - """ - - def scroll(self): - return True - """Retrieve CLI/configuration parameters for this module. For example, if the module is called "test" and the user specifies "-p test.x=123" on the commandline, using self.parameter("x") retrieves the value 123. @@ -138,8 +100,6 @@ class Module(core.input.Object): for prefix in [self.name, self.module_name, self.alias]: value = self.__config.get("{}.{}".format(prefix, key), value) - if self.minimized: - value = self.__config.get("{}.minimized.{}".format(prefix, key), value) return value """Set a parameter for this module @@ -163,7 +123,7 @@ class Module(core.input.Object): def update_wrapper(self): if self.background == True: - if self.__thread and self.__thread.is_alive(): + if self.__thread and self.__thread.isAlive(): return # skip this update interval self.__thread = threading.Thread(target=self.internal_update, args=(True,)) self.__thread.start() @@ -210,9 +170,8 @@ class Module(core.input.Object): :rtype: bumblebee_status.widget.Widget """ - def add_widget(self, full_text="", name=None, hidden=False): - widget_id = "{}::{}".format(self.name, len(self.widgets())) - widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id, hidden=hidden) + def add_widget(self, full_text="", name=None): + widget = core.widget.Widget(full_text=full_text, name=name) self.widgets().append(widget) widget.module = self return widget @@ -225,14 +184,12 @@ class Module(core.input.Object): :rtype: bumblebee_status.widget.Widget """ - def widget(self, name=None, widget_id=None): - if not name and not widget_id: + def widget(self, name=None): + if not name: return self.widgets()[0] for w in self.widgets(): - if name and w.name == name: - return w - if w.id == widget_id: + if w.name == name: return w return None @@ -305,7 +262,7 @@ class Error(Module): def full_text(self, widget): return "{}: {}".format(self.__module, self.__error) - """Overridden state, always returns critical (it *is* an error, after all""" + """Overriden state, always returns critical (it *is* an error, after all""" def state(self, widget): return ["critical"] diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 9ff3010..ab68c6a 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -1,7 +1,6 @@ import sys import json import time -import threading import core.theme import core.event @@ -58,9 +57,6 @@ class block(object): def set(self, key, value): self.__attributes[key] = value - def get(self, key, default=None): - return self.__attributes.get(key, default) - def is_pango(self, attr): if isinstance(attr, dict) and "pango" in attr: return True @@ -95,17 +91,9 @@ class block(object): assign(self.__attributes, result, "background", "bg") if "full_text" in self.__attributes: - prefix = self.__pad(self.pangoize(self.__attributes.get("prefix"))) - suffix = self.__pad(self.pangoize(self.__attributes.get("suffix"))) - self.set("_prefix", prefix) - self.set("_suffix", suffix) - self.set("_raw", self.get("full_text")) result["full_text"] = self.pangoize(result["full_text"]) result["full_text"] = self.__format(self.__attributes["full_text"]) - if "min-width" in self.__attributes and "padding" in self.__attributes: - self.set("min-width", self.__format(self.get("min-width"))) - for k in [ "name", "instance", @@ -135,8 +123,11 @@ class block(object): def __format(self, text): if text is None: return None - prefix = self.get("_prefix") - suffix = self.get("_suffix") + prefix = self.__pad(self.pangoize(self.__attributes.get("prefix"))) + suffix = self.__pad(self.pangoize(self.__attributes.get("suffix"))) + self.set("_prefix", prefix) + self.set("_suffix", suffix) + self.set("_raw", text) return "{}{}{}".format(prefix, text, suffix) @@ -146,17 +137,10 @@ class i3(object): self.__content = {} self.__theme = theme self.__config = config - self.__offset = 0 - self.__lock = threading.Lock() core.event.register("update", self.update) core.event.register("start", self.draw, "start") core.event.register("draw", self.draw, "statusline") core.event.register("stop", self.draw, "stop") - core.event.register("output.scroll-left", self.scroll_left) - core.event.register("output.scroll-right", self.scroll_right) - - def content(self): - return self.__content def theme(self, new_theme=None): if new_theme: @@ -168,28 +152,15 @@ class i3(object): return self.__modules self.__modules = modules if isinstance(modules, list) else [modules] - def toggle_minimize(self, event): - widget_id = event["instance"] - - for module in self.__modules: - if module.widget(widget_id=widget_id) and util.format.asbool(module.parameter("minimize", False)) == True: - # this module can customly minimize - module.minimized = not module.minimized - return - - if widget_id in self.__content: - self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] - def draw(self, what, args=None): - with self.__lock: - cb = getattr(self, what) - data = cb(args) if args else cb() - if "blocks" in data: - sys.stdout.write(json.dumps(data["blocks"], default=dump_json)) - if "suffix" in data: - sys.stdout.write(data["suffix"]) - sys.stdout.write("\n") - sys.stdout.flush() + cb = getattr(self, what) + data = cb(args) if args else cb() + if "blocks" in data: + sys.stdout.write(json.dumps(data["blocks"], default=dump_json)) + if "suffix" in data: + sys.stdout.write(data["suffix"]) + sys.stdout.write("\n") + sys.stdout.flush() def start(self): return { @@ -216,7 +187,7 @@ class i3(object): except: blk.set("min-width", minwidth) blk.set("align", widget.theme("align")) - blk.set("full_text", "\u2026" if self.__content[widget.id]["minimized"] else self.__content[widget.id]["text"]) + blk.set("full_text", self.__content[widget]) if widget.get("pango", False): blk.set("markup", "pango") if self.__config.debug(): @@ -226,75 +197,43 @@ class i3(object): blk.set("__state", state) return blk - def scroll_left(self): - if self.__offset > 0: - self.__offset -= 1 - - def scroll_right(self): - self.__offset += 1 - def blocks(self, module): blocks = [] - if module.minimized: - blocks.extend(self.separator_block(module, module.widgets()[0])) - blocks.append(self.__content_block(module, module.widgets()[0])) - self.__widgetcount += 1 - return blocks - - width = self.__config.get("output.width", 0) for widget in module.widgets(): - if module.scroll() == True and width > 0: - self.__widgetcount += 1 - if self.__widgetcount-1 < self.__offset: - continue - if self.__widgetcount-1 >= self.__offset + width: - continue if widget.module and self.__config.autohide(widget.module.name): if not any( - state in widget.state() for state in ["warning", "critical", "no-autohide"] + state in widget.state() for state in ["warning", "critical"] ): continue if module.hidden(): continue - if widget.hidden: - continue - if "critical" in widget.state() and self.__config.errorhide(widget.module.name): - continue blocks.extend(self.separator_block(module, widget)) blocks.append(self.__content_block(module, widget)) core.event.trigger("next-widget") - core.event.trigger("output.done", self.__offset, self.__widgetcount) return blocks - def update(self, affected_modules=None, redraw_only=False, force=False): - with self.__lock: - self.update2(affected_modules, redraw_only, force) - - def update2(self, affected_modules=None, redraw_only=False, force=False): + # TODO: only updates full text, not the state!? + def update(self, affected_modules=None, redraw_only=False): now = time.time() for module in self.__modules: if affected_modules and not module.id in affected_modules: continue if not affected_modules and module.next_update: - if now < module.next_update and not force: + if module.parameter("interval", "") == "never": + continue + if now < module.next_update: continue - if not redraw_only: module.update_wrapper() if module.parameter("interval", "") != "never": module.next_update = now + util.format.seconds( module.parameter("interval", self.__config.interval()) ) - else: - module.next_update = sys.maxsize for widget in module.widgets(): - if not widget.id in self.__content: - self.__content[widget.id] = { "minimized": False } - self.__content[widget.id]["text"] = widget.full_text() + self.__content[widget] = widget.full_text() def statusline(self): blocks = [] - self.__widgetcount = 0 for module in self.__modules: blocks.extend(self.blocks(module)) return {"blocks": blocks, "suffix": ","} diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index 1426450..f50286a 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -7,27 +7,16 @@ import glob import core.event import util.algorithm -import util.xresources log = logging.getLogger(__name__) THEME_BASE_DIR = os.path.dirname(os.path.realpath(__file__)) PATHS = [ ".", - os.path.join(THEME_BASE_DIR, "../../themes") -] - -if os.environ.get("XDG_DATA_DIRS"): - PATHS.extend([ - os.path.join(p, "bumblebee-status/themes") for p in os.environ["XDG_DATA_DIRS"].split(":") - ]) - -PATHS.extend([ + os.path.join(THEME_BASE_DIR, "../../themes"), os.path.expanduser("~/.config/bumblebee-status/themes"), os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP - os.path.expanduser("~/.local/pipx/venvs/bumblebee-status/share/bumblebee-status/themes"), # PIPX - "/usr/share/bumblebee-status/themes", -]) +] def themes(): @@ -64,7 +53,7 @@ class Theme(object): self.__data = raw_data if raw_data else self.load(name) for icons in self.__data.get("icons", []): - self.__data = util.algorithm.merge(self.__data, self.load(icons, "icons")) + self.__data = util.algorithm.merge(self.load(icons, "icons"), self.__data) if iconset != "auto": self.__data = util.algorithm.merge(self.load(iconset, "icons"), self.__data) for colors in self.__data.get("colors", []): @@ -100,21 +89,13 @@ class Theme(object): try: if isinstance(name, dict): return name - - result = {} if name.lower() == "wal": wal = self.__load_json("~/.cache/wal/colors.json") + result = {} for field in ["special", "colors"]: for key in wal.get(field, {}): result[key] = wal[field][key] - 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: log.error("failed to load colors: {}", e) diff --git a/bumblebee_status/core/widget.py b/bumblebee_status/core/widget.py index 5d823ea..dd9fe1f 100644 --- a/bumblebee_status/core/widget.py +++ b/bumblebee_status/core/widget.py @@ -10,13 +10,11 @@ log = logging.getLogger(__name__) class Widget(util.store.Store, core.input.Object): - def __init__(self, full_text="", name=None, widget_id=None, hidden=False): + def __init__(self, full_text="", name=None): super(Widget, self).__init__() self.__full_text = full_text self.module = None self.name = name - self.id = widget_id or self.id - self.hidden = hidden @property def module(self): diff --git a/bumblebee_status/modules/contrib/amixer.py b/bumblebee_status/modules/contrib/amixer.py index 6f44e8c..4ab30f9 100644 --- a/bumblebee_status/modules/contrib/amixer.py +++ b/bumblebee_status/modules/contrib/amixer.py @@ -1,18 +1,12 @@ """get volume level or control it -Requires the following executable: - * amixer - Parameters: - * amixer.card: Sound Card to use (default is 0) * amixer.device: Device to use (default is Master,0) * amixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) contributed by `zetxx `_ - many thanks! input handling contributed by `ardadem `_ - many thanks! - -multiple audio cards contributed by `hugoeustaquio `_ - many thanks! """ import re @@ -29,7 +23,6 @@ class Module(core.module.Module): self.__level = "n/a" self.__muted = True - self.__card = self.parameter("card", "0") self.__device = self.parameter("device", "Master,0") self.__change = util.format.asint( self.parameter("percent_change", "4%").strip("%"), 0, 100 @@ -66,7 +59,7 @@ class Module(core.module.Module): self.set_parameter("{}%-".format(self.__change)) def set_parameter(self, parameter): - util.cli.execute("amixer -c {} -q set {} {}".format(self.__card, self.__device, parameter)) + util.cli.execute("amixer -q set {} {}".format(self.__device, parameter)) def volume(self, widget): if self.__level == "n/a": @@ -83,7 +76,7 @@ class Module(core.module.Module): def update(self): try: self.__level = util.cli.execute( - "amixer -c {} get {}".format(self.__card, self.__device) + "amixer get {}".format(self.__device) ) except Exception as e: self.__level = "n/a" diff --git a/bumblebee_status/modules/contrib/apt.py b/bumblebee_status/modules/contrib/apt.py index 575968f..2a41aea 100644 --- a/bumblebee_status/modules/contrib/apt.py +++ b/bumblebee_status/modules/contrib/apt.py @@ -14,7 +14,6 @@ import threading import core.module import core.widget import core.decorators -import core.input import util.cli @@ -57,8 +56,6 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.updates)) self.__thread = None - core.input.register(self, button=core.input.RIGHT_MOUSE, - cmd=self.updates) def updates(self, widget): if widget.get("error"): @@ -68,7 +65,7 @@ class Module(core.module.Module): ) def update(self): - if self.__thread and self.__thread.is_alive(): + if self.__thread and self.__thread.isAlive(): return self.__thread = threading.Thread(target=get_apt_check_info, args=(self,)) diff --git a/bumblebee_status/modules/contrib/arandr.py b/bumblebee_status/modules/contrib/arandr.py index 8328f7e..7af565d 100644 --- a/bumblebee_status/modules/contrib/arandr.py +++ b/bumblebee_status/modules/contrib/arandr.py @@ -8,9 +8,6 @@ saved screen layout as well as toggle on/off individual connected displays. Parameters: * No configuration parameters -Requires the following python modules: - * tkinter - Requires the following executable: * arandr * xrandr @@ -57,7 +54,7 @@ class Module(core.module.Module): def activate_layout(layout_path): log.debug("activating layout") log.debug(layout_path) - execute(layout_path, ignore_errors=True) + execute(layout_path) def popup(self, widget): """Create Popup that allows the user to control their displays in one @@ -67,7 +64,7 @@ class Module(core.module.Module): menu = popup.menu() menu.add_menuitem( "arandr", - callback=partial(execute, self.manager, ignore_errors=True) + callback=partial(execute, self.manager) ) menu.add_separator() @@ -108,12 +105,11 @@ class Module(core.module.Module): if count_on == 1: log.info("attempted to turn off last display") return - execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True) + execute("{} --output {} --off".format(self.toggle_cmd, display)) else: log.debug("toggling on {}".format(display)) execute( - "{} --output {} --auto".format(self.toggle_cmd, display), - ignore_errors=True + "{} --output {} --auto".format(self.toggle_cmd, display) ) @staticmethod @@ -124,7 +120,7 @@ class Module(core.module.Module): connected). """ displays = {} - for line in execute("xrandr -q", ignore_errors=True).split("\n"): + for line in execute("xrandr -q").split("\n"): if "connected" not in line: continue is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line)) @@ -140,19 +136,16 @@ class Module(core.module.Module): def _get_layouts(): """Loads and parses the arandr screen layout scripts.""" layouts = {} - try: - for filename in os.listdir(__screenlayout_dir__): - if fnmatch.fnmatch(filename, '*.sh'): - fullpath = os.path.join(__screenlayout_dir__, filename) - with open(fullpath, "r") as file: - for line in file: - s_line = line.strip() - if "xrandr" not in s_line: - continue - displays_in_file = Module._parse_layout(line) - layouts[filename] = displays_in_file - except Exception as e: - log.error(str(e)) + for filename in os.listdir(__screenlayout_dir__): + if fnmatch.fnmatch(filename, '*.sh'): + fullpath = os.path.join(__screenlayout_dir__, filename) + with open(fullpath, "r") as file: + for line in file: + s_line = line.strip() + if "xrandr" not in s_line: + continue + displays_in_file = Module._parse_layout(line) + layouts[filename] = displays_in_file return layouts @staticmethod diff --git a/bumblebee_status/modules/contrib/arch-update.py b/bumblebee_status/modules/contrib/arch-update.py index d8da8fb..c93545b 100644 --- a/bumblebee_status/modules/contrib/arch-update.py +++ b/bumblebee_status/modules/contrib/arch-update.py @@ -7,7 +7,6 @@ contributed by `lucassouto `_ - many thanks! """ import logging -from time import sleep import core.module import core.widget @@ -36,13 +35,12 @@ class Module(core.module.Module): def update(self): self.__error = False - sleep(1) code, result = util.cli.execute( "checkupdates", ignore_errors=True, return_exitcode=True ) if code == 0: - self.__packages = len(result.strip().split("\n")) + self.__packages = len(result.split("\n")) elif code == 2: self.__packages = 0 else: diff --git a/bumblebee_status/modules/contrib/arch_update.py b/bumblebee_status/modules/contrib/arch_update.py deleted file mode 120000 index 57fd99f..0000000 --- a/bumblebee_status/modules/contrib/arch_update.py +++ /dev/null @@ -1 +0,0 @@ -arch-update.py \ No newline at end of file diff --git a/bumblebee_status/modules/contrib/aur-update.py b/bumblebee_status/modules/contrib/aur-update.py deleted file mode 100644 index 9afdc89..0000000 --- a/bumblebee_status/modules/contrib/aur-update.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Check updates for AUR. - -Requires the following executable: - * yay (https://github.com/Jguer/yay) - -contributed by `ishaanbhimwal `_ - many thanks! -""" - -import logging - -import core.module -import core.widget -import core.decorators - -import util.cli - - -class Module(core.module.Module): - @core.decorators.every(minutes=60) - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.utilization)) - self.background = True - self.__packages = 0 - self.__error = False - - @property - def __format(self): - return self.parameter("format", "Update AUR: {}") - - def utilization(self, widget): - return self.__format.format(self.__packages) - - def hidden(self): - return self.__packages == 0 - - def update(self): - self.__error = False - code, result = util.cli.execute( - "yay -Qum", ignore_errors=True, return_exitcode=True - ) - - if code == 0: - if result == "": - self.__packages = 0 - else: - self.__packages = len(result.strip().split("\n")) - else: - self.__error = True - logging.error("aur-update exited with {}: {}".format(code, result)) - - def state(self, widget): - if self.__error: - return "warning" - return self.threshold_state(self.__packages, 1, 100) - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/battery-upower.py b/bumblebee_status/modules/contrib/battery-upower.py index ff65acd..9723058 100644 --- a/bumblebee_status/modules/contrib/battery-upower.py +++ b/bumblebee_status/modules/contrib/battery-upower.py @@ -68,7 +68,7 @@ class UPowerManager: isPresent = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "IsPresent" ) - isRechargeable = battery_proxy_interface.Get( + isRechargable = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "IsRechargeable" ) online = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Online") @@ -128,7 +128,7 @@ class UPowerManager: "HasHistory": hasHistory, "HasStatistics": hasStatistics, "IsPresent": isPresent, - "IsRechargeable": isRechargeable, + "IsRechargeable": isRechargable, "Online": online, "PowerSupply": powersupply, "Capacity": capacity, @@ -207,14 +207,6 @@ class UPowerManager: data = upower_interface.GetTotal() return data - def is_battery_present(self, battery): - battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery) - battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES) - - return bool( - battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "IsPresent") - ) - def is_loading(self, battery): battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery) battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES) @@ -267,11 +259,6 @@ class Module(core.module.Module): widget.set("capacity", -1) widget.set("ac", False) output = "n/a" - if not self.power.is_battery_present(self.device): - widget.set("ac", True) - widget.set("capacity", 100) - output = "ac" - return output try: capacity = int(self.power.get_device_percentage(self.device)) capacity = capacity if capacity < 100 else 100 @@ -311,6 +298,11 @@ class Module(core.module.Module): if capacity < 0: return ["critical", "unknown"] + if capacity < int(self.parameter("critical", 10)): + state.append("critical") + elif capacity < int(self.parameter("warning", 20)): + state.append("warning") + if widget.get("ac"): state.append("AC") else: @@ -336,16 +328,6 @@ class Module(core.module.Module): state.append("charged") else: state.append("charging") - if ( - capacity < int(self.parameter("critical", 10)) - and self.power.get_state(self.device) == "Discharging" - ): - state.append("critical") - elif ( - capacity < int(self.parameter("warning", 20)) - and self.power.get_state(self.device) == "Discharging" - ): - state.append("warning") return state diff --git a/bumblebee_status/modules/contrib/battery.py b/bumblebee_status/modules/contrib/battery.py index 21951c1..2db2031 100644 --- a/bumblebee_status/modules/contrib/battery.py +++ b/bumblebee_status/modules/contrib/battery.py @@ -130,19 +130,10 @@ class Module(core.module.Module): log.debug("adding new widget for {}".format(battery)) widget = self.add_widget(full_text=self.capacity, name=battery) - try: - with open("/sys/class/power_supply/{}/model_name".format(battery)) as f: - widget.set("pen", ("Pen" in f.read().strip())) - except Exception: - pass - - if util.format.asbool(self.parameter("decorate", True)) == False: - for widget in self.widgets(): + for w in self.widgets(): + if util.format.asbool(self.parameter("decorate", True)) == False: widget.set("theme.exclude", "suffix") - def hidden(self): - return len(self._batteries) == 0 - def ac(self, widget): return "ac" @@ -153,16 +144,15 @@ class Module(core.module.Module): capacity = self.__manager.capacity(widget.name) widget.set("capacity", capacity) widget.set("ac", self.__manager.isac_any(self._batteries)) + widget.set("theme.minwidth", "100%") # Read power conumption if util.format.asbool(self.parameter("showpowerconsumption", False)): output = "{}% ({})".format( capacity, self.__manager.consumption(widget.name) ) - elif capacity < 100: - output = "{}%".format(capacity) else: - output = "" + output = "{}%".format(capacity) if ( util.format.asbool(self.parameter("showremaining", True)) @@ -174,16 +164,6 @@ class Module(core.module.Module): output, util.format.duration(remaining, compact=True, unit=True) ) -# if bumblebee.util.asbool(self.parameter("rate", True)): -# try: -# with open("{}/power_now".format(widget.name)) as f: -# rate = (float(f.read())/1000000) -# if rate > 0: -# output = "{} {:.2f}w".format(output, rate) -# except Exception: -# pass - - if util.format.asbool(self.parameter("showdevice", False)): output = "{} ({})".format(output, widget.name) @@ -193,13 +173,15 @@ class Module(core.module.Module): state = [] capacity = widget.get("capacity") - if widget.get("pen"): - state.append("PEN") - if capacity < 0: log.debug("battery state: {}".format(state)) return ["critical", "unknown"] + if capacity < int(self.parameter("critical", 10)): + state.append("critical") + elif capacity < int(self.parameter("warning", 20)): + state.append("warning") + if widget.get("ac"): state.append("AC") else: @@ -207,10 +189,16 @@ class Module(core.module.Module): charge = self.__manager.charge_any(self._batteries) else: charge = self.__manager.charge(widget.name) - if charge in ["Discharging", "Unknown"]: + if charge == "Discharging": state.append( "discharging-{}".format( - min([5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], key=lambda i: abs(i - capacity)) + min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity)) + ) + ) + elif charge == "Unknown": + state.append( + "unknown-{}".format( + min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity)) ) ) else: @@ -218,18 +206,6 @@ class Module(core.module.Module): state.append("charged") else: state.append("charging") - - if ( - capacity < int(self.parameter("critical", 10)) - and self.__manager.charge_any(self._batteries) == "Discharging" - ): - state.append("critical") - elif ( - capacity < int(self.parameter("warning", 20)) - and self.__manager.charge_any(self._batteries) == "Discharging" - ): - state.append("warning") - return state diff --git a/bumblebee_status/modules/contrib/battery_upower.py b/bumblebee_status/modules/contrib/battery_upower.py deleted file mode 120000 index 4a7bb68..0000000 --- a/bumblebee_status/modules/contrib/battery_upower.py +++ /dev/null @@ -1 +0,0 @@ -battery-upower.py \ No newline at end of file diff --git a/bumblebee_status/modules/contrib/bluetooth.py b/bumblebee_status/modules/contrib/bluetooth.py index 56f5b8b..64fcc0b 100644 --- a/bumblebee_status/modules/contrib/bluetooth.py +++ b/bumblebee_status/modules/contrib/bluetooth.py @@ -1,4 +1,4 @@ -"""Displays bluetooth status (Bluez). Left mouse click launches manager app `blueman-manager`, +"""Displays bluetooth status (Bluez). Left mouse click launches manager app, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state. Parameters: @@ -75,15 +75,19 @@ class Module(core.module.Module): def popup(self, widget): """Show a popup menu.""" - menu = util.popup.menu(self.__config) + menu = util.popup.PopupMenu() if self._status == "On": - menu.add_menuitem("Disable Bluetooth", callback=self._toggle) + menu.add_menuitem("Disable Bluetooth") elif self._status == "Off": - menu.add_menuitem("Enable Bluetooth", callback=self._toggle) + menu.add_menuitem("Enable Bluetooth") else: return - menu.show(widget) + # show menu and get return code + ret = menu.show(widget) + if ret == 0: + # first (and only) item selected. + self._toggle() def _toggle(self, widget=None): """Toggle bluetooth state.""" @@ -102,7 +106,7 @@ class Module(core.module.Module): ) logging.debug("bt: toggling bluetooth") - util.cli.execute(cmd, ignore_errors=True) + util.cli.execute(cmd) def state(self, widget): """Get current state.""" diff --git a/bumblebee_status/modules/contrib/bluetooth2.py b/bumblebee_status/modules/contrib/bluetooth2.py index a9742ba..2bcdc32 100644 --- a/bumblebee_status/modules/contrib/bluetooth2.py +++ b/bumblebee_status/modules/contrib/bluetooth2.py @@ -1,4 +1,4 @@ -"""Displays bluetooth status. Left mouse click launches manager app `blueman-manager`, +"""Displays bluetooth status. Left mouse click launches manager app, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and python-dbus to count the number of connections @@ -8,6 +8,7 @@ Parameters: contributed by `martindoublem `_ - many thanks! """ + import os import re import subprocess @@ -21,6 +22,7 @@ import core.input import util.cli + class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.status)) @@ -35,7 +37,7 @@ class Module(core.module.Module): def status(self, widget): """Get status.""" - return self._status if self._status.isdigit() and int(self._status) > 1 else "" + return self._status def update(self): """Update current state.""" @@ -44,7 +46,7 @@ class Module(core.module.Module): ) if state > 0: connected_devices = self.get_connected_devices() - self._status = "{}".format(connected_devices) + self._status = "On - {}".format(connected_devices) else: self._status = "Off" adapters_cmd = "rfkill list | grep Bluetooth" @@ -56,23 +58,31 @@ class Module(core.module.Module): def _toggle(self, widget=None): """Toggle bluetooth state.""" - logging.debug("bt: toggling bluetooth") + if "On" in self._status: + state = "false" + else: + state = "true" - SetRfkillState = self._bus.get_object("org.blueman.Mechanism", "/org/blueman/mechanism").get_dbus_method("SetRfkillState", dbus_interface="org.blueman.Mechanism") - SetRfkillState(self._status == "Off") + cmd = ( + "dbus-send --system --print-reply --dest=org.blueman.Mechanism /org/blueman/mechanism org.blueman.Mechanism.SetRfkillState boolean:%s" + % state + ) + + logging.debug("bt: toggling bluetooth") + core.util.execute(cmd) def state(self, widget): """Get current state.""" state = [] - if self._status in [ "No Adapter Found", "Off" ]: + if self._status == "No Adapter Found": state.append("critical") - elif self._status == "0": - state.append("enabled") + elif self._status == "On - 0": + state.append("warning") + elif "On" in self._status and not (self._status == "On - 0"): + state.append("ON") else: - state.append("connected") - state.append("good") - + state.append("critical") return state def get_connected_devices(self): @@ -82,8 +92,12 @@ class Module(core.module.Module): ).GetManagedObjects() for path, interfaces in objects.items(): if "org.bluez.Device1" in interfaces: - if dbus.Interface(self._bus.get_object("org.bluez", path), "org.freedesktop.DBus.Properties", ).Get("org.bluez.Device1", "Connected"): + if dbus.Interface( + self._bus.get_object("org.bluez", path), + "org.freedesktop.DBus.Properties", + ).Get("org.bluez.Device1", "Connected"): devices += 1 return devices + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/blugon.py b/bumblebee_status/modules/contrib/blugon.py deleted file mode 100644 index 83e0bb6..0000000 --- a/bumblebee_status/modules/contrib/blugon.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Displays temperature of blugon and Controls it. - -Use wheel up and down to change temperature, middle click to toggle and right click to reset temperature. - -Default Values: - * Minimum temperature: 1000 (red) - * Maximum temperature: 20000 (blue) - * Default temperature: 6600 - -Requires the following executable: - * blugon - -Parameters: - * blugon.step: The amount of increase/decrease on scroll (default: 200) - -contributed by `DTan13 ` -""" - -import core.module -import core.widget - -import util.cli -import util.format - -import os - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.full_text)) - self.__state = True - self.__default = 6600 - self.__step = ( - util.format.asint(self.parameter("step")) if self.parameter("step") else 200 - ) - self.__max, self.__min = 20000, 1000 - - file = open(os.path.expanduser("~/.config/blugon/current")) - self.__current = int(float(file.read())) - - events = [ - { - "type": "toggle", - "action": self.toggle, - "button": core.input.MIDDLE_MOUSE, - }, - { - "type": "blue", - "action": self.blue, - "button": core.input.WHEEL_UP, - }, - { - "type": "red", - "action": self.red, - "button": core.input.WHEEL_DOWN, - }, - { - "type": "reset", - "action": self.reset, - "button": core.input.RIGHT_MOUSE, - }, - ] - - for event in events: - core.input.register(self, button=event["button"], cmd=event["action"]) - - def set_temp(self): - temp = self.__current if self.__state else self.__default - util.cli.execute("blugon --setcurrent={}".format(temp)) - - def full_text(self, widget): - return self.__current if self.__state else self.__default - - def state(self, widget): - if not self.__state: - return ["critical"] - - def toggle(self, event): - self.__state = not self.__state - self.set_temp() - - def reset(self, event): - self.__current = 6600 - self.set_temp() - - def blue(self, event): - if self.__state and (self.__current < self.__max): - self.__current += self.__step - self.set_temp() - - def red(self, event): - if self.__state and (self.__current > self.__min): - self.__current -= self.__step - self.set_temp() - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/brightness.py b/bumblebee_status/modules/contrib/brightness.py index dc1691a..eb68a4f 100644 --- a/bumblebee_status/modules/contrib/brightness.py +++ b/bumblebee_status/modules/contrib/brightness.py @@ -2,15 +2,8 @@ """Displays the brightness of a display -The following executables can be used if `use_acpi` is not enabled: - * brightnessctl - * light - * xbacklight - Parameters: * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) - * brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true - * brightness.use_acpi: If set to true, read brightness directly from the sys ACPI interface, using the device specified in brightness.device_path (defaults to false) contributed by `TheEdgeOfRage `_ - many thanks! """ @@ -34,12 +27,8 @@ class Module(core.module.Module): self.__brightness = "n/a" self.__readcmd = None step = self.parameter("step", 2) - self.__device_path = self.find_device(self.parameter("device_path", "/sys/class/backlight/intel_backlight")) - if util.format.asbool(self.parameter("use_acpi", False)): - self.__readcmd = self.__acpi - # TODO: add setting - elif shutil.which("light"): + if shutil.which("light"): self.__readcmd = self.__light self.register_cmd("light -A {}%".format(step), "light -U {}%".format(step)) elif shutil.which("brightnessctl"): @@ -53,12 +42,6 @@ class Module(core.module.Module): "xbacklight +{}%".format(step), "xbacklight -{}%".format(step) ) - def find_device(self, device_path): - res = glob.glob(device_path) - if len(res) == 0: - return device_path - return res[0] - def register_cmd(self, up_cmd, down_cmd): core.input.register(self, button=core.input.WHEEL_UP, cmd=up_cmd) core.input.register(self, button=core.input.WHEEL_DOWN, cmd=down_cmd) @@ -66,18 +49,6 @@ class Module(core.module.Module): def brightness(self, widget): return self.__brightness - def __acpi(self): - try: - backlight = 1 - max_brightness = 1 - with open("{}/brightness".format(self.__device_path)) as f: - backlight = int(f.readline()) - with open("{}/max_brightness".format(self.__device_path)) as f: - max_brightness = int(f.readline()) - return float(backlight*100)/max_brightness - except: - return "unable to read brightness from {}".format(self.__device_path) - def __light(self): return util.cli.execute("light").strip() @@ -91,11 +62,7 @@ class Module(core.module.Module): def update(self): try: - tmp = self.__readcmd() - if isinstance(tmp, str): - self.__brightness = tmp - else: - self.__brightness = "{:3.0f}%".format(float(tmp)) + self.__brightness = "{:3.0f}%".format(float(self.__readcmd())) except: self.__brightness = "n/a" diff --git a/bumblebee_status/modules/contrib/cpu2.py b/bumblebee_status/modules/contrib/cpu2.py index e017f04..1512681 100644 --- a/bumblebee_status/modules/contrib/cpu2.py +++ b/bumblebee_status/modules/contrib/cpu2.py @@ -24,9 +24,9 @@ Parameters: * cpu2.fanspeed * cpu2.colored: 1 for colored per core load graph, 0 for mono (default) * cpu2.temp_pattern: pattern to look for in the output of 'sensors -u'; - required if cpu2.temp widget is used + required if cpu2.temp widged is used * cpu2.fan_pattern: pattern to look for in the output of 'sensors -u'; - required if cpu2.fanspeed widget is used + required if cpu2.fanspeed widged is used Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're lacking the aforementioned pattern settings or they have wrong values. diff --git a/bumblebee_status/modules/contrib/cpu3.py b/bumblebee_status/modules/contrib/cpu3.py deleted file mode 100644 index 5321012..0000000 --- a/bumblebee_status/modules/contrib/cpu3.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Multiwidget CPU module - -Can display any combination of: - - * max CPU frequency - * total CPU load in percents (integer value) - * per-core CPU load as graph - either mono or colored - * CPU temperature (in Celsius degrees) - * CPU fan speed - -Requirements: - - * the psutil Python module for the first three items from the list above - * sensors executable for the rest - -Parameters: - * cpu3.layout: Space-separated list of widgets to add. - Possible widgets are: - - * cpu3.maxfreq - * cpu3.cpuload - * cpu3.coresload - * cpu3.temp - * cpu3.fanspeed - * cpu3.colored: 1 for colored per core load graph, 0 for mono (default) - * cpu3.temp_json: json path to look for in the output of 'sensors -j'; - required if cpu3.temp widget is used - * cpu3.fan_json: json path to look for in the output of 'sensors -j'; - required if cpu3.fanspeed widget is used - -Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're -lacking the aforementioned json path settings or they have wrong values. - -Example json paths: - * `cpu3.temp_json="coretemp-isa-0000.Package id 0.temp1_input"` - * `cpu3.fan_json="thinkpad-isa-0000.fan1.fan1_input"` - -contributed by `SuperQ ` -based on cpu2 by `` -""" - -import json -import psutil - -import core.module - -import util.cli -import util.graph -import util.format - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, []) - - self.__layout = self.parameter( - "layout", "cpu3.maxfreq cpu3.cpuload cpu3.coresload cpu3.temp cpu3.fanspeed" - ) - self.__widget_names = self.__layout.split() - self.__colored = util.format.asbool(self.parameter("colored", False)) - for widget_name in self.__widget_names: - if widget_name == "cpu3.maxfreq": - widget = self.add_widget(name=widget_name, full_text=self.maxfreq) - widget.set("type", "freq") - elif widget_name == "cpu3.cpuload": - widget = self.add_widget(name=widget_name, full_text=self.cpuload) - widget.set("type", "load") - elif widget_name == "cpu3.coresload": - widget = self.add_widget(name=widget_name, full_text=self.coresload) - widget.set("type", "loads") - elif widget_name == "cpu3.temp": - widget = self.add_widget(name=widget_name, full_text=self.temp) - widget.set("type", "temp") - elif widget_name == "cpu3.fanspeed": - widget = self.add_widget(name=widget_name, full_text=self.fanspeed) - widget.set("type", "fan") - if self.__colored: - widget.set("pango", True) - self.__temp_json = self.parameter("temp_json") - if self.__temp_json is None: - self.__temp = "n/a" - self.__fan_json = self.parameter("fan_json") - if self.__fan_json is None: - self.__fan = "n/a" - # maxfreq is loaded only once at startup - if "cpu3.maxfreq" in self.__widget_names: - self.__maxfreq = psutil.cpu_freq().max / 1000 - - def maxfreq(self, _): - return "{:.2f}GHz".format(self.__maxfreq) - - def cpuload(self, _): - return "{:>3}%".format(self.__cpuload) - - def add_color(self, bar): - """add color as pango markup to a bar""" - if bar in ["▁", "▂"]: - color = self.theme.color("green", "green") - elif bar in ["▃", "▄"]: - color = self.theme.color("yellow", "yellow") - elif bar in ["▅", "▆"]: - color = self.theme.color("orange", "orange") - elif bar in ["▇", "█"]: - color = self.theme.color("red", "red") - colored_bar = '{}'.format(color, bar) - return colored_bar - - def coresload(self, _): - mono_bars = [util.graph.hbar(x) for x in self.__coresload] - if not self.__colored: - return "".join(mono_bars) - colored_bars = [self.add_color(x) for x in mono_bars] - return "".join(colored_bars) - - def temp(self, _): - if self.__temp == "n/a" or self.__temp == 0: - return "n/a" - return "{}°C".format(self.__temp) - - def fanspeed(self, _): - if self.__fanspeed == "n/a": - return "n/a" - return "{}RPM".format(self.__fanspeed) - - def _parse_sensors_output(self): - output = util.cli.execute("sensors -j") - json_data = json.loads(output) - - temp = "n/a" - fan = "n/a" - temp_json = json_data - fan_json = json_data - for path in self.__temp_json.split('.'): - temp_json = temp_json[path] - for path in self.__fan_json.split('.'): - fan_json = fan_json[path] - if temp_json is not None: - temp = float(temp_json) - if fan_json is not None: - fan = int(fan_json) - return temp, fan - - def update(self): - if "cpu3.maxfreq" in self.__widget_names: - self.__maxfreq = psutil.cpu_freq().max / 1000 - if "cpu3.cpuload" in self.__widget_names: - self.__cpuload = round(psutil.cpu_percent(percpu=False)) - if "cpu3.coresload" in self.__widget_names: - self.__coresload = psutil.cpu_percent(percpu=True) - if "cpu3.temp" in self.__widget_names or "cpu3.fanspeed" in self.__widget_names: - self.__temp, self.__fanspeed = self._parse_sensors_output() - - def state(self, widget): - """for having per-widget icons""" - return [widget.get("type", "")] - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/datetimetz.py b/bumblebee_status/modules/contrib/datetimetz.py index df4507f..e4baa4c 100644 --- a/bumblebee_status/modules/contrib/datetimetz.py +++ b/bumblebee_status/modules/contrib/datetimetz.py @@ -2,10 +2,6 @@ """Displays the current date and time with timezone options. -Requires the following python packages: - * tzlocal - * pytz - Parameters: * datetimetz.format : strftime()-compatible formatting string * datetimetz.timezone : IANA timezone name diff --git a/bumblebee_status/modules/contrib/deadbeef.py b/bumblebee_status/modules/contrib/deadbeef.py index 0bbba41..327972d 100644 --- a/bumblebee_status/modules/contrib/deadbeef.py +++ b/bumblebee_status/modules/contrib/deadbeef.py @@ -5,6 +5,8 @@ some media control bindings. Left click toggles pause, scroll up skips the current song, scroll down returns to the previous song. +Requires the following library: + * subprocess Parameters: * deadbeef.format: Format string (defaults to '{artist} - {title}') Available values are: {artist}, {title}, {album}, {length}, @@ -112,7 +114,7 @@ class Module(core.module.Module): self._song = "" return ## perform the actual query -- these can be much more sophisticated - data = util.cli.execute(self.now_playing_tf + '"'+self._tf_format+'"') + data = util.cli.execute(self.now_playing_tf + self._tf_format) self._song = data def update_standard(self, widgets): diff --git a/bumblebee_status/modules/contrib/dnf.py b/bumblebee_status/modules/contrib/dnf.py index f90ee84..3f5e484 100644 --- a/bumblebee_status/modules/contrib/dnf.py +++ b/bumblebee_status/modules/contrib/dnf.py @@ -5,8 +5,13 @@ Requires the following executable: * dnf +Parameters: + * dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes) + """ +import threading + import core.event import core.module import core.widget @@ -15,13 +20,46 @@ import core.decorators import util.cli +def get_dnf_info(widget): + res = util.cli.execute("dnf updateinfo", ignore_errors=True) + + security = 0 + bugfixes = 0 + enhancements = 0 + other = 0 + for line in res.split("\n"): + if not line.startswith(" "): + continue + elif "ecurity" in line: + for s in line.split(): + if s.isdigit(): + security += int(s) + elif "ugfix" in line: + for s in line.split(): + if s.isdigit(): + bugfixes += int(s) + elif "hancement" in line: + for s in line.split(): + if s.isdigit(): + enhancements += int(s) + else: + for s in line.split(): + if s.isdigit(): + other += int(s) + + widget.set("security", security) + widget.set("bugfixes", bugfixes) + widget.set("enhancements", enhancements) + widget.set("other", other) + + core.event.trigger("update", [widget.module.id], redraw_only=True) + + class Module(core.module.Module): @core.decorators.every(minutes=30) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.updates)) - self.background = True - def updates(self, widget): result = [] for t in ["security", "bugfixes", "enhancements", "other"]: @@ -29,38 +67,8 @@ class Module(core.module.Module): return "/".join(result) def update(self): - widget = self.widget() - res = util.cli.execute("dnf updateinfo", ignore_errors=True) - - security = 0 - bugfixes = 0 - enhancements = 0 - other = 0 - for line in res.split("\n"): - if not line.startswith(" "): - continue - elif "ecurity" in line: - for s in line.split(): - if s.isdigit(): - security += int(s) - elif "ugfix" in line: - for s in line.split(): - if s.isdigit(): - bugfixes += int(s) - elif "hancement" in line: - for s in line.split(): - if s.isdigit(): - enhancements += int(s) - else: - for s in line.split(): - if s.isdigit(): - other += int(s) - - widget.set("security", security) - widget.set("bugfixes", bugfixes) - widget.set("enhancements", enhancements) - widget.set("other", other) - + thread = threading.Thread(target=get_dnf_info, args=(self.widget(),)) + thread.start() def state(self, widget): cnt = 0 diff --git a/bumblebee_status/modules/contrib/dunstctl.py b/bumblebee_status/modules/contrib/dunstctl.py deleted file mode 100644 index 1ad43df..0000000 --- a/bumblebee_status/modules/contrib/dunstctl.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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 `_ - many thanks! -contributed by `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"]} - if util.format.asbool(self.parameter("disabled", False)): - util.cli.execute("dunstctl set-paused true", ignore_errors=True) - - 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" diff --git a/bumblebee_status/modules/contrib/emerge_status.py b/bumblebee_status/modules/contrib/emerge_status.py deleted file mode 100644 index 3758585..0000000 --- a/bumblebee_status/modules/contrib/emerge_status.py +++ /dev/null @@ -1,113 +0,0 @@ -"""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[\d]+) of (?P[\d]+)\) " - r"(?P[a-zA-Z/]+( [a-zA-Z]+)?) " - r"\((?P[\w\-]+)/(?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 diff --git a/bumblebee_status/modules/contrib/gcalendar.py b/bumblebee_status/modules/contrib/gcalendar.py deleted file mode 100644 index 6848ba8..0000000 --- a/bumblebee_status/modules/contrib/gcalendar.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Displays first upcoming event in google calendar. - -Events that are set as 'all-day' will not be shown. - -Requires credentials.json from a google api application where the google calendar api is installed. -On first time run the browser will open and google will ask for permission for this app to access -the google calendar and then save a .gcalendar_token.json file to the credentials_path directory -which stores this permission. - -A refresh is done every 15 minutes. - -Parameters: - * gcalendar.time_format: Format time output. Defaults to "%H:%M". - * gcalendar.date_format: Format date output. Defaults to "%d.%m.%y". - * gcalendar.credentials_path: Path to credentials.json. Defaults to "~/". - * gcalendar.locale: locale to use rather than the system default. - -Requires these pip packages: - * google-api-python-client >= 1.8.0 - * google-auth-httplib2 - * google-auth-oauthlib -""" - -# This import belongs to the google code -from __future__ import print_function - -from dateutil.parser import parse as dtparse - -import core.module -import core.widget -import core.decorators -import util.format - -import datetime -import os.path -import locale -import time - -from google.auth.transport.requests import Request -from google.oauth2.credentials import Credentials -from google_auth_oauthlib.flow import InstalledAppFlow -from googleapiclient.discovery import build -from googleapiclient.errors import HttpError - -# Minutes -update_every = 15 - - -class Module(core.module.Module): - @core.decorators.every(minutes=update_every) - def __init__(self, config, theme): - super().__init__(config, theme, [core.widget.Widget(self.__datetime), core.widget.Widget(self.__summary)]) - self.__error = False - self.__time_format = self.parameter("time_format", "%H:%M") - self.__date_format = self.parameter("date_format", "%d.%m.%y") - self.__credentials_path = os.path.expanduser( - self.parameter("credentials_path", "~/") - ) - self.__credentials = os.path.join(self.__credentials_path, "credentials.json") - self.__token = os.path.join(self.__credentials_path, ".gcalendar_token.json") - - l = locale.getdefaultlocale() - if not l or l == (None, None): - l = ("en_US", "UTF-8") - lcl = self.parameter("locale", ".".join(l)) - try: - locale.setlocale(locale.LC_TIME, lcl.split(".")) - except Exception: - locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8")) - - self.__last_update = time.time() - self.__gcalendar_date, self.__gcalendar_summary = self.__fetch_from_calendar() - - def hidden(self): - return self.__error - - def __datetime(self, _): - return self.__gcalendar_date - - @core.decorators.scrollable - def __summary(self, _): - return self.__gcalendar_summary - - - def __fetch_from_calendar(self): - SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] - - creds = None - - try: - # The file token.json stores the user's access and refresh tokens, and is - # created automatically when the authorization flow completes for the first - # time. - if os.path.exists(self.__token): - creds = Credentials.from_authorized_user_file(self.__token, SCOPES) - # If there are no (valid) credentials available, let the user log in. - if not creds or not creds.valid: - if creds and creds.expired and creds.refresh_token: - creds.refresh(Request()) - else: - flow = InstalledAppFlow.from_client_secrets_file( - self.__credentials, SCOPES - ) - creds = flow.run_local_server(port=0) - # Save the credentials for the next run - with open(self.__token, "w") as token: - token.write(creds.to_json()) - - service = build("calendar", "v3", credentials=creds) - - # Call the Calendar API - now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time - end = ( - datetime.datetime.utcnow() + datetime.timedelta(days=7) - ).isoformat() + "Z" # 'Z' indicates UTC time - # Get all calendars - calendar_list = service.calendarList().list().execute() - event_list = [] - for calendar_list_entry in calendar_list["items"]: - calendar_id = calendar_list_entry["id"] - events_result = ( - service.events() - .list( - calendarId=calendar_id, - timeMin=now, - timeMax=end, - singleEvents=True, - orderBy="startTime", - ) - .execute() - ) - events = events_result.get("items", []) - - for event in events: - start = dtparse( - event["start"].get("dateTime", event["start"].get("date")) - ) - # Only add to list if not an whole day event - if start.tzinfo: - event_list.append( - { - "date": start, - "summary": event["summary"], - "type": event["eventType"], - } - ) - sorted_list = sorted(event_list, key=lambda t: t["date"]) - next_event = sorted_list[0] - - if next_event["date"] >= datetime.datetime.now(datetime.timezone.utc): - if next_event["date"].date() == datetime.datetime.utcnow().date(): - dt = next_event["date"].astimezone()\ - .strftime(f"{self.__time_format}") - else: - dt = next_event["date"].astimezone()\ - .strftime(f"{self.__date_format} {self.__time_format}") - return (dt, next_event["summary"]) - - return (None, "No upcoming events.") - except: - self.__error = True - - def update(self): - # Since scrolling runs the update command and therefore negates the - # every decorator, this need to be stopped - # to not break the API rules of google. - if self.__last_update+(update_every*60) < time.time(): - self.__last_update = time.time() - self.__gcalendar_date, self.__gcalendar_summary = self.__fetch_from_calendar() - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/github.py b/bumblebee_status/modules/contrib/github.py index 07b8ea0..fc16df0 100644 --- a/bumblebee_status/modules/contrib/github.py +++ b/bumblebee_status/modules/contrib/github.py @@ -5,8 +5,6 @@ Displays the unread GitHub notifications count for a GitHub user using the follo * https://developer.github.com/v3/activity/notifications/#notification-reasons -Uses `xdg-open` or `x-www-browser` to open web-pages. - Requires the following library: * requests @@ -83,6 +81,7 @@ class Module(core.module.Module): self.__label += "/".join(counts) except Exception as err: + print(err) self.__label = "n/a" def __getUnreadNotificationsCountByReason(self, notifications, reason): diff --git a/bumblebee_status/modules/contrib/gitlab.py b/bumblebee_status/modules/contrib/gitlab.py deleted file mode 100644 index 24f2eb8..0000000 --- a/bumblebee_status/modules/contrib/gitlab.py +++ /dev/null @@ -1,87 +0,0 @@ -# pylint: disable=C0111,R0903 - -""" -Displays the GitLab todo count: - - * https://docs.gitlab.com/ee/user/todos.html - * https://docs.gitlab.com/ee/api/todos.html - -Uses `xdg-open` or `x-www-browser` to open web-pages. - -Requires the following library: - * requests - -Errors: - if the GitLab todo query failed, the shown value is `n/a` - -Parameters: - * gitlab.token: GitLab personal access token, the token needs to have the "read_api" scope. - * gitlab.host: Host of the GitLab instance, default is "gitlab.com". - * gitlab.actions: Comma separated actions to be parsed (e.g.: gitlab.actions=assigned,approval_required) -""" - -import shutil - -import requests - -import core.decorators -import core.input -import core.module -import core.widget -import util - - -class Module(core.module.Module): - @core.decorators.every(minutes=5) - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.gitlab)) - - self.background = True - self.__label = "" - self.__host = self.parameter("host", "gitlab.com") - - self.__actions = [] - actions = self.parameter("actions", "") - if actions: - self.__actions = util.format.aslist(actions) - - self.__requests = requests.Session() - self.__requests.headers.update({"PRIVATE-TOKEN": self.parameter("token", "")}) - - cmd = "xdg-open" - if not shutil.which(cmd): - cmd = "x-www-browser" - - core.input.register( - self, - button=core.input.LEFT_MOUSE, - cmd="{cmd} https:/{host}//dashboard/todos".format( - cmd=cmd, host=self.__host - ), - ) - - def gitlab(self, _): - return self.__label - - def update(self): - try: - url = "https://{host}/api/v4/todos".format(host=self.__host) - response = self.__requests.get(url) - todos = response.json() - if self.__actions: - todos = [t for t in todos if t["action_name"] in self.__actions] - self.__label = str(len(todos)) - except Exception as e: - self.__label = "n/a" - - def state(self, widget): - state = [] - - try: - if int(self.__label) > 0: - state.append("warning") - except ValueError: - pass - return state - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/hddtemp.py b/bumblebee_status/modules/contrib/hddtemp.py index 6e269cb..a039166 100644 --- a/bumblebee_status/modules/contrib/hddtemp.py +++ b/bumblebee_status/modules/contrib/hddtemp.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Fetch hard drive temperature data from a hddtemp daemon +"""Fetch hard drive temeperature data from a hddtemp daemon that runs on localhost and default port (7634) contributed by `somospocos `_ - many thanks! diff --git a/bumblebee_status/modules/contrib/indicator.py b/bumblebee_status/modules/contrib/indicator.py index 63445f2..6b01c41 100644 --- a/bumblebee_status/modules/contrib/indicator.py +++ b/bumblebee_status/modules/contrib/indicator.py @@ -2,9 +2,6 @@ """Displays the indicator status, for numlock, scrolllock and capslock -Requires the following executable: - * xset - Parameters: * indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock') * indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning') diff --git a/bumblebee_status/modules/contrib/layout-xkbswitch.py b/bumblebee_status/modules/contrib/layout-xkbswitch.py index a749522..767deb9 100644 --- a/bumblebee_status/modules/contrib/layout-xkbswitch.py +++ b/bumblebee_status/modules/contrib/layout-xkbswitch.py @@ -19,13 +19,13 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.current_layout)) - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_keymap) + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap) self.__current_layout = self.__get_current_layout() def current_layout(self, _): return self.__current_layout - def next_keymap(self, event): + def __next_keymap(self, event): util.cli.execute("xkb-switch -n", ignore_errors=True) def __get_current_layout(self): diff --git a/bumblebee_status/modules/contrib/layout_xkbswitch.py b/bumblebee_status/modules/contrib/layout_xkbswitch.py deleted file mode 120000 index e7d6b94..0000000 --- a/bumblebee_status/modules/contrib/layout_xkbswitch.py +++ /dev/null @@ -1 +0,0 @@ -layout-xkbswitch.py \ No newline at end of file diff --git a/bumblebee_status/modules/contrib/messagereceiver.py b/bumblebee_status/modules/contrib/messagereceiver.py deleted file mode 100644 index 74943cd..0000000 --- a/bumblebee_status/modules/contrib/messagereceiver.py +++ /dev/null @@ -1,85 +0,0 @@ -# pylint: disable=C0111,R0903 - -""" -Displays the message that's received via unix socket. - -Parameters: - * messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock) - -Example: - The following examples assume that /tmp/bumblebee_messagereceiver.sock is used as unix socket address. - - In order to send the string "I  bumblebee-status" to your status bar, use the following command: - echo -e '{"message":"I  bumblebee-status", "state": ""}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO - - In order to highlight the text, the state variable can be used: - echo -e '{"message":"I  bumblebee-status", "state": "warning"}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO - -contributed by `bbernhard `_ - many thanks! -""" - -import socket -import logging -import os -import json - -import core.module -import core.widget -import core.input - - -class Module(core.module.Module): - @core.decorators.never - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.message)) - - self.background = True - - self.__unix_socket_address = self.parameter("address", "") - - self.__message = "" - self.__state = [] - - def message(self, widget): - return self.__message - - def __read_data_from_socket(self): - while True: - try: - os.unlink(self.__unix_socket_address) - except OSError: - if os.path.exists(self.__unix_socket_address): - logging.exception( - "Couldn't bind to unix socket %s", self.__unix_socket_address - ) - raise - - with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: - s.bind(self.__unix_socket_address) - s.listen() - - conn, _ = s.accept() - with conn: - while True: - data = conn.recv(1024) - if not data: - break - yield data.decode("utf-8") - - def update(self): - try: - for received_data in self.__read_data_from_socket(): - parsed_data = json.loads(received_data) - self.__message = parsed_data["message"] - self.__state = parsed_data["state"] - core.event.trigger("update", [self.id], redraw_only=True) - except json.JSONDecodeError: - logging.exception("Couldn't parse message") - except Exception: - logging.exception("Unexpected exception while reading from socket") - - def state(self, widget): - return self.__state - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/mpd.py b/bumblebee_status/modules/contrib/mpd.py index 35cb2f6..3efffff 100644 --- a/bumblebee_status/modules/contrib/mpd.py +++ b/bumblebee_status/modules/contrib/mpd.py @@ -42,7 +42,6 @@ Parameters: if {file} = '/foo/bar.baz', then {file2} = 'bar' * mpd.host: MPD host to connect to. (mpc behaviour by default) - * mpd.port: MPD port to connect to. (mpc behaviour by default) * mpd.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles mpd.prev, mpd.next, mpd.shuffle and mpd.repeat, and the main display with play/pause function mpd.main. contributed by `alrayyes `_ - many thanks! @@ -74,12 +73,10 @@ class Module(core.module.Module): self._repeat = False self._tags = defaultdict(lambda: "") - self._hostcmd = "" - if self.parameter("host"): - self._hostcmd = " -h {}".format(self.parameter("host")) - if self.parameter("port"): - self._hostcmd += " -p {}".format(self.parameter("port")) - + if not self.parameter("host"): + self._hostcmd = "" + else: + self._hostcmd = " -h " + self.parameter("host") # Create widgets widget_map = {} @@ -97,12 +94,6 @@ class Module(core.module.Module): "cmd": "mpc toggle" + self._hostcmd, } widget.full_text(self.description) - elif widget_name == "mpd.toggle": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": "mpc toggle" + self._hostcmd, - } - widget.full_text(self.toggle) elif widget_name == "mpd.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, @@ -136,9 +127,6 @@ class Module(core.module.Module): def description(self, widget): return string.Formatter().vformat(self._fmt, (), self._tags) - def toggle(self, widget): - return str(util.cli.execute("mpc status %currenttime%/%totaltime%", ignore_errors=True)).strip() - def update(self): self._load_song() diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py deleted file mode 100644 index a91c947..0000000 --- a/bumblebee_status/modules/contrib/network.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -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 - diff --git a/bumblebee_status/modules/contrib/network_traffic.py b/bumblebee_status/modules/contrib/network_traffic.py index ad5781f..bd1a9b6 100644 --- a/bumblebee_status/modules/contrib/network_traffic.py +++ b/bumblebee_status/modules/contrib/network_traffic.py @@ -38,8 +38,8 @@ class Module(core.module.Module): try: self._bandwidth = BandwidthInfo() - self._rate_recv = 0 - self._rate_sent = 0 + self._rate_recv = "?" + self._rate_sent = "?" self._bytes_recv = self._bandwidth.bytes_recv() self._bytes_sent = self._bandwidth.bytes_sent() except Exception: @@ -97,6 +97,9 @@ class BandwidthInfo(object): """Return default active network adapter""" gateway = netifaces.gateways()["default"] + if not gateway: + raise "No default gateway found" + return gateway[netifaces.AF_INET][1] @classmethod diff --git a/bumblebee_status/modules/contrib/nvidiagpu.py b/bumblebee_status/modules/contrib/nvidiagpu.py index 3b929a2..4aa9de9 100644 --- a/bumblebee_status/modules/contrib/nvidiagpu.py +++ b/bumblebee_status/modules/contrib/nvidiagpu.py @@ -4,15 +4,11 @@ Parameters: * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB') - Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct} + Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} Requires nvidia-smi contributed by `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 @@ -45,9 +41,6 @@ class Module(core.module.Module): clockMem = "" clockGpu = "" fanspeed = "" - gpuUsagePct = "" - memIoPct = "" - memUsage = "not found" for item in sp.split("\n"): try: key, val = item.split(":") @@ -68,18 +61,10 @@ class Module(core.module.Module): name = val elif key == "Fan Speed": fanspeed = val.split(" ")[0] - elif title == "Utilization": - if key == "Gpu": - gpuUsagePct = val.split(" ")[0] - elif key == "Memory": - memIoPct = val.split(" ")[0] except: title = item.strip() - if totalMem and usedMem: - memUsage = int(int(usedMem) / int(totalMem) * 100) - str_format = self.parameter( "format", "{name}: {temp}°C {mem_used}/{mem_total} MiB" ) @@ -91,9 +76,6 @@ class Module(core.module.Module): clock_gpu=clockGpu, clock_mem=clockMem, fanspeed=fanspeed, - gpu_usage_pct=gpuUsagePct, - mem_io_pct=memIoPct, - mem_usage_pct=memUsage, ) diff --git a/bumblebee_status/modules/contrib/octoprint.py b/bumblebee_status/modules/contrib/octoprint.py index a7ed8ac..ce1aa89 100644 --- a/bumblebee_status/modules/contrib/octoprint.py +++ b/bumblebee_status/modules/contrib/octoprint.py @@ -1,12 +1,9 @@ # pylint: disable=C0111,R0903 -"""Displays the Octorrint status and the printer's bed/tools temperature in the status bar. +"""Displays the Octorpint status and the printer's bed/tools temperature in the status bar. Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled). -Prerequisites: - * tk python library (usually python-tk or python3-tk, depending on your distribution) - Parameters: * octoprint.address : Octoprint address (e.q: http://192.168.1.3) * octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface) @@ -85,15 +82,8 @@ class Module(core.module.Module): core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup) def octoprint_status(self, widget): - if ( - self.__octoprint_state.startswith("Offline") - or self.__octoprint_state == "Unknown" - ): - return ( - (self.__octoprint_state[:25] + "...") - if len(self.__octoprint_state) > 25 - else self.__octoprint_state - ) + if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown": + return self.__octoprint_state return ( self.__octoprint_state + " | B: " diff --git a/bumblebee_status/modules/contrib/optman.py b/bumblebee_status/modules/contrib/optman.py deleted file mode 100644 index 337003c..0000000 --- a/bumblebee_status/modules/contrib/optman.py +++ /dev/null @@ -1,30 +0,0 @@ -"""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" diff --git a/bumblebee_status/modules/contrib/pacman.py b/bumblebee_status/modules/contrib/pacman.py index cb3de96..b50ed53 100644 --- a/bumblebee_status/modules/contrib/pacman.py +++ b/bumblebee_status/modules/contrib/pacman.py @@ -3,7 +3,7 @@ """Displays update information per repository for pacman. Parameters: - * pacman.sum: If you prefer displaying updates with a single digit (defaults to 'False') + * pacman.sum: If you prefere displaying updates with a single digit (defaults to 'False') Requires the following executables: * fakeroot diff --git a/bumblebee_status/modules/contrib/pamixer.py b/bumblebee_status/modules/contrib/pamixer.py deleted file mode 100644 index 46f3bb5..0000000 --- a/bumblebee_status/modules/contrib/pamixer.py +++ /dev/null @@ -1,88 +0,0 @@ -"""get volume level or control it - -Requires the following executable: - * pamixer - -Parameters: - * pamixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) - -heavily based on amixer module -""" -import re - -import core.module -import core.widget -import core.input - -import util.cli -import util.format - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.volume)) - - self.__level = "volume 0%" - self.__muted = True - self.__change = util.format.asint( - self.parameter("percent_change", "4%").strip("%"), 0, 200 - ) - - events = [ - { - "type": "mute", - "action": self.toggle, - "button": core.input.LEFT_MOUSE, - }, - { - "type": "volume", - "action": self.increase_volume, - "button": core.input.WHEEL_UP, - }, - { - "type": "volume", - "action": self.decrease_volume, - "button": core.input.WHEEL_DOWN, - }, - ] - - for event in events: - core.input.register(self, button=event["button"], cmd=event["action"]) - - def toggle(self, event): - self.set_parameter("--toggle-mute") - - def increase_volume(self, event): - self.set_parameter("--increase {}".format(self.__change)) - - def decrease_volume(self, event): - self.set_parameter("--decrease {}".format(self.__change)) - - def set_parameter(self, parameter): - util.cli.execute("pamixer {}".format(parameter)) - - def volume(self, widget): - if self.__level == "volume 0%": - self.__muted = True - return self.__level - m = re.search(r"([\d]+)\%", self.__level) - if m: - if m.group(1) != "0%" in self.__level: - self.__muted = False - return "volume {}%".format(m.group(1)) - else: - return "volume 0%" - - def update(self): - try: - volume = util.cli.execute("pamixer --get-volume-human".format()) - self.__level = volume - self.__muted = False - except Exception as e: - self.__level = "volume 0%" - - def state(self, widget): - if self.__muted: - return ["warning", "muted"] - return ["unmuted"] - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/persian_date.py b/bumblebee_status/modules/contrib/persian_date.py deleted file mode 100644 index 9873e1a..0000000 --- a/bumblebee_status/modules/contrib/persian_date.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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" -""" - -import jdatetime - -import core.decorators -from modules.core.datetime import Module as dtmodule - - -class Module(dtmodule): - @core.decorators.every(minutes=1) - def __init__(self, config, theme): - super().__init__(config, theme, dtlibrary=jdatetime) - - def default_format(self): - return "%A %d %B" - - def default_locale(self): - return ("fa_IR", "UTF-8") - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/pihole.py b/bumblebee_status/modules/contrib/pihole.py index 6e07287..7334abf 100644 --- a/bumblebee_status/modules/contrib/pihole.py +++ b/bumblebee_status/modules/contrib/pihole.py @@ -4,20 +4,13 @@ Parameters: * pihole.address : pi-hole address (e.q: http://192.168.1.3) - - - * pihole.apitoken : pi-hole API token (can be obtained in the pi-hole webinterface (Settings -> API) - - OR (deprecated!) - - * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) - + * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) contributed by `bbernhard `_ - many thanks! """ import requests -import logging + import core.module import core.widget import core.input @@ -29,18 +22,7 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.pihole_status)) self._pihole_address = self.parameter("address", "") - pihole_pw_hash = self.parameter("pwhash", "") - pihole_api_token = self.parameter("apitoken", "") - - self._pihole_secret = ( - pihole_api_token if pihole_api_token != "" else pihole_pw_hash - ) - - if pihole_pw_hash != "": - logging.warn( - "pihole: The 'pwhash' parameter is deprecated - consider using the 'apitoken' parameter instead!" - ) - + self._pihole_pw_hash = self.parameter("pwhash", "") self._pihole_status = None self._ads_blocked_today = "-" self.update_pihole_status() @@ -60,11 +42,7 @@ class Module(core.module.Module): def update_pihole_status(self): try: - data = requests.get( - self._pihole_address - + "/admin/api.php?summary&auth=" - + self._pihole_secret - ).json() + data = requests.get(self._pihole_address + "/admin/api.php?summary").json() self._pihole_status = True if data["status"] == "enabled" else False self._ads_blocked_today = data["ads_blocked_today"] except Exception as e: @@ -78,13 +56,13 @@ class Module(core.module.Module): req = requests.get( self._pihole_address + "/admin/api.php?disable&auth=" - + self._pihole_secret + + self._pihole_pw_hash ) else: req = requests.get( self._pihole_address + "/admin/api.php?enable&auth=" - + self._pihole_secret + + self._pihole_pw_hash ) if req is not None: if req.status_code == 200: diff --git a/bumblebee_status/modules/contrib/pipewire.py b/bumblebee_status/modules/contrib/pipewire.py deleted file mode 100644 index 86c9b9d..0000000 --- a/bumblebee_status/modules/contrib/pipewire.py +++ /dev/null @@ -1,90 +0,0 @@ -"""get volume level or control it - -Requires the following executable: - * wpctl - -Parameters: - * wpctl.percent_change: How much to change volume by when scrolling on the module (default is 4%) - -heavily based on amixer module -""" -import re - -import core.module -import core.widget -import core.input - -import util.cli -import util.format - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.volume)) - - self.__level = "N/A" - self.__muted = True - self.__change = ( - util.format.asint(self.parameter("percent_change", "4%").strip("%"), 0, 200) - / 100.0 - ) # divide by 100 because wpctl represents 100% volume as 1.00, 50% as 0.50, etc - - self.__id = self.parameter("sink_id") or "@DEFAULT_AUDIO_SINK@" - - events = [ - { - "type": "mute", - "action": self.toggle, - "button": core.input.LEFT_MOUSE, - }, - { - "type": "volume", - "action": self.increase_volume, - "button": core.input.WHEEL_UP, - }, - { - "type": "volume", - "action": self.decrease_volume, - "button": core.input.WHEEL_DOWN, - }, - ] - - for event in events: - core.input.register(self, button=event["button"], cmd=event["action"]) - - def toggle(self, event): - util.cli.execute("wpctl set-mute {} toggle".format(self.__id)) - - def increase_volume(self, event): - util.cli.execute( - "wpctl set-volume --limit 1.0 {} {}+".format(self.__id, self.__change) - ) - - def decrease_volume(self, event): - util.cli.execute( - "wpctl set-volume --limit 1.0 {} {}-".format(self.__id, self.__change) - ) - - def volume(self, widget): - if self.__level == "N/A": - return self.__level - return "{}%".format(int(float(self.__level) * 100)) - - def update(self): - try: - # `wpctl get-volume` will return a string like "Volume: n.nn" or "Volume: n.nn [MUTED]" - volume = util.cli.execute("wpctl get-volume {}".format(self.__id)) - v = re.search("\d\.\d+", volume) - m = re.search("MUTED", volume) - self.__level = v.group() - self.__muted = True if m else False - except Exception: - self.__level = "N/A" - - def state(self, widget): - if self.__muted: - return ["warning", "muted"] - return ["unmuted"] - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py old mode 100644 new mode 100755 index c405a0e..b145500 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -5,131 +5,57 @@ Requires the following executable: * playerctl -Parameters: - * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}'). - The format string is passed to 'playerctl -f' as an argument. Read `the README `_ 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 `_. For example, it could be '-p vlc,%any'. - * playerctl.hide: Hide the widgets when no players are found. Defaults to "false". - -Parameters are inspired by the `spotify` module, many thanks to its developers! - contributed by `smitajit `_ - many thanks! + """ import core.module import core.widget import core.input import util.cli -import util.format - -import logging class Module(core.module.Module): - def __init__(self, config, theme): - super(Module, self).__init__(config, theme, []) + def __init__(self,config , theme): + widgets = [ + core.widget.Widget(name="playerctl.prev"), + core.widget.Widget(name="playerctl.main", full_text=self.description), + core.widget.Widget(name="playerctl.next"), + ] + super(Module, self).__init__(config, theme , widgets) - self.background = True + core.input.register(widgets[0], button=core.input.LEFT_MOUSE, + cmd="playerctl previous") + core.input.register(widgets[1], button=core.input.LEFT_MOUSE, + cmd="playerctl play-pause") + core.input.register(widgets[2], button=core.input.LEFT_MOUSE, + cmd="playerctl next") - self.__hide = util.format.asbool(self.parameter("hide", "false")); - self.__hidden = self.__hide + self._status = None + self._tags = None - 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", - } - ] - else: - raise KeyError( - "The playerctl module does not have a {widget_name!r} widget".format( - widget_name=widget_name - ) - ) - - for widget, callback_options in widget_map.items(): - if isinstance(callback_options, dict): - core.input.register(widget, **callback_options) - - def hidden(self): - return self.__hidden - - def status(self): - try: - playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip() - if playback_status == "No players found": - return None - return playback_status - except Exception as e: - logging.exception(e) - return None + def description(self, widget): + return self._tags if self._tags else "..." def update(self): - playback_status = self.status() - if not playback_status: - self.__hidden = self.__hide - else: - self.__hidden = False - for widget in self.widgets(): - if playback_status: - if widget.name == "playerctl.pause": - if playback_status == "Playing": - widget.set("state", "playing") - elif playback_status == "Paused": - widget.set("state", "paused") - elif playback_status == "Stopped": - widget.set("state", "stopped") - else: - widget.set("state", "") - elif widget.name == "playerctl.next": - widget.set("state", "next") - elif widget.name == "playerctl.prev": - widget.set("state", "prev") - elif widget.name == "playerctl.song": - widget.full_text(self.__get_song()) - else: - widget.set("state", "") - widget.full_text(" ") + self._load_song() - def __get_song(self): + 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: - return str(util.cli.execute(self.__cmd + "metadata -f '" + self.__format + "'")).strip() - except Exception as e: - logging.exception(e) - return " " + status = util.cli.execute("playerctl status").lower() + info = util.cli.execute("playerctl metadata xesam:title") + except : + self._status = None + self._tags = None + return + self._status = status.split("\n")[0].lower() + self._tags = info.split("\n")[0][:20] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/pomodoro.py b/bumblebee_status/modules/contrib/pomodoro.py index 3f1397a..fe5959f 100644 --- a/bumblebee_status/modules/contrib/pomodoro.py +++ b/bumblebee_status/modules/contrib/pomodoro.py @@ -13,7 +13,7 @@ Parameters: Example: 'notify-send 'Time up!''. If you want to chain multiple commands, please use an external wrapper script and invoke that. The module itself does not support command chaining (see https://github.com/tobi-wan-kenobi/bumblebee-status/issues/532 - for a detailed explanation) + for a detailled explanation) contributed by `martindoublem `_, inspired by `karthink `_ - many thanks! """ diff --git a/bumblebee_status/modules/contrib/portage_status.py b/bumblebee_status/modules/contrib/portage_status.py deleted file mode 100644 index ab2a32e..0000000 --- a/bumblebee_status/modules/contrib/portage_status.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Displays the status of Gentoo portage operations. - -Parameters: - * portage_status.logfile: logfile for portage (default is /var/log/emerge.log) - -contributed by `andrewreisner `_ - many thanks! -""" - -import os - -import core.module -import core.widget - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.output)) - self.__logfile = self.parameter("logfile", "/var/log/emerge.log") - self.clear() - - def clear(self): - self.__action = "" - self.__package = "" - self.__status = "" - - def output(self, widget): - return " ".join( - [ - atom - for atom in (self.__action, self.__package, self.__status) - if atom != "" - ] - ) - - def state(self, widgets): - if self.__action == "": - return "idle" - return "active" - - def update(self): - try: - with open(self.__logfile, "rb") as f: - f.seek(-2, os.SEEK_END) - while f.read(1) != b"\n": - f.seek(-2, os.SEEK_CUR) - last_line = f.readline().decode() - if "===" in last_line: - if "Unmerging..." in last_line: - self.__action = "Unmerging" - package_beg = last_line.find("(") + 1 - package_end = last_line.find("-", last_line.find("/")) - 1 - self.__package = last_line[package_beg : package_end + 1] - else: # merging - status_beg = last_line.find("(") - status_end = last_line.find(")") - self.__status = last_line[status_beg : status_end + 1] - package_beg = last_line.find("(", status_end) + 1 - package_end = ( - package_beg - + last_line[package_beg:].find( - "-", last_line[package_beg:].find("/") - ) - - 1 - ) - self.__package = last_line[package_beg : package_end + 1] - action_beg = status_end + 2 - action_end = package_beg - 3 - self.__action = last_line[action_beg : action_end + 1] - else: - self.clear() - except Exception: - self.clear() diff --git a/bumblebee_status/modules/contrib/power-profile.py b/bumblebee_status/modules/contrib/power-profile.py deleted file mode 100644 index 2959391..0000000 --- a/bumblebee_status/modules/contrib/power-profile.py +++ /dev/null @@ -1,99 +0,0 @@ -# pylint: disable=C0111,R0903 -""" -Displays the current Power-Profile active - - -Left-Click or Right-Click as well as Scrolling up / down changes the active Power-Profile - -Prerequisites: - * dbus-python - * power-profiles-daemon -""" - -import dbus -import core.module -import core.widget -import core.input - - -class PowerProfileManager: - def __init__(self): - self.POWER_PROFILES_NAME = "net.hadess.PowerProfiles" - self.POWER_PROFILES_PATH = "/net/hadess/PowerProfiles" - self.PP_PROPERTIES_CURRENT_POWER_PROFILE = "ActiveProfile" - self.PP_PROPERTIES_ALL_POWER_PROFILES = "Profiles" - - self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties" - bus = dbus.SystemBus() - pp_proxy = bus.get_object(self.POWER_PROFILES_NAME, self.POWER_PROFILES_PATH) - self.pp_interface = dbus.Interface(pp_proxy, self.DBUS_PROPERTIES) - - def get_current_power_profile(self): - return self.pp_interface.Get( - self.POWER_PROFILES_NAME, self.PP_PROPERTIES_CURRENT_POWER_PROFILE - ) - - def __get_all_power_profile_names(self): - power_profiles = self.pp_interface.Get( - self.POWER_PROFILES_NAME, self.PP_PROPERTIES_ALL_POWER_PROFILES - ) - power_profiles_names = [] - for pp in power_profiles: - power_profiles_names.append(pp["Profile"]) - - return power_profiles_names - - def next_power_profile(self, event): - all_pp_names = self.__get_all_power_profile_names() - current_pp_index = self.__get_current_pp_index() - next_index = 0 - if current_pp_index != (len(all_pp_names) - 1): - next_index = current_pp_index + 1 - - self.pp_interface.Set( - self.POWER_PROFILES_NAME, - self.PP_PROPERTIES_CURRENT_POWER_PROFILE, - all_pp_names[next_index], - ) - - def prev_power_profile(self, event): - all_pp_names = self.__get_all_power_profile_names() - current_pp_index = self.__get_current_pp_index() - last_index = len(all_pp_names) - 1 - if current_pp_index is not 0: - last_index = current_pp_index - 1 - - self.pp_interface.Set( - self.POWER_PROFILES_NAME, - self.PP_PROPERTIES_CURRENT_POWER_PROFILE, - all_pp_names[last_index], - ) - - def __get_current_pp_index(self): - all_pp_names = self.__get_all_power_profile_names() - current_pp = self.get_current_power_profile() - return all_pp_names.index(current_pp) - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.full_text)) - self.pp_manager = PowerProfileManager() - core.input.register( - self, button=core.input.WHEEL_UP, cmd=self.pp_manager.next_power_profile - ) - core.input.register( - self, button=core.input.WHEEL_DOWN, cmd=self.pp_manager.prev_power_profile - ) - core.input.register( - self, button=core.input.LEFT_MOUSE, cmd=self.pp_manager.next_power_profile - ) - core.input.register( - self, button=core.input.RIGHT_MOUSE, cmd=self.pp_manager.prev_power_profile - ) - - def full_text(self, widgets): - return self.pp_manager.get_current_power_profile() - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/prime.py b/bumblebee_status/modules/contrib/prime.py index 3fbd049..8a096cb 100644 --- a/bumblebee_status/modules/contrib/prime.py +++ b/bumblebee_status/modules/contrib/prime.py @@ -20,8 +20,7 @@ Parameters: * prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel') * prime.intelstring: String to use when intel is selected (defaults to 'intel') -Requires the following executables: - * sudo +Requires the following executable: * prime-select contributed by `jeffeb3 `_ - many thanks! diff --git a/bumblebee_status/modules/contrib/progress.py b/bumblebee_status/modules/contrib/progress.py index c8905e7..a1938d2 100644 --- a/bumblebee_status/modules/contrib/progress.py +++ b/bumblebee_status/modules/contrib/progress.py @@ -29,9 +29,6 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.get_progress_text)) self.__active = False - def hidden(self): - return not self.__active - def get_progress_text(self, widget): if self.update_progress_info(widget): width = util.format.asint(self.parameter("barwidth", 8)) @@ -56,7 +53,7 @@ class Module(core.module.Module): return self.parameter("placeholder", "n/a") def update_progress_info(self, widget): - """Update widget's information about the copy""" + """Update widget's informations about the copy""" if not self.__active: return @@ -64,8 +61,8 @@ class Module(core.module.Module): # 1. pid # 2. command # 3. arguments - # 4. progress (xx.x formatted) - # 5. quantity (.. unit / .. unit formatted) + # 4. progress (xx.x formated) + # 5. quantity (.. unit / .. unit formated) # 6. speed # 7. time remaining extract_nospeed = re.compile( @@ -80,7 +77,7 @@ class Module(core.module.Module): result = extract_wtspeed.match(raw) if not result: - # Abort speed measures + # Abord speed measures raw = util.cli.execute("progress -q") result = extract_nospeed.match(raw) @@ -104,7 +101,7 @@ class Module(core.module.Module): def state(self, widget): if self.__active: - return ["copying", "no-autohide"] + return "copying" return "pending" diff --git a/bumblebee_status/modules/contrib/publicip.py b/bumblebee_status/modules/contrib/publicip.py index 8ae10a0..17a23e3 100644 --- a/bumblebee_status/modules/contrib/publicip.py +++ b/bumblebee_status/modules/contrib/publicip.py @@ -1,155 +1,28 @@ +"""Displays public IP address """ -Displays information about the public IP address associated with the default route: - * Public IP address - * Country Name - * Country Code - * City Name - * Geographic Coordinates - -Left mouse click on the widget forces immediate update. -Any change to the default route will cause the widget to update. - -Requirements: - * netifaces - -Parameters: - * publicip.format: Format string (defaults to ‘{ip} ({country_code})’) - * Available format strings - ip, country_name, country_code, city_name, coordinates - -Examples: - * bumblebee-status -m publicip -p publicip.format="{ip} ({country_code})" - * bumblebee-status -m publicip -p publicip.format="{ip} which is in {city_name}" - * bumblebee-status -m publicip -p publicip.format="Your packets are right here: {coordinates}" - -contributed by `tfwiii ` - many thanks! -""" - -import re -import threading -import netifaces -import time import core.module import core.widget -import core.input import core.decorators -import util.format import util.location -import logging - -log = logging.getLogger(__name__) class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.publicip)) + super().__init__(config, theme, core.widget.Widget(self.public_ip)) - self.__previous_default_route = None - self.__current_default_route = None - self.background = True + self.__ip = "" - # Immediate update (override default) when left click on widget - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__click_update) - - # By default show: (<2 letter country code>) - self._format = self.parameter("format", "{ip} ({country_code})") - - self.__monitor = threading.Thread(target=self.monitor, args=()) - self.__monitor.start() - - def monitor(self): - __previous_ips = set() - __current_ips = set() - # Initially set to True to force an info update on first pass - __information_changed = True - - self.update() - - while threading.main_thread().is_alive(): - __current_ips.clear() - # Look for any changes to IP addresses - try: - for interface in netifaces.interfaces(): - try: - __current_ips.add(netifaces.ifaddresses(interface)[2][0]['addr']) - except: - pass - except: - # If not ip address information found clear __current_ips - __current_ips.clear() - - # If a change of any interfaces' IP then flag change - if __current_ips.symmetric_difference(__previous_ips): - __previous_ips = __current_ips.copy() - __information_changed = True - - # Update if change is flagged - if __information_changed: - __information_changed = False - self.update() - - # Throttle the calls to netifaces - time.sleep(1) - - def publicip(self, widget): - if widget.get("public_ip") is None: - return "n/a" - return self._format.format( - ip = widget.get("public_ip", "-"), - country_name = widget.get("country_name", "-"), - country_code = widget.get("country_code", "-"), - city_name = widget.get("city_name", "-"), - coordinates = widget.get("coordinates", "-"), - ) - - def __click_update(self, event): - util.location.reset() + def public_ip(self, widget): + return self.__ip def update(self): - widget = self.widget() - try: - util.location.reset() - time.sleep(5) # wait for reset to complete before querying results - - # Fetch fresh location information - __info = util.location.location_info() - __raw_lat = __info["latitude"] - __raw_lon = __info["longitude"] - - # Contstruct coordinates string if util.location has provided required info - if isinstance(__raw_lat, float) and isinstance(__raw_lon, float): - __lat = float("{:.2f}".format(__raw_lat)) - __lon = float("{:.2f}".format(__raw_lon)) - if __lat < 0: - __coords = str(__lat) + "°S" - else: - __coords = str(__lat) + "°N" - __coords += "," - if __lon < 0: - __coords += str(__lon) + "°W" - else: - __coords += str(__lon) + "°E" - else: - __coords = "Unknown" - - # Set widget values - widget.set("public_ip", __info["public_ip"]) - widget.set("country_name", __info["country"]) - widget.set("country_code", __info["country_code"]) - widget.set("city_name", __info["city_name"]) - widget.set("coordinates", __coords) - - # Update widget values - core.event.trigger("update", [widget.module.id], redraw_only=True) - except Exception as ex: - widget.set("public_ip", None) - logging.error(str(ex)) - - def state(self, widget): - return widget.get("state", None) + self.__ip = util.location.public_ip() + except Exception: + self.__ip = "n/a" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/rofication.py b/bumblebee_status/modules/contrib/rofication.py deleted file mode 100644 index 1fd5355..0000000 --- a/bumblebee_status/modules/contrib/rofication.py +++ /dev/null @@ -1,59 +0,0 @@ -"""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 - - Parameters: - * rofication.regolith: Switch to regolith fork of rofication, see . - -""" - -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 - self.__regolith = self.parameter("regolith", False) - - - 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 - if self.__regolith: - client.sendall(bytes("num\n", "utf-8")) - else: - client.sendall(bytes("num", "utf-8")) - val = client.recv(512) - val = val.decode("utf-8") - if self.__regolith: - l = val.split(',',2) - else: - 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 diff --git a/bumblebee_status/modules/contrib/rotation.py b/bumblebee_status/modules/contrib/rotation.py index d05c463..13f656e 100644 --- a/bumblebee_status/modules/contrib/rotation.py +++ b/bumblebee_status/modules/contrib/rotation.py @@ -31,13 +31,14 @@ class Module(core.module.Module): orientation = curr_orient break - widget = self.widget(name=display) + widget = self.widget(display) if not widget: widget = self.add_widget(full_text=display, name=display) core.input.register( widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle ) widget.set("orientation", orientation) + widgets.append(widget) def state(self, widget): return widget.get("orientation", "normal") diff --git a/bumblebee_status/modules/contrib/rss.py b/bumblebee_status/modules/contrib/rss.py index 7824e2e..7b8c032 100644 --- a/bumblebee_status/modules/contrib/rss.py +++ b/bumblebee_status/modules/contrib/rss.py @@ -55,7 +55,7 @@ class Module(core.module.Module): self._state = [] - self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html") + self._newspaper_filename = tempfile.mktemp(".html") self._last_refresh = 0 self._last_update = 0 @@ -308,11 +308,10 @@ class Module(core.module.Module): while newspaper_items: content += self._create_news_section(newspaper_items) - self._newspaper_file.write( + open(self._newspaper_filename, "w").write( HTML_TEMPLATE.replace("[[CONTENT]]", content) ) - self._newspaper_file.flush() - webbrowser.open("file://" + self._newspaper_file.name) + webbrowser.open("file://" + self._newspaper_filename) self._update_history("newspaper") self._save_history() diff --git a/bumblebee_status/modules/contrib/sensors.py b/bumblebee_status/modules/contrib/sensors.py index 7d42c83..68b792a 100644 --- a/bumblebee_status/modules/contrib/sensors.py +++ b/bumblebee_status/modules/contrib/sensors.py @@ -4,7 +4,6 @@ """Displays sensor temperature Parameters: - * sensors.use_sensors: whether to use the sensors command * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output of 'sensors -j' (i.e. //.../), for example, path could @@ -19,7 +18,6 @@ contributed by `mijoharas `_ - many thanks! """ import re -import os import json import logging @@ -48,25 +46,22 @@ class Module(core.module.Module): self._json = util.format.asbool(self.parameter("json", False)) self._freq = util.format.asbool(self.parameter("show_freq", True)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors") - self.use_sensors = self.determine_method() + self.determine_method() def determine_method(self): - if util.format.asbool(self.parameter("use_sensors")) == True: - return True - if util.format.asbool(self.parameter("use_sensors")) == False: - return False if self.parameter("path") != None and self._json == False: - return False - # try to use output of sensors -u - try: - _ = util.cli.execute("sensors -u") - log.debug("Sensors command available") - return True - except FileNotFoundError as e: - log.info( - "Sensors command not available, using /sys/class/thermal/thermal_zone*/" - ) - return False + self.use_sensors = False # use thermal zone + else: + # try to use output of sensors -u + try: + output = util.cli.execute("sensors -u") + self.use_sensors = True + log.debug("Sensors command available") + except FileNotFoundError as e: + log.info( + "Sensors command not available, using /sys/class/thermal/thermal_zone*/" + ) + self.use_sensors = False def _get_temp_from_sensors(self): if self._json == True: @@ -97,31 +92,22 @@ class Module(core.module.Module): def get_temp(self): if self.use_sensors: + temperature = self._get_temp_from_sensors() log.debug("Retrieve temperature from sensors -u") - return self._get_temp_from_sensors() - try: - path = None - # use path provided by the user - if self.parameter("path") is not None: - path = self.parameter("path") - # find the thermal zone that provides cpu temperature - else: - for zone in os.listdir("/sys/class/thermal"): - if not zone.startswith("thermal_zone"): - continue - if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp": - continue - path = f"/sys/class/thermal/{zone}/temp" - # use zone 0 as fallback - if path is None: - log.info("Can not determine temperature path, using thermal_zone0") - path = "/sys/class/thermal/thermal_zone0/temp" - log.debug(f"retrieving temperature from {path}") - # the values are t°C * 1000, so divide by 1000 - return str(int(open(path).read()) / 1000) - except IOError: - log.info("Can not determine temperature, please install lm-sensors") - return "unknown" + else: + try: + temperature = open( + self.parameter("path", "/sys/class/thermal/thermal_zone0/temp") + ).read()[:2] + log.debug("retrieved temperature from /sys/class/") + # TODO: Iterate through all thermal zones to determine the correct one and use its value + # https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal + + except IOError: + temperature = "unknown" + log.info("Can not determine temperature, please install lm-sensors") + + return temperature def get_mhz(self): mhz = None diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index 4aabb5a..a451692 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -41,28 +41,26 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.get_output)) self.__command = self.parameter("command", 'echo "no command configured"') - self.__command = os.path.expanduser(self.__command) self.__async = util.format.asbool(self.parameter("async")) if self.__async: self.__output = "please wait..." self.__current_thread = threading.Thread() - if self.parameter("scrolling.makewide") is None: - self.set("scrolling.makewide", False) + # LMB and RMB will update output regardless of timer + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update) + core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update) def set_output(self, value): self.__output = value - core.event.trigger("update", [self.id], redraw_only=True) - @core.decorators.scrollable def get_output(self, _): return self.__output def update(self): # if requested then run not async version and just execute command in this thread if not self.__async: - self.__output = util.cli.execute(self.__command, shell=True, ignore_errors=True).strip() + self.__output = util.cli.execute(self.__command, ignore_errors=True).strip() return # if previous thread didn't end yet then don't do anything diff --git a/bumblebee_status/modules/contrib/shortcut.py b/bumblebee_status/modules/contrib/shortcut.py index ff5b47f..4ea0a7c 100644 --- a/bumblebee_status/modules/contrib/shortcut.py +++ b/bumblebee_status/modules/contrib/shortcut.py @@ -4,12 +4,12 @@ when clicking on it. For more than one shortcut, the commands and labels are strings separated by -a delimiter (; semicolon by default). +a demiliter (; semicolon by default). For example in order to create two shortcuts labeled A and B with commands cmdA and cmdB you could do: - ./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)' + ./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B' Parameters: * shortcut.cmds : List of commands to execute diff --git a/bumblebee_status/modules/contrib/smartstatus.py b/bumblebee_status/modules/contrib/smartstatus.py index ddd187c..2b886e3 100644 --- a/bumblebee_status/modules/contrib/smartstatus.py +++ b/bumblebee_status/modules/contrib/smartstatus.py @@ -5,12 +5,8 @@ """Displays HDD smart status of different drives or all drives -Requires the following executables: - * sudo - * smartctl - Parameters: - * smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'separate' or 'singles') + * smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') * smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all. """ @@ -38,7 +34,7 @@ class Module(core.module.Module): self.create_widgets() def create_widgets(self): - if self.display == "combined" or self.display == "combined_singles": + if self.display == "combined": widget = self.add_widget() widget.set("device", "combined") widget.set("assessment", self.combined()) @@ -81,8 +77,6 @@ class Module(core.module.Module): def combined(self): for device in self.devices: - if self.display == "combined_singles" and device not in self.drives: - continue result = self.smart(device) if result == "Fail": return "Fail" diff --git a/bumblebee_status/modules/contrib/solaar.py b/bumblebee_status/modules/contrib/solaar.py deleted file mode 100644 index b7396f3..0000000 --- a/bumblebee_status/modules/contrib/solaar.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Shows status and load percentage of logitech's unifying device - -Requires the following executable: - * solaar (from community) - -contributed by `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 diff --git a/bumblebee_status/modules/contrib/spaceapi.py b/bumblebee_status/modules/contrib/spaceapi.py index e36d7bd..d29c081 100644 --- a/bumblebee_status/modules/contrib/spaceapi.py +++ b/bumblebee_status/modules/contrib/spaceapi.py @@ -9,6 +9,7 @@ an example. Requires the following libraries: * requests + * regex Parameters: * spaceapi.url: String representation of the api endpoint @@ -16,7 +17,7 @@ Parameters: Format Strings: * Format strings are indicated by double %% - * They represent a leaf in the JSON tree, layers separated by '.' + * They represent a leaf in the JSON tree, layers seperated by '.' * Boolean values can be overwritten by appending '%true%false' in the format string * Example: to reference 'open' in '{'state':{'open': true}}' diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index d597b47..dc371db 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -1,23 +1,22 @@ -"""Displays the current song being played and allows pausing, skipping ahead, and skipping back. +# pylint: disable=C0111,R0903 + +"""Displays the current song being played Requires the following library: * python-dbus Parameters: * spotify.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber} - * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) - Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next - * spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget. - Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous. - * spotify.bus_name: String (defaults to `spotify`) - Available values: spotify, spotifyd + Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} + * spotify.previous: Change binding for previous song (default is left click) + * spotify.next: Change binding for next song (default is right click) + * spotify.pause: Change binding for toggling pause (default is middle click) + + Available options for spotify.previous, spotify.next and spotify.pause are: + LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN + contributed by `yvesh `_ - many thanks! - -added controls by `LtPeriwinkle `_ - many thanks! - -fixed icons and layout parameter by `gkeep `_ - many thanks! """ import sys @@ -27,138 +26,63 @@ import core.module import core.widget import core.input import core.decorators -import util.format -import logging class Module(core.module.Module): def __init__(self, config, theme): - super().__init__(config, theme, []) + super().__init__(config, theme, core.widget.Widget(self.spotify)) - self.background = True + buttons = { + "LEFT_CLICK": core.input.LEFT_MOUSE, + "RIGHT_CLICK": core.input.RIGHT_MOUSE, + "MIDDLE_CLICK": core.input.MIDDLE_MOUSE, + "SCROLL_UP": core.input.WHEEL_UP, + "SCROLL_DOWN": core.input.WHEEL_DOWN, + } - self.__bus_name = self.parameter("bus_name", "spotify") - - self.__layout = util.format.aslist( - self.parameter( - "layout", "spotify.song,spotify.prev,spotify.pause,spotify.next", - ) - ) - - self.__bus = dbus.SessionBus() self.__song = "" - self.__pause = "" self.__format = self.parameter("format", "{artist} - {title}") + prev_button = self.parameter("previous", "LEFT_CLICK") + next_button = self.parameter("next", "RIGHT_CLICK") + pause_button = self.parameter("pause", "MIDDLE_CLICK") - 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 \ + cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." + core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous") + core.input.register(self, button=buttons[next_button], cmd=cmd + "Next") + core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause") - widget_map = {} - for widget_name in self.__layout: - widget = self.add_widget(name=widget_name) - if widget_name == "spotify.prev": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "Previous", - } - widget.set("state", "prev") - elif widget_name == "spotify.pause": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "PlayPause", - } - elif widget_name == "spotify.next": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "Next", - } - widget.set("state", "next") - elif widget_name == "spotify.song": - if util.format.asbool(self.parameter("concise_controls", "false")): - widget_map[widget] = [ - { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "PlayPause", - }, { - "button": core.input.WHEEL_UP, - "cmd": self.__cmd + "Next", - }, { - "button": core.input.WHEEL_DOWN, - "cmd": self.__cmd + "Previous", - } - ] - else: - raise KeyError( - "The spotify module does not have a {widget_name!r} widget".format( - widget_name=widget_name - ) - ) - # is there any reason the inputs can't be directly registered above? - for widget, callback_options in widget_map.items(): - if isinstance(callback_options, dict): - core.input.register(widget, **callback_options) - - elif isinstance(callback_options, list): # used by concise_controls - for opts in callback_options: - core.input.register(widget, **opts) - + @core.decorators.scrollable + def spotify(self, widget): + return self.string_song def hidden(self): return self.string_song == "" - @core.decorators.scrollable - def __get_song(self, widget): - bus = self.__bus - if self.__bus_name == "spotifyd": - spotify = bus.get_object( - "org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2" - ) - else: + def update(self): + try: + 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")), - ) - return self.__song + spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") + props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") + playback_status = str( + spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + ) + 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")), + playbackStatus="\u25B6" + if playback_status == "Playing" + else "\u258D\u258D" + if playback_status == "Paused" + else "", + ) - def update(self): - try: - 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.__get_song(widget)) - - except Exception as e: + except Exception: self.__song = "" @property @@ -166,3 +90,6 @@ class Module(core.module.Module): if sys.version_info.major < 3: return unicode(self.__song) return str(self.__song) + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/stock.py b/bumblebee_status/modules/contrib/stock.py index 6b35bf7..7c13488 100644 --- a/bumblebee_status/modules/contrib/stock.py +++ b/bumblebee_status/modules/contrib/stock.py @@ -1,13 +1,14 @@ # -*- coding: UTF-8 -*- # pylint: disable=C0111,R0903 -"""Display a stock quote from finance.yahoo.com +"""Display a stock quote from worldtradingdata.com + +Requires the following python packages: + * requests Parameters: * stock.symbols : Comma-separated list of symbols to fetch - * stock.apikey : API key created on https://alphavantage.co - * stock.url : URL to use, defaults to "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}" - * stock.fields : Fields from the response to show, defaults to "01. symbol,05. price,10. change percent" + * stock.change : Should we fetch change in stock value (defaults to True) contributed by `msoulier `_ - many thanks! @@ -24,12 +25,6 @@ import core.decorators import util.format -def flatten(d, result): - for k, v in d.items(): - if type(v) is dict: - flatten(v, result) - else: - result[k] = v class Module(core.module.Module): @core.decorators.every(hours=1) @@ -37,41 +32,37 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.value)) self.__symbols = self.parameter("symbols", "") - self.__apikey = self.parameter("apikey", None) - self.__fields = self.parameter("fields", "01. symbol,05. price,10. change percent").split(",") - self.__url = self.parameter("url", "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}") self.__change = util.format.asbool(self.parameter("change", True)) - self.__values = [] - + self.__value = None def value(self, widget): - result = "" + results = [] + if not self.__value: + return "n/a" + data = json.loads(self.__value) - for value in self.__values: - res = {} - flatten(value, res) - for field in self.__fields: - result += res.get(field, "n/a") + " " - result = result[:-1] - return result + for symbol in data["quoteResponse"]["result"]: + valkey = "regularMarketChange" if self.__change else "regularMarketPrice" + sym = symbol.get("symbol", "n/a") + currency = symbol.get("currency", "USD") + val = "n/a" if not valkey in symbol else "{:.2f}".format(symbol[valkey]) + results.append("{} {} {}".format(sym, val, currency)) + return " ".join(results) def fetch(self): - results = [] if self.__symbols: - for symbol in self.__symbols.split(","): - url = self.__url.format(symbol=symbol, apikey=self.__apikey) - try: - results.append(json.loads(urllib.request.urlopen(url).read().strip())) - except urllib.request.URLError: - logging.error("unable to open stock exchange url") - return [] + url = "https://query1.finance.yahoo.com/v7/finance/quote?symbols=" + url += ( + self.__symbols + + "&fields=regularMarketPrice,currency,regularMarketChange" + ) + return urllib.request.urlopen(url).read().strip() else: logging.error("unable to retrieve stock exchange rate") - return [] - return results + return None def update(self): - self.__values = self.fetch() + self.__value = self.fetch() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/sun.py b/bumblebee_status/modules/contrib/sun.py index 34a4b71..dddc10d 100644 --- a/bumblebee_status/modules/contrib/sun.py +++ b/bumblebee_status/modules/contrib/sun.py @@ -5,11 +5,10 @@ Requires the following python packages: * requests * suntime - * python-dateutil Parameters: - * sun.lat : Latitude of your location - * sun.lon : Longitude of your location + * cpu.lat : Latitude of your location + * cpu.lon : Longitude of your location (if none of those are set, location is determined automatically via location APIs) @@ -39,11 +38,7 @@ class Module(core.module.Module): self.__sun = None if not lat or not lon: - try: - lat, lon = util.location.coordinates() - except Exception: - pass - + lat, lon = util.location.coordinates() if lat and lon: self.__sun = Sun(float(lat), float(lon)) @@ -59,10 +54,6 @@ class Module(core.module.Module): return "n/a" def __calculate_times(self): - if not self.__sun: - self.__sunset = self.__sunrise = None - return - self.__isup = False order_matters = True diff --git a/bumblebee_status/modules/contrib/system.py b/bumblebee_status/modules/contrib/system.py index 8b136a1..e96ee3f 100644 --- a/bumblebee_status/modules/contrib/system.py +++ b/bumblebee_status/modules/contrib/system.py @@ -8,11 +8,11 @@ adds the possibility to * reboot the system. - + Per default a confirmation dialog is shown before the actual action is performed. - + Parameters: - * system.confirm: show confirmation dialog before performing any action (default: true) + * system.confirm: show confirmation dialog before performing any action (default: true) * system.reboot: specify a reboot command (defaults to 'reboot') * system.shutdown: specify a shutdown command (defaults to 'shutdown -h now') * system.logout: specify a logout command (defaults to 'i3exit logout') @@ -21,9 +21,6 @@ Parameters: * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') -Requirements: - tkinter (python3-tk package on debian based systems either you can install it as python package) - contributed by `bbernhard `_ - many thanks! """ @@ -72,12 +69,7 @@ class Module(core.module.Module): util.cli.execute(command) def popup(self, widget): - popupcmd = self.parameter("popupcmd", ""); - if (popupcmd != ""): - util.cli.execute(popupcmd) - return - - menu = util.popup.menu(self.__config) + menu = util.popup.menu() reboot_cmd = self.parameter("reboot", "reboot") shutdown_cmd = self.parameter("shutdown", "shutdown -h now") logout_cmd = self.parameter("logout", "i3exit logout") @@ -101,7 +93,7 @@ class Module(core.module.Module): menu.add_menuitem( "log out", callback=functools.partial( - self.__on_command, "Log out", "Log out?", logout_cmd + self.__on_command, "Log out", "Log out?", "i3exit logout" ), ) # don't ask for these diff --git a/bumblebee_status/modules/contrib/taskwarrior.py b/bumblebee_status/modules/contrib/taskwarrior.py index 81040e4..0d540b5 100644 --- a/bumblebee_status/modules/contrib/taskwarrior.py +++ b/bumblebee_status/modules/contrib/taskwarrior.py @@ -5,7 +5,6 @@ Requires the following library: Parameters: * taskwarrior.taskrc : path to the taskrc file (defaults to ~/.taskrc) - * taskwarrior.show_active: true/false(default) to show the active task ID and description when one is active, otherwise show the total number pending. contributed by `chdorb `_ - many thanks! @@ -23,45 +22,20 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.output)) self.__pending_tasks = "0" - self.__status = "stopped" def update(self): - """Return a string with the number of pending tasks from TaskWarrior - or the descripton of an active task. - - if show.active is set in the config, show the description of the - current active task, otherwise the number of pending tasks will be displayed. - """ + """Return a string with the number of pending tasks from TaskWarrior.""" try: taskrc = self.parameter("taskrc", "~/.taskrc") - show_active = self.parameter("show_active", False) w = TaskWarrior(config_filename=taskrc) - active_tasks = ( - w.filter_tasks({"start.any": "", "status": "pending"}) or None - ) - if show_active and active_tasks: - # this is using the first element of the list, if there happen - # to be other active tasks, they won't be displayed. - reporting_tasks = ( - f"{active_tasks[0]['id']} - {active_tasks[0]['description']}" - ) - self.__status = "active" - else: - reporting_tasks = len(w.filter_tasks({"status": "pending"})) - self.__status = "stopped" - self.__pending_tasks = reporting_tasks + pending_tasks = w.filter_tasks({"status": "pending"}) + self.__pending_tasks = str(len(pending_tasks)) except: self.__pending_tasks = "n/a" - self.__status = "stopped" - @core.decorators.scrollable def output(self, _): """Format the task counter to output in bumblebee.""" return "{}".format(self.__pending_tasks) - def state(self, widget): - """Return the set status to reflect state""" - return self.__status - # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/thunderbird.py b/bumblebee_status/modules/contrib/thunderbird.py deleted file mode 100644 index 5240e6b..0000000 --- a/bumblebee_status/modules/contrib/thunderbird.py +++ /dev/null @@ -1,89 +0,0 @@ -# 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 `_ - 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 diff --git a/bumblebee_status/modules/contrib/title.py b/bumblebee_status/modules/contrib/title.py index 1c98b42..33af47a 100644 --- a/bumblebee_status/modules/contrib/title.py +++ b/bumblebee_status/modules/contrib/title.py @@ -9,7 +9,6 @@ Parameters: * title.max : Maximum character length for title before truncating. Defaults to 64. * title.placeholder : Placeholder text to be placed if title was truncated. Defaults to '...'. * title.scroll : Boolean flag for scrolling title. Defaults to False - * title.short : Boolean flag for short title. Defaults to False contributed by `UltimatePancake `_ - many thanks! @@ -36,7 +35,6 @@ class Module(core.module.Module): # parsing of parameters self.__scroll = util.format.asbool(self.parameter("scroll", False)) - self.__short = util.format.asbool(self.parameter("short", False)) self.__max = int(self.parameter("max", 64)) self.__placeholder = self.parameter("placeholder", "...") self.__title = "" @@ -50,9 +48,8 @@ class Module(core.module.Module): # create a connection with i3ipc self.__i3 = i3ipc.Connection() - # event is called both on focus change and title change, and on workspace change + # event is called both on focus change and title change self.__i3.on("window", lambda __p_i3, __p_e: self.__pollTitle()) - self.__i3.on("workspace", lambda __p_i3, __p_e: self.__pollTitle()) # begin listening for events threading.Thread(target=self.__i3.main).start() @@ -69,9 +66,7 @@ class Module(core.module.Module): def __pollTitle(self): """Updating current title.""" try: - focused = self.__i3.get_tree().find_focused().name - self.__full_title = focused.split( - "-")[-1].strip() if self.__short else focused + self.__full_title = self.__i3.get_tree().find_focused().name except: self.__full_title = no_title if self.__full_title is None: diff --git a/bumblebee_status/modules/contrib/todo.py b/bumblebee_status/modules/contrib/todo.py index 76e289e..878b63f 100644 --- a/bumblebee_status/modules/contrib/todo.py +++ b/bumblebee_status/modules/contrib/todo.py @@ -21,10 +21,9 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.output)) self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt")) - self.__editor = self.parameter("editor", "xdg-open") self.__todos = self.count_items() core.input.register( - self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc) + self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc) ) def output(self, widget): @@ -40,12 +39,11 @@ class Module(core.module.Module): def count_items(self): try: - i = 0 + i = -1 with open(self.__doc) as f: - for l in f.readlines(): - if l.strip() != '': - i += 1 - return i + for i, l in enumerate(f): + pass + return i + 1 except Exception: return 0 diff --git a/bumblebee_status/modules/contrib/todo_org.py b/bumblebee_status/modules/contrib/todo_org.py deleted file mode 100644 index 05fa90b..0000000 --- a/bumblebee_status/modules/contrib/todo_org.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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 ` -""" - -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 diff --git a/bumblebee_status/modules/contrib/todoist.py b/bumblebee_status/modules/contrib/todoist.py deleted file mode 100644 index 5fae228..0000000 --- a/bumblebee_status/modules/contrib/todoist.py +++ /dev/null @@ -1,76 +0,0 @@ -# pylint: disable=C0111,R0903 - -""" -Displays the nº of Todoist tasks that are due: - - * https://developer.todoist.com/rest/v2/#get-active-tasks - -Uses `xdg-open` or `x-www-browser` to open web-pages. - -Requires the following library: - * requests - -Errors: - if the Todoist get active tasks query failed, the shown value is `n/a` - -Parameters: - * todoist.token: Todoist api token, you can get it in https://todoist.com/app/settings/integrations/developer. - * todoist.filter: a filter statement defined by Todoist (https://todoist.com/help/articles/introduction-to-filters), eg: "!assigned to: others & (Overdue | due: today)" -""" - -import shutil - -import requests - -import core.decorators -import core.input -import core.module -import core.widget - -HOST_API = "https://api.todoist.com" -HOST_WEBSITE = "https://todoist.com/app/today" - -TASKS_URL = f"{HOST_API}/rest/v2/tasks" - - -class Module(core.module.Module): - @core.decorators.every(minutes=5) - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.todoist)) - - self.__user_id = None - self.background = True - self.__label = "" - - token = self.parameter("token", "") - self.__filter = self.parameter("filter", "") - - self.__requests = requests.Session() - self.__requests.headers.update({"Authorization": f"Bearer {token}"}) - - cmd = "xdg-open" - if not shutil.which(cmd): - cmd = "x-www-browser" - - core.input.register( - self, - button=core.input.LEFT_MOUSE, - cmd=f"{cmd} {HOST_WEBSITE}", - ) - - def todoist(self, _): - return self.__label - - def update(self): - try: - self.__label = self.__get_pending_tasks() - except Exception: - self.__label = "n/a" - - def __get_pending_tasks(self) -> str: - params = {"filter": self.__filter} if self.__filter else None - - response = self.__requests.get(TASKS_URL, params=params) - data = response.json() - - return str(len(data)) diff --git a/bumblebee_status/modules/contrib/traffic.py b/bumblebee_status/modules/contrib/traffic.py index 4c91b61..c8b87db 100644 --- a/bumblebee_status/modules/contrib/traffic.py +++ b/bumblebee_status/modules/contrib/traffic.py @@ -8,7 +8,7 @@ Parameters: * traffic.showname: If set to False, hide network interface name (defaults to True) * traffic.format: Format string for download/upload speeds. Defaults to '{:.2f}' - * traffic.graphlen: Graph length in seconds. Positive even integer. Each + * traffic.graphlen: Graph lenth in seconds. Positive even integer. Each char shows 2 seconds. If set, enables up/down traffic graphs diff --git a/bumblebee_status/modules/contrib/twmn.py b/bumblebee_status/modules/contrib/twmn.py index 0fc46b3..58b2ba1 100644 --- a/bumblebee_status/modules/contrib/twmn.py +++ b/bumblebee_status/modules/contrib/twmn.py @@ -2,9 +2,6 @@ """Toggle twmn notifications. -Requires the following executable: - * systemctl - contributed by `Pseudonick47 `_ - many thanks! """ diff --git a/bumblebee_status/modules/contrib/usage.py b/bumblebee_status/modules/contrib/usage.py deleted file mode 100644 index d64e3e1..0000000 --- a/bumblebee_status/modules/contrib/usage.py +++ /dev/null @@ -1,78 +0,0 @@ -# pylint: disable=C0111,R0903 - -""" -Module for ActivityWatch (https://activitywatch.net/) -Displays the amount of time the system was used actively. - -Requirements: - * sqlite3 module for python - * ActivityWatch - -Errors: - * when you get 'error: unable to open database file', modify the parameter 'database' to your ActivityWatch database file - -> often found by running 'locate aw-server/peewee-sqlite.v2.db' - -Parameters: - * usage.database: path to your database file - * usage.format: Specify what gets printed to the bar - -> use 'HH', 'MM' or 'SS', they will get replaced by the number of hours, minutes and seconds, respectively - -contributed by lasnikr (https://github.com/lasnikr) -""" - -import sqlite3 -import os - -import core.module -import core.widget - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.output)) - self.__usage = "" - - def output(self, _): - return "{}".format(self.__usage) - - def update(self): - database_loc = self.parameter( - "database", "~/.local/share/activitywatch/aw-server/peewee-sqlite.v2.db" - ) - home = os.path.expanduser("~") - - database = sqlite3.connect(database_loc.replace("~", home)) - cursor = database.cursor() - - cursor.execute("SELECT key, id FROM bucketmodel") - - bucket_id = 1 - - for tuple in cursor.fetchall(): - if "aw-watcher-afk" in tuple[1]: - bucket_id = tuple[0] - - cursor.execute( - f"SELECT duration, datastr FROM eventmodel WHERE bucket_id = {bucket_id} " - + 'AND strftime("%Y,%m,%d", timestamp) = strftime("%Y,%m,%d", "now")' - ) - - duration = 0 - - for tuple in cursor.fetchall(): - if '{"status": "not-afk"}' in tuple[1]: - duration += tuple[0] - - hours = "%.0f" % (duration // 3600) - minutes = "%.0f" % ((duration % 3600) // 60) - seconds = "%.0f" % (duration % 60) - - formatting = self.parameter("format", "HHh, MMmin") - self.__usage = ( - formatting.replace("HH", hours) - .replace("MM", minutes) - .replace("SS", seconds) - ) - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/vpn.py b/bumblebee_status/modules/contrib/vpn.py index 3381e4f..d9c8793 100644 --- a/bumblebee_status/modules/contrib/vpn.py +++ b/bumblebee_status/modules/contrib/vpn.py @@ -1,5 +1,4 @@ # pylint: disable=C0111,R0903 -# -*- coding: utf-8 -*- """ Displays the VPN profile that is currently in use. @@ -69,7 +68,7 @@ class Module(core.module.Module): def vpn_status(self, widget): if self.__connected_vpn_profile is None: - return "" + return "off" return self.__connected_vpn_profile def __on_vpndisconnect(self): @@ -94,7 +93,7 @@ class Module(core.module.Module): self.__connected_vpn_profile = None def popup(self, widget): - menu = util.popup.menu(self.__config) + menu = util.popup.menu() if self.__connected_vpn_profile is not None: menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect) diff --git a/bumblebee_status/modules/contrib/wakatime.py b/bumblebee_status/modules/contrib/wakatime.py deleted file mode 100644 index 1ac4461..0000000 --- a/bumblebee_status/modules/contrib/wakatime.py +++ /dev/null @@ -1,94 +0,0 @@ -# pylint: disable=C0111,R0903 - -""" -Displays the WakaTime daily/weekly/monthly times: - - * https://wakatime.com/developers#stats - -Uses `xdg-open` or `x-www-browser` to open web-pages. - -Requires the following library: - * requests - -Errors: - if the Wakatime status query failed, the shown value is `n/a` - -Parameters: - * wakatime.token: Wakatime secret api key, you can get it in https://wakatime.com/settings/account. - * wakatime.range: Range of the output, default is "Today". Can be one of “Today”, “Yesterday”, “Last 7 Days”, “Last 7 Days from Yesterday”, “Last 14 Days”, “Last 30 Days”, “This Week”, “Last Week”, “This Month”, or “Last Month”. - * wakatime.format: Format of the output, default is "digital" - Valid inputs are: - * "decimal" -> 1.37 - * "digital" -> 1:22 - * "seconds" -> 4931.29 - * "text" -> 1 hr 22 mins - * "%H:%M:%S" -> 01:22:31 (or any other valid format) -""" - -import base64 -import shutil -import time - -import requests - -import core.decorators -import core.input -import core.module -import core.widget - -HOST_API = "https://wakatime.com" -SUMMARIES_URL = f"{HOST_API}/api/v1/users/current/summaries" -UTF8 = "utf-8" -FORMAT_PARAMETERS = ["decimal", "digital", "seconds", "text"] - - -class Module(core.module.Module): - @core.decorators.every(minutes=5) - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.wakatime)) - - self.background = True - self.__label = "" - - self.__output_format = self.parameter("format", "digital") - self.__range = self.parameter("range", "Today") - - self.__requests = requests.Session() - - token = self.__encode_to_base_64(self.parameter("token", "")) - self.__requests.headers.update({"Authorization": f"Basic {token}"}) - - cmd = "xdg-open" - if not shutil.which(cmd): - cmd = "x-www-browser" - - core.input.register( - self, - button=core.input.LEFT_MOUSE, - cmd=f"{cmd} {HOST_API}/dashboard", - ) - - def wakatime(self, _): - return self.__label - - def update(self): - try: - self.__label = self.__get_waka_time(self.__range) - except Exception: - self.__label = "n/a" - - def __get_waka_time(self, since_date: str) -> str: - response = self.__requests.get(f"{SUMMARIES_URL}?range={since_date}") - - data = response.json() - grand_total = data["cumulative_total"] - - if self.__output_format in FORMAT_PARAMETERS: - return str(grand_total[self.__output_format]) - else: - total_seconds = int(grand_total["seconds"]) - return time.strftime(self.__output_format, time.gmtime(total_seconds)) - - @staticmethod - def __encode_to_base_64(s: str) -> str: - return base64.b64encode(s.encode(UTF8)).decode(UTF8) diff --git a/bumblebee_status/modules/contrib/watson.py b/bumblebee_status/modules/contrib/watson.py index a717342..d7b260b 100644 --- a/bumblebee_status/modules/contrib/watson.py +++ b/bumblebee_status/modules/contrib/watson.py @@ -5,10 +5,6 @@ Requires the following executable: * watson -Parameters: - * watson.format: Output format, defaults to "{project} [{tags}]" - Supported fields are: {project}, {tags}, {relative_start}, {absolute_start} - contributed by `bendardenne `_ - many thanks! """ @@ -30,11 +26,11 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.text)) self.__tracking = False - self.__info = {} - self.__format = self.parameter("format", "{project} [{tags}]") + self.__project = "" core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle) def toggle(self, widget): + self.__project = "hit" if self.__tracking: util.cli.execute("watson stop") else: @@ -43,27 +39,20 @@ class Module(core.module.Module): def text(self, widget): if self.__tracking: - return self.__format.format(**self.__info) + return self.__project else: return "Paused" def update(self): output = util.cli.execute("watson status") - - m = re.search(r"Project ([^\[\]]+)(?: \[(.+)\])? started (.+) \((.+)\)", output) - - if m: - self.__tracking = True - self.__info = { - "project": m.group(1), - "tags": m.group(2) or "", - "relative_start": m.group(3), - "absolute_start": m.group(4), - } - else: + if re.match(r"No project started", output): self.__tracking = False return + self.__tracking = True + m = re.search(r"Project (.+) started", output) + self.__project = m.group(1) + def state(self, widget): return "on" if self.__tracking else "off" diff --git a/bumblebee_status/modules/contrib/weather.py b/bumblebee_status/modules/contrib/weather.py index 4166995..72e0c27 100644 --- a/bumblebee_status/modules/contrib/weather.py +++ b/bumblebee_status/modules/contrib/weather.py @@ -13,7 +13,7 @@ Parameters: * weather.unit: metric (default), kelvin, imperial * weather.showcity: If set to true, show location information, otherwise hide it (defaults to true) * weather.showminmax: If set to true, show the minimum and maximum temperature, otherwise hide it (defaults to false) - * weather.apikey: API key from https://api.openweathermap.org + * weather.apikey: API key from http://api.openweathermap.org contributed by `TheEdgeOfRage `_ - many thanks! @@ -116,7 +116,7 @@ class Module(core.module.Module): def update(self): try: - weather_url = "https://api.openweathermap.org/data/2.5/weather?appid={}".format( + weather_url = "http://api.openweathermap.org/data/2.5/weather?appid={}".format( self.__apikey ) weather_url = "{}&units={}".format(weather_url, self.__unit) diff --git a/bumblebee_status/modules/contrib/wlrotation.py b/bumblebee_status/modules/contrib/wlrotation.py deleted file mode 100644 index 242c50b..0000000 --- a/bumblebee_status/modules/contrib/wlrotation.py +++ /dev/null @@ -1,126 +0,0 @@ -# pylint: disable=C0111,R0903 -# -*- coding: utf-8 -*- - -"""Shows a widget for each connected screen and allows the user to loop through different orientations. - -Parameters: - * wlrotation.display : Name of the output display that will be rotated - + wlrotation.auto : Boolean value if the display should be rotatet automatic by default - -Requires the following executable: - * swaymsg -""" - -import core.module -import core.input -import util.cli - -import iio -import json -from math import degrees, atan2, sqrt -from os import environ, path - -possible_orientations = ["normal", "90", "180", "270"] - -class iioValue: - def __init__(self, channel): - self.channel = channel - self.scale = self.read('scale') - self.offset = self.read('offset') - - def read(self, attr): - return float(self.channel.attrs[attr].value) - - def value(self): - return (self.read('raw') + self.offset) * self.scale - -class iioAccelDevice: - def __init__(self): - self.ctx = iio.Context() # store ctx pointer - d = self.ctx.find_device('accel_3d') - self.x = iioValue(d.find_channel('accel_x')) - self.y = iioValue(d.find_channel('accel_y')) - self.z = iioValue(d.find_channel('accel_z')) - - def orientation(self): - """ - returns tuple of `[success, value]` where `success` indicates, if an accurate value could be meassured and `value` the sway output api compatible value or `normal` if success is `False` - """ - x_deg, y_deg, z_deg = self._deg() - if abs(z_deg) < 70: # checks if device is angled too shallow - if x_deg >= 70: return True, "270" - if x_deg <= -70: return True, "90" - if abs(x_deg) <= 20: - if y_deg < 0: return True, "normal" - if y_deg > 0: return True, "180" - return False, "normal" - - def _deg(self): - gravity = 9.81 - x, y, z = self.x.value() / gravity, self.y.value() / gravity, self.z.value() / gravity - return degrees(atan2(x, sqrt(pow(y, 2) + pow(z, 2)))), degrees(atan2(y, sqrt(pow(z, 2) + pow(x, 2)))), degrees(atan2(z, sqrt(pow(x, 2) + pow(y, 2)))) - -class Display(): - def __init__(self, name, widget, display_data, auto=False): - self.name = name - self.widget = widget - self.accelDevice = iioAccelDevice() - self._lock_auto_rotation(not auto) - - self.widget.set("orientation", display_data['transform']) - - core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=self.rotate_90deg) - core.input.register(widget, button=core.input.RIGHT_MOUSE, cmd=self.toggle) - - def rotate_90deg(self, event): - # compute new orientation based on current orientation - current = self.widget.get("orientation") - self._set_rotation(possible_orientations[(possible_orientations.index(current) + 1) % len(possible_orientations)]) - # disable auto rotation - self._lock_auto_rotation(True) - - def toggle(self, event): - self._lock_auto_rotation(not self.locked) - - def auto_rotate(self): - # automagically rotate the display based on sensor values - # this is only called if rotation lock is disabled - success, value = self.accelDevice.orientation() - if success: - self._set_rotation(value) - - def _set_rotation(self, new_orientation): - self.widget.set("orientation", new_orientation) - util.cli.execute("swaymsg 'output {} transform {}'".format(self.name, new_orientation)) - - def _lock_auto_rotation(self, locked): - self.locked = locked - self.widget.set("locked", self.locked) - -class Module(core.module.Module): - @core.decorators.every(seconds=1) - def __init__(self, config, theme): - super().__init__(config, theme, []) - - self.display = None - display_filter = self.parameter("display", None) - for display in json.loads(util.cli.execute("swaymsg -t get_outputs -r")): - name = display['name'] - if display_filter == None or display_filter == name: - self.display = Display(name, self.add_widget(name=name), display, auto=util.format.asbool(self.parameter("auto", False))) - break # I assume that it makes only sense to rotate a single screen - - def update(self): - if self.display == None: - return - if self.display.locked: - return - - self.display.auto_rotate() - - def state(self, widget): - state = [] - state.append("locked" if widget.get("locked", True) else "auto") - return state - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/zpool.py b/bumblebee_status/modules/contrib/zpool.py index 9863793..78cc26a 100644 --- a/bumblebee_status/modules/contrib/zpool.py +++ b/bumblebee_status/modules/contrib/zpool.py @@ -1,8 +1,5 @@ """Displays info about zpools present on the system -Requires the following executable: - * sudo (if `zpool.sudo` is explicitly set to `true`) - Parameters: * zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools is displayed. (Default: '') diff --git a/bumblebee_status/modules/core/cpu.py b/bumblebee_status/modules/core/cpu.py index 77ac20a..1e8c9db 100644 --- a/bumblebee_status/modules/core/cpu.py +++ b/bumblebee_status/modules/core/cpu.py @@ -2,17 +2,13 @@ """Displays CPU utilization across all CPUs. -By default, opens `gnome-system-monitor` on left mouse click. - Requirements: * the psutil Python module for the first three items from the list above - * gnome-system-monitor for default mouse click action Parameters: * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) * cpu.critical: Critical threshold in % of CPU usage (defaults to 80%) * cpu.format : Format string (defaults to '{:.01f}%') - * cpu.percpu : If set to true, show each individual cpu (defaults to false) """ import psutil @@ -21,19 +17,12 @@ import core.module import core.widget import core.input -import util.format - class Module(core.module.Module): def __init__(self, config, theme): - super().__init__(config, theme, []) - self._percpu = util.format.asbool(self.parameter("percpu", False)) - - for idx, cpu_perc in enumerate(self.cpu_utilization()): - widget = self.add_widget(name="cpu#{}".format(idx), full_text=self.utilization) - widget.set("utilization", cpu_perc) - widget.set("theme.minwidth", self._format.format(100.0 - 10e-20)) - + super().__init__(config, theme, core.widget.Widget(self.utilization)) + self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20)) + self._utilization = psutil.cpu_percent(percpu=False) core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) @@ -42,19 +31,14 @@ class Module(core.module.Module): def _format(self): return self.parameter("format", "{:.01f}%") - def utilization(self, widget): - return self._format.format(widget.get("utilization", 0.0)) - - def cpu_utilization(self): - tmp = psutil.cpu_percent(percpu=self._percpu) - return tmp if self._percpu else [tmp] + def utilization(self, _): + return self._format.format(self._utilization) def update(self): - for idx, cpu_perc in enumerate(self.cpu_utilization()): - self.widgets()[idx].set("utilization", cpu_perc) + self._utilization = psutil.cpu_percent(percpu=False) - def state(self, widget): - return self.threshold_state(widget.get("utilization", 0.0), 70, 80) + def state(self, _): + return self.threshold_state(self._utilization, 70, 80) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/datetime.py b/bumblebee_status/modules/core/datetime.py index 0a15c70..f421e15 100644 --- a/bumblebee_status/modules/core/datetime.py +++ b/bumblebee_status/modules/core/datetime.py @@ -17,33 +17,26 @@ import core.input class Module(core.module.Module): - def __init__(self, config, theme, dtlibrary=None): + def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.full_text)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar") - self.dtlibrary = dtlibrary or datetime - - def set_locale(self): - l = self.default_locale() + self._fmt = self.parameter("format", self.default_format()) + l = locale.getdefaultlocale() if not l or l == (None, None): l = ("en_US", "UTF-8") lcl = self.parameter("locale", ".".join(l)) try: - locale.setlocale(locale.LC_ALL, lcl.split(".")) + locale.setlocale(locale.LC_TIME, lcl.split(".")) except Exception as e: - locale.setlocale(locale.LC_ALL, ("en_US", "UTF-8")) + locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8")) def default_format(self): return "%x %X" - def default_locale(self): - return locale.getdefaultlocale() - def full_text(self, widget): - self.set_locale() enc = locale.getpreferredencoding() - fmt = self.parameter("format", self.default_format()) - retval = self.dtlibrary.datetime.now().strftime(fmt) + retval = datetime.datetime.now().strftime(self._fmt) if hasattr(retval, "decode"): return retval.decode(enc) return retval diff --git a/bumblebee_status/modules/core/disk.py b/bumblebee_status/modules/core/disk.py index 0488535..12c7a16 100644 --- a/bumblebee_status/modules/core/disk.py +++ b/bumblebee_status/modules/core/disk.py @@ -4,11 +4,10 @@ Parameters: * disk.warning: Warning threshold in % of disk space (defaults to 80%) - * disk.critical: Critical threshold in % of disk space (defaults to 90%) + * disk.critical: Critical threshold in % of disk space (defaults ot 90%) * disk.path: Path to calculate disk usage from (defaults to /) * disk.open: Which application / file manager to launch (default xdg-open) * disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)') - * disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC') """ import os @@ -26,7 +25,6 @@ class Module(core.module.Module): self._path = self.parameter("path", "/") self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)") - self._system = self.parameter("system", "IEC") self._used = 0 self._left = 0 @@ -40,9 +38,9 @@ class Module(core.module.Module): ) def diskspace(self, widget): - used_str = util.format.byte(self._used, sys=self._system) - size_str = util.format.byte(self._size, sys=self._system) - left_str = util.format.byte(self._left, sys=self._system) + used_str = util.format.byte(self._used) + size_str = util.format.byte(self._size) + left_str = util.format.byte(self._left) percent_str = self._percent return self._format.format( diff --git a/bumblebee_status/modules/core/keys.py b/bumblebee_status/modules/core/keys.py deleted file mode 100644 index a579c7d..0000000 --- a/bumblebee_status/modules/core/keys.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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 diff --git a/bumblebee_status/modules/core/layout-xkb.py b/bumblebee_status/modules/core/layout-xkb.py index 909017d..4d1dc7a 100644 --- a/bumblebee_status/modules/core/layout-xkb.py +++ b/bumblebee_status/modules/core/layout-xkb.py @@ -53,7 +53,7 @@ class Module(core.module.Module): log.debug("group num: {}".format(xkb.group_num)) name = ( xkb.group_name - if util.format.asbool(self.parameter("showname", False)) + if util.format.asbool(self.parameter("showname"), False) else xkb.group_symbol ) if self.__show_variant: diff --git a/bumblebee_status/modules/core/layout.py b/bumblebee_status/modules/core/layout.py deleted file mode 120000 index f2e8037..0000000 --- a/bumblebee_status/modules/core/layout.py +++ /dev/null @@ -1 +0,0 @@ -layout-xkb.py \ No newline at end of file diff --git a/bumblebee_status/modules/core/layout_xkb.py b/bumblebee_status/modules/core/layout_xkb.py deleted file mode 120000 index f2e8037..0000000 --- a/bumblebee_status/modules/core/layout_xkb.py +++ /dev/null @@ -1 +0,0 @@ -layout-xkb.py \ No newline at end of file diff --git a/bumblebee_status/modules/core/load.py b/bumblebee_status/modules/core/load.py index ca7d26a..7ca839b 100644 --- a/bumblebee_status/modules/core/load.py +++ b/bumblebee_status/modules/core/load.py @@ -2,11 +2,6 @@ """Displays system load. -By default, opens `gnome-system-monitor` on left mouse click. - -Requirements: - * gnome-system-monitor for default mouse click action - Parameters: * load.warning : Warning threshold for the one-minute load average (defaults to 70% of the number of CPUs) * load.critical: Critical threshold for the one-minute load average (defaults to 80% of the number of CPUs) @@ -27,7 +22,6 @@ class Module(core.module.Module): self._cpus = multiprocessing.cpu_count() except NotImplementedError as e: self._cpus = 1 - core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) diff --git a/bumblebee_status/modules/core/memory.py b/bumblebee_status/modules/core/memory.py index c9ecf44..7ce7545 100644 --- a/bumblebee_status/modules/core/memory.py +++ b/bumblebee_status/modules/core/memory.py @@ -2,11 +2,6 @@ """Displays available RAM, total amount of RAM and percentage available. -By default, opens `gnome-system-monitor` on left mouse click. - -Requirements: - * gnome-system-monitor for default mouse click action - Parameters: * memory.warning : Warning threshold in % of memory used (defaults to 80%) * memory.critical: Critical threshold in % of memory used (defaults to 90%) @@ -41,8 +36,18 @@ class Module(core.module.Module): return self._format.format(**self._mem) def update(self): - data = self.__parse_meminfo() - + data = {} + with open("/proc/meminfo", "r") as f: + for line in f: + tmp = re.split(r"[:\s]+", line) + value = int(tmp[1]) + if tmp[2] == "kB": + value = value * 1024 + if tmp[2] == "mB": + value = value * 1024 * 1024 + if tmp[2] == "gB": + value = value * 1024 * 1024 * 1024 + data[tmp[0]] = value if "MemAvailable" in data: used = data["MemTotal"] - data["MemAvailable"] else: @@ -68,28 +73,5 @@ class Module(core.module.Module): return "warning" return None - def __parse_meminfo(self): - data = {} - with open("/proc/meminfo", "r") as f: - # https://bugs.python.org/issue32933 - for line in f.readlines(): - tmp = re.split(r"[:\s]+", line) - value = self.__parse_value(tmp) - - data[tmp[0]] = value - - return data - - def __parse_value(self, data): - value = int(data[1]) - - if data[2] == "kB": - value = value * 1024 - if data[2] == "mB": - value = value * 1024 * 1024 - if data[2] == "gB": - value = value * 1024 * 1024 * 1024 - - return value # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index f153a94..a5f26b0 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -5,40 +5,35 @@ Requires the following python module: * netifaces -Requires the following executable: - * iw - * (until and including 2.0.5: iwgetid) - Parameters: - * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi') + * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') * nic.include: Comma-separated list of interfaces to include * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) - * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {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) + * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') """ -import re import shutil import netifaces import subprocess import core.module import core.decorators -import core.input import util.cli import util.format class Module(core.module.Module): - @core.decorators.every(seconds=5) + @core.decorators.every(seconds=10) def __init__(self, config, theme): widgets = [] super().__init__(config, theme, widgets) - self._exclude = util.format.aslist( - self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br,.*:avahi") + self._exclude = tuple( + filter( + 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": []} for state in tuple( @@ -48,19 +43,9 @@ class Module(core.module.Module): self._states["exclude"].append(state[1:]) else: self._states["include"].append(state) - self._format = self.parameter("format", "{intf} {state} {ip} {ssid} {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._format = self.parameter("format", "{intf} {state} {ip} {ssid}") + self.iwgetid = shutil.which("iwgetid") self._update_widgets(widgets) - core.input.register(self, button=core.input.LEFT_MOUSE, cmd='wifi-menu') - core.input.register(self, button=core.input.RIGHT_MOUSE, cmd='nm-connection-editor') def update(self): self._update_widgets(self.widgets()) @@ -77,21 +62,15 @@ class Module(core.module.Module): iftype = "wireless" if self._iswlan(intf) else "wired" iftype = "tunnel" if self._istunnel(intf) else iftype - # "strength" is none if interface type is not wlan - strength = widget.get("strength") - if self._iswlan(intf) and strength: - if strength < self._strength_threshold_critical: - states.append("critical") - elif strength < self._strength_threshold_warning: - states.append("warning") - states.append("{}-{}".format(iftype, widget.get("state"))) return states def _iswlan(self, intf): # wifi, wlan, wlp, seems to work for me - return intf.startswith("w") and not intf.startswith("wwan") + if intf.startswith("w"): + return True + return False def _istunnel(self, intf): return intf.startswith("tun") or intf.startswith("wg") @@ -106,18 +85,11 @@ class Module(core.module.Module): return [] return retval - def _excluded(self, intf): - for e in self._exclude: - if re.match(e, intf): - return True - return False - def _update_widgets(self, widgets): self.clear_widgets() - interfaces = [] - for i in netifaces.interfaces(): - if not self._excluded(i): - interfaces.append(i) + interfaces = [ + i for i in netifaces.interfaces() if not i.startswith(self._exclude) + ] interfaces.extend([i for i in netifaces.interfaces() if i in self._include]) for intf in interfaces: @@ -135,9 +107,6 @@ class Module(core.module.Module): ): continue - strength_dbm = self.get_strength_dbm(intf) - strength_percent = self.convert_strength_dbm_percent(strength_dbm) - widget = self.widget(intf) if not widget: widget = self.add_widget(name=intf) @@ -148,44 +117,19 @@ class Module(core.module.Module): ip=", ".join(addr), intf=intf, state=state, - strength=str(strength_percent) + "%" if strength_percent else "", ssid=self.get_ssid(intf), ).split() ) ) widget.set("intf", intf) widget.set("state", state) - widget.set("strength", strength_percent) def get_ssid(self, intf): - if not self._iswlan(intf) or self._istunnel(intf) or not self.iw: - return "" - - iw_info = util.cli.execute("{} dev {} info".format(self.iw, intf)) - for line in iw_info.split("\n"): - match = re.match(r"^\s+ssid\s(.+)$", line) - if match: - return match.group(1) - + if self._iswlan(intf) and self.iwgetid: + return util.cli.execute( + "{} -r {}".format(self.iwgetid, intf), ignore_errors=True + ) 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 diff --git a/bumblebee_status/modules/core/ping.py b/bumblebee_status/modules/core/ping.py index 7d650cd..d43c027 100644 --- a/bumblebee_status/modules/core/ping.py +++ b/bumblebee_status/modules/core/ping.py @@ -24,6 +24,8 @@ import core.decorators import util.cli + + class Module(core.module.Module): @core.decorators.every(seconds=60) def __init__(self, config, theme): diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 5f74248..120470f 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -2,8 +2,6 @@ """Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. -!!! This module will eventually be deprecated (since it has bad performance and high CPU load) and be replaced with "pulsectl", which is a much better drop-in replacement !!! - Aliases: pasink (use this to control output instead of input), pasource Parameters: @@ -13,20 +11,6 @@ Parameters: Note: If the left and right channels have different volumes, the limit might not be reached exactly. * pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango; 0 for not showing volume bars (default) - * pulseaudio.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. - Per default, the sink/source name returned by "pactl list sinks short" is used as display name. - - As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"), - its possible to map the name to more a user friendly name. - - e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following - bumblebee-status config entry: pulseaudio.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset - - Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the - "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: - pulseaudio.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 - * Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current - default device add the following config entry to your bumblebee-status config: pulseaudio.left-click=select_default_device_popup Requires the following executable: * pulseaudio @@ -36,7 +20,6 @@ Requires the following executable: import re import logging -import functools import core.module import core.widget @@ -46,14 +29,10 @@ import util.cli import util.graph import util.format -try: - import util.popup -except ImportError as e: - logging.warning("Couldn't import util.popup: %s. Popups won't work!", e) class Module(core.module.Module): def __init__(self, config, theme, channel): - super().__init__(config, theme, core.widget.Widget(self.display)) + super().__init__(config, theme, core.widget.Widget(self.volume)) if util.format.asbool(self.parameter("autostart", False)): util.cli.execute("pulseaudio --start", ignore_errors=True) @@ -69,11 +48,7 @@ class Module(core.module.Module): self._mute = False self._failed = False self._channel = channel - self.__selected_default_device = None self._showbars = util.format.asbool(self.parameter("showbars", 0)) - self.__show_device_name = util.format.asbool( - self.parameter("showdevicename", False) - ) self._patterns = [ {"expr": "Name:", "callback": (lambda line: False)}, @@ -148,12 +123,9 @@ class Module(core.module.Module): m = re.search(r"mono:.*\s*\/\s*(\d+)%", line) if m: self._mono = m.group(1) - self._left = 0 - self._right = 0 else: m = re.search(r"left:.*\s*\/\s*(\d+)%.*right:.*\s*\/\s*(\d+)%", line) if m: - self._mono = 0 self._left = m.group(1) self._right = m.group(2) @@ -166,19 +138,19 @@ class Module(core.module.Module): logging.error("no pulseaudio device found") return "n/a" - def display(self, widget): + def volume(self, widget): if self._failed == True: return "n/a" - - vol = None if int(self._mono) > 0: vol = "{}%".format(self._mono) if self._showbars: vol = "{} {}".format(vol, util.graph.hbar(float(self._mono))) + return vol elif self._left == self._right: vol = "{}%".format(self._left) if self._showbars: vol = "{} {}".format(vol, util.graph.hbar(float(self._left))) + return vol else: vol = "{}%/{}%".format(self._left, self._right) if self._showbars: @@ -187,31 +159,19 @@ class Module(core.module.Module): util.graph.hbar(float(self._left)), util.graph.hbar(float(self._right)), ) - - output = vol - if self.__show_device_name: - friendly_name = self.parameter( - self.__selected_default_device, self.__selected_default_device - ) - icon = self.parameter("icon." + self.__selected_default_device, "") - output = ( - icon + " " + friendly_name + " | " + vol - if icon != "" - else friendly_name + " | " + vol - ) - return output + return vol def update(self): try: self._failed = False channel = "sinks" if self._channel == "sink" else "sources" - self.__selected_default_device = self._default_device() + device = self._default_device() result = util.cli.execute("pactl list {}".format(channel)) found = False for line in result.split("\n"): - if "Name: {}".format(self.__selected_default_device) in line: + if "Name: {}".format(device) in line: found = True continue if found is False: @@ -229,32 +189,11 @@ class Module(core.module.Module): else: raise e - def __on_sink_selected(self, sink_name): - util.cli.execute("pactl set-default-{} {}".format(self._channel, sink_name)) - - def select_default_device_popup(self, widget): - channel = "sinks" if self._channel == "sink" else "sources" - result = util.cli.execute("pactl list {} short".format(channel)) - - menu = util.popup.menu(self.__config) - lines = result.splitlines() - for line in lines: - info = line.split("\t") - try: - friendly_name = self.parameter(info[1], info[1]) - menu.add_menuitem( - friendly_name, - callback=functools.partial(self.__on_sink_selected, info[1]), - ) - except: - logging.exception("Couldn't parse {}".format(channel)) - pass - - menu.show(widget) - def state(self, widget): if self._mute: return ["warning", "muted"] + if int(self._left) > int(100): + return ["critical", "unmuted"] return ["unmuted"] diff --git a/bumblebee_status/modules/core/pulsectl.py b/bumblebee_status/modules/core/pulsectl.py deleted file mode 100644 index 63f9e36..0000000 --- a/bumblebee_status/modules/core/pulsectl.py +++ /dev/null @@ -1,207 +0,0 @@ -# pylint: disable=C0111,R0903 - -"""Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. - -**Please prefer this module over the "pulseaudio" module, which will eventually be deprecated - -Aliases: pulseout (for outputs, such as headsets, speakers), pulsein (for microphones) - -NOTE: Do **not** use this module directly, but rather use either pulseout or pulsein! -NOTE2: For the parameter names below, please also use pulseout or pulsein, instead of pulsectl - -Parameters: - * pulsectl.autostart: If set to 'true' (default is 'false'), automatically starts the pulsectl daemon if it is not running - * pulsectl.percent_change: How much to change volume by when scrolling on the module (default is 2%) - * pulsectl.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') - * pulsectl.popup-filter: Comma-separated list of device strings (if the device name contains it) to exclude - from the default device popup menu (e.g. Monitor for sources) - * pulsectl.showbars: 'true' for showing volume bars, requires --markup=pango; - 'false' for not showing volume bars (default) - * pulsectl.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. - Per default, the sink/source name returned by "pactl list sinks short" is used as display name. - - As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"), - its possible to map the name to more a user friendly name. - - e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following - bumblebee-status config entry: pulsectl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset - - Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the - "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: - pulsectl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 - * Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current - default device add the following config entry to your bumblebee-status config: pulsectl.left-click=select_default_device_popup - -Requires the following Python module: - * pulsectl -""" - -import pulsectl -import logging -import functools - -import core.module -import core.widget -import core.input -import core.event - -import util.cli -import util.graph -import util.format - -try: - import util.popup -except ImportError as e: - logging.warning("Couldn't import util.popup: %s. Popups won't work!", e) - -class Module(core.module.Module): - def __init__(self, config, theme, type): - super().__init__(config, theme, core.widget.Widget(self.display)) - self.background = True - - self.__type = type - self.__volume = 0 - self.__devicename = "n/a" - self.__muted = False - self.__showbars = util.format.asbool(self.parameter("showbars", False)) - self.__show_device_name = util.format.asbool( - self.parameter("showdevicename", False) - ) - - self.__change = util.format.asint( - self.parameter("percent_change", "2%").strip("%"), 0, 100 - ) - self.__limit = util.format.asint(self.parameter("limit", "0%").strip("%"), 0) - popup_filter_param = self.parameter("popup-filter", []) - if popup_filter_param == '': - self.__popup_filter = [] - else: - self.__popup_filter = util.format.aslist(popup_filter_param) - - events = [ - { - "type": "mute", - "action": self.toggle_mute, - "button": core.input.LEFT_MOUSE - }, - { - "type": "volume", - "action": self.increase_volume, - "button": core.input.WHEEL_UP, - }, - { - "type": "volume", - "action": self.decrease_volume, - "button": core.input.WHEEL_DOWN, - }, - ] - - for event in events: - core.input.register(self, button=event["button"], cmd=event["action"]) - - if util.format.asbool(self.parameter("autostart", False)): - util.cli.execute("pulseaudio --start", ignore_errors=True) - - self.process(None) - - def display(self, _): - res = f"{int(self.__volume*100)}%" - if self.__showbars: - res = f"{res} {util.graph.hbar(self.__volume*100)}" - - if self.__show_device_name: - friendly_name = self.parameter(self.__devicename, self.__devicename) - icon = self.parameter("icon." + self.__devicename, "") - res = ( - icon + " " + friendly_name + " | " + res - if icon != "" - else friendly_name + " | " + res - ) - return res - - def toggle_mute(self, _): - with pulsectl.Pulse(self.id + "vol") as pulse: - dev = self.get_device(pulse) - if not dev: - return - pulse.mute(dev, not self.__muted) - - def change_volume(self, amount): - with pulsectl.Pulse(self.id + "vol") as pulse: - dev = self.get_device(pulse) - if not dev: - return - vol = dev.volume - vol.value_flat += amount - if self.__limit > 0 and vol.value_flat > self.__limit/100: - vol.value_flat = self.__limit/100 - pulse.volume_set(dev, vol) - - def increase_volume(self, _): - self.change_volume(self.__change/100.0) - - def decrease_volume(self, _): - self.change_volume(-self.__change/100.0) - - def get_device(self, pulse): - devs = pulse.sink_list() if self.__type == "sink" else pulse.source_list() - default = pulse.server_info().default_sink_name if self.__type == "sink" else pulse.server_info().default_source_name - - for dev in devs: - if dev.name == default: - return dev - if len(devs) == 0: - return None - - return devs[0] # fallback - - - def process(self, _): - with pulsectl.Pulse(self.id + "proc") as pulse: - dev = self.get_device(pulse) - if not dev: - self.__volume = 0 - self.__devicename = "n/a" - else: - self.__volume = dev.volume.value_flat - self.__muted = dev.mute - self.__devicename = dev.name - core.event.trigger("update", [self.id], redraw_only=True) - core.event.trigger("draw") - - def update(self): - with pulsectl.Pulse(self.id) as pulse: - pulse.event_mask_set(self.__type) - pulse.event_callback_set(self.process) - pulse.event_listen() - - def select_default_device_popup(self, widget): - with pulsectl.Pulse(self.id) as pulse: - if self.__type == "sink": - devs = pulse.sink_list() - else: - devs = pulse.source_list() - - devs = filter(lambda dev: not any(filter in dev.description for filter in self.__popup_filter), devs) - menu = util.popup.menu(self.__config) - for dev in devs: - menu.add_menuitem( - dev.description, - callback=functools.partial(self.__on_default_changed, dev), - ) - menu.show(widget) - - def __on_default_changed(self, dev): - with pulsectl.Pulse(self.id) as pulse: - pulse.default_set(dev) - - def state(self, _): - if self.__muted: - return ["warning", "muted"] - if self.__volume >= .5: - return ["unmuted", "unmuted-high"] - if self.__volume >= .1: - return ["unmuted", "unmuted-mid"] - return ["unmuted", "unmuted-low"] - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/pulsein.py b/bumblebee_status/modules/core/pulsein.py deleted file mode 100644 index 0034dd0..0000000 --- a/bumblebee_status/modules/core/pulsein.py +++ /dev/null @@ -1,9 +0,0 @@ -from .pulsectl import Module - - -class Module(Module): - def __init__(self, config, theme): - super().__init__(config, theme, "source") - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/pulseout.py b/bumblebee_status/modules/core/pulseout.py deleted file mode 100644 index 31fecec..0000000 --- a/bumblebee_status/modules/core/pulseout.py +++ /dev/null @@ -1,9 +0,0 @@ -from .pulsectl import Module - - -class Module(Module): - def __init__(self, config, theme): - super().__init__(config, theme, "sink") - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/redshift.py b/bumblebee_status/modules/core/redshift.py index c5835cd..c60ed9e 100644 --- a/bumblebee_status/modules/core/redshift.py +++ b/bumblebee_status/modules/core/redshift.py @@ -12,7 +12,6 @@ Parameters: * redshift.lat : latitude if location is set to 'manual' * redshift.lon : longitude if location is set to 'manual' * redshift.show_transition: information about the transitions (x% day) defaults to True - * redshift.adjust: set this to 'true' (defaults to false) to let bumblebee-status adjust color temperature, instead of just showing the current settings """ import re @@ -39,13 +38,7 @@ def get_redshift_value(module): if location == "manual" and (lat is None or lon is None): location = "geoclue2" - command = ["redshift"] - - if util.format.asbool(module.parameter("adjust", "false")) == True: - command.extend(["-o", "-v"]) - else: - command.append("-p") - + command = ["redshift", "-p"] if location == "manual": command.extend(["-l", "{}:{}".format(lat, lon)]) if location == "geoclue2": @@ -61,7 +54,7 @@ def get_redshift_value(module): for line in res.split("\n"): line = line.lower() if "temperature" in line: - widget.set("temp", line.split(" ")[2].upper()) + widget.set("temp", line.split(" ")[2]) if "period" in line: state = line.split(" ")[1] if "day" in state: @@ -108,7 +101,7 @@ class Module(core.module.Module): return val def update(self): - if self.__thread is not None and self.__thread.is_alive(): + if self.__thread is not None and self.__thread.isAlive(): return self.__thread = threading.Thread(target=get_redshift_value, args=(self,)) self.__thread.start() diff --git a/bumblebee_status/modules/core/scroll.py b/bumblebee_status/modules/core/scroll.py deleted file mode 100644 index b897c5b..0000000 --- a/bumblebee_status/modules/core/scroll.py +++ /dev/null @@ -1,53 +0,0 @@ -# pylint: disable=C0111,R0903 - -"""Displays two widgets that can be used to scroll the whole status bar - -Parameters: - * scroll.width: Width (in number of widgets) to display -""" - -import core.module -import core.widget -import core.input -import core.event - -import util.format - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, []) - self.__offset = 0 - self.__widgetcount = 0 - w = self.add_widget(full_text = "<") - core.input.register(w, button=core.input.LEFT_MOUSE, cmd=self.scroll_left) - w = self.add_widget(full_text = ">") - core.input.register(w, button=core.input.LEFT_MOUSE, cmd=self.scroll_right) - self.__width = util.format.asint(self.parameter("width")) - config.set("output.width", self.__width) - core.event.register("output.done", self.update_done) - - - def scroll_left(self, _): - if self.__offset > 0: - core.event.trigger("output.scroll-left") - - def scroll_right(self, _): - if self.__offset + self.__width < self.__widgetcount: - core.event.trigger("output.scroll-right") - - def update_done(self, offset, widgetcount): - self.__offset = offset - self.__widgetcount = widgetcount - - def scroll(self): - return False - - def state(self, widget): - if widget.id == self.widgets()[0].id: - if self.__offset == 0: - return ["warning"] - elif self.__offset + self.__width >= self.__widgetcount: - return ["warning"] - return [] - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/sensors2.py b/bumblebee_status/modules/core/sensors2.py index 68b58b6..5f101fa 100644 --- a/bumblebee_status/modules/core/sensors2.py +++ b/bumblebee_status/modules/core/sensors2.py @@ -11,7 +11,7 @@ Parameters: * sensors2.showother: Enable or display 'other' sensor readings (default: false) * sensors2.showname: Enable or disable show of sensor name (default: false) * sensors2.chip_include: Comma-separated list of chip to include (defaults to '' will include all by default, example: 'coretemp,bat') - * sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exclude none by default) + * sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exlude none by default) * sensors2.field_include: Comma separated list of chip to include (defaults to '' will include all by default, example: 'temp,fan') * sensors2.field_exclude: Comma separated list of chip to exclude (defaults to '' will exclude none by default) * sensors2.chip_field_exclude: Comma separated list of chip field to exclude (defaults to '' will exclude none by default, example: 'coretemp-isa-0000.temp1,coretemp-isa-0000.fan1') diff --git a/bumblebee_status/modules/core/spacer.py b/bumblebee_status/modules/core/spacer.py index e5a2d5e..7e4453a 100644 --- a/bumblebee_status/modules/core/spacer.py +++ b/bumblebee_status/modules/core/spacer.py @@ -9,7 +9,7 @@ Parameters: import core.module import core.widget import core.decorators -import core.input + class Module(core.module.Module): @core.decorators.every(minutes=60) @@ -20,8 +20,5 @@ class Module(core.module.Module): def text(self, _): return self.__text - def update_text(self, event): - self.__text = core.input.button_name(event["button"]) - # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/speedtest.py b/bumblebee_status/modules/core/speedtest.py deleted file mode 100644 index 947666a..0000000 --- a/bumblebee_status/modules/core/speedtest.py +++ /dev/null @@ -1,65 +0,0 @@ -# pylint: disable=C0111,R0903 - -"""Performs a speedtest - only updates when the "play" button is clicked - -Requires the following python module: - * speedtest-cli - -""" - -import sys - -import core.module -import core.widget -import core.input -import core.event -import core.decorators - -import speedtest - - -class Module(core.module.Module): - @core.decorators.never - def __init__(self, config, theme): - super().__init__(config, theme, []) - - self.background = True - self.__result = "" - self.__running = False - - start = self.add_widget(name="start") - main = self.add_widget(name="main", full_text=self.result) - - core.input.register(start, button=core.input.LEFT_MOUSE, cmd=self.update_event) - - def result(self, _): - return self.__result - - def update_event(self, _): - self.__running = True - self.update() - - def update(self): - if not self.__running: - return - core.event.trigger("update", [self.id], redraw_only=True) - s = speedtest.Speedtest() - s.get_best_server() - s.download(threads=None) - s.upload(threads=None) - - self.__result = "ping: {:.2f}ms down: {:.2f}Mbps up: {:.2f}Mbps".format( - s.results.ping, - s.results.download / 1024 / 1024, - s.results.upload / 1024 / 1024, - ) - self.__running = False - core.event.trigger("update", [self.id], redraw_only=True) - - def state(self, widget): - if widget.name == "start": - return "running" if self.__running else "not-running" - return None - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/core/time.py b/bumblebee_status/modules/core/time.py index 020bef1..1c2c4b1 100644 --- a/bumblebee_status/modules/core/time.py +++ b/bumblebee_status/modules/core/time.py @@ -12,6 +12,7 @@ from .datetime import Module class Module(Module): + @core.decorators.every(seconds=59) # ensures one update per minute def __init__(self, config, theme): super().__init__(config, theme) diff --git a/bumblebee_status/modules/core/vault.py b/bumblebee_status/modules/core/vault.py index ba316bc..0ec4257 100644 --- a/bumblebee_status/modules/core/vault.py +++ b/bumblebee_status/modules/core/vault.py @@ -4,15 +4,11 @@ Many thanks to [@bbernhard](https://github.com/bbernhard) for the idea! -Requires the following executable: - * pass (aka password-store) - Parameters: * vault.duration: Duration until password is cleared from clipboard (defaults to 30) * vault.location: Location of the password store (defaults to ~/.password-store) * vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0) - * vault.text: Text to display on the widget (defaults to ) Many thanks to `bbernhard `_ for the idea! """ @@ -52,7 +48,7 @@ def build_menu(parent, current_directory, callback): ) else: - submenu = util.popup.menu(self.__config, parent, leave=False) + submenu = util.popup.menu(parent, leave=False) build_menu( submenu, os.path.join(current_directory, entry.name), callback ) @@ -73,7 +69,7 @@ class Module(core.module.Module): core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) def popup(self, widget): - menu = util.popup.menu(self.__config, leave=False) + menu = util.popup.menu(leave=False) build_menu(menu, self.__path, self.__callback) menu.show(widget, offset_x=self.__offx, offset_y=self.__offy) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index 699599f..04278ba 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -8,9 +8,6 @@ Parameters: and appending a file '~/.config/i3/config.' for every screen. * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the module will only refresh when displays are enabled or disabled (defaults to true) - * xrandr.exclude: Comma-separated list of display name prefixes to exclude - * xrandr.autotoggle: Boolean flag to automatically enable new displays (defaults to false) - * xrandr.autotoggle_side: Which side to put autotoggled displays on ('right' or 'left', defaults to 'right') Requires the following python module: * (optional) i3 - if present, the need for updating the widget list is auto-detected @@ -19,6 +16,7 @@ Requires the following executable: * xrandr """ +import os import re import sys @@ -33,151 +31,106 @@ import util.format try: import i3 -except Exception: +except: pass -RESOLUTION_REGEX = re.compile(r"\d+x\d+\+(\d+)\+\d+") - - -class DisplayInfo: - def __init__(self, name, resolution, connected, added, removed): - self.name = name - self.active = resolution is not None - self.connected = connected - self.added = added - self.removed = removed - - self.position = int(resolution.group(1)) if self.active else sys.maxsize - self.state = "on" if self.active else "off" - - def __str__(self): - return "DisplayInfo(name={}, active={}, connected={}, added={}, removed={}, position={}, state={})".format( - self.name, - self.active, - self.connected, - self.added, - self.removed, - self.position, - self.state, - ) - - def __repr__(self): - return str(self) - - class Module(core.module.Module): @core.decorators.every(seconds=5) # takes up to 5s to detect a new screen def __init__(self, config, theme): super().__init__(config, theme, []) - self._exclude = tuple(util.format.aslist(self.parameter("exclude"))) self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) - self._autotoggle = util.format.asbool(self.parameter("autotoggle", False)) - self._autotoggle_side = self.parameter("autotoggle_side", "right") - - self._connected_displays = [] - self._active_displays = [] - self._initialized = False + self._needs_update = True try: i3.Subscription(self._output_update, "output") - except Exception: + except: pass - def _output_update(self, *_): - self.update(force=True) - - def _query_displays(self): - displays = [] - - for line in util.cli.execute("xrandr -q").split("\n"): - # disconnected or connected - if "connected" not in line: - continue - - name = line.split(" ", 2)[0] - resolution = RESOLUTION_REGEX.search(line) - active = resolution is not None - - connected = "disconnected" not in line - added = connected and not active and name not in self._connected_displays - removed = not connected and active and name in self._active_displays - - displays.append(DisplayInfo(name, resolution, connected, added, removed)) - - self._connected_displays = [ - display.name for display in displays if display.connected - ] - self._active_displays = [display.name for display in displays if display.active] - - return displays - - def update(self, force=False): - if not (self._autoupdate or force or not self._initialized): - return + def _output_update(self, event, data, _): + self._needs_update = True + def update(self): self.clear_widgets() - for display in self._query_displays(): - if display.name.startswith(self._exclude): + if self._autoupdate == False and self._needs_update == False: + return + + self._needs_update = False + + for line in util.cli.execute("xrandr -q").split("\n"): + if not " connected" in line: continue + display = line.split(" ", 2)[0] + m = re.search(r"\d+x\d+\+(\d+)\+\d+", line) - if self._initialized and self._autotoggle: - if display.added: - self._enable_display(display.name, self._autotoggle_side) - elif display.removed: - self._disable_display(display.name) + widget = self.widget(display) + if not widget: + widget = self.add_widget(full_text=display, name=display) + core.input.register(widget, button=1, cmd=self._toggle) + core.input.register(widget, button=3, cmd=self._toggle) + widget.set("state", "on" if m else "off") + widget.set("pos", int(m.group(1)) if m else sys.maxsize) - if not display.connected: - continue - - widget = self.add_widget(full_text=display.name, name=display.name) - core.input.register(widget, button=1, cmd=self._toggle) - core.input.register(widget, button=3, cmd=self._toggle) - - widget.set("state", display.state) - widget.set("pos", display.position) - - if not self._autoupdate: + if self._autoupdate == False: widget = self.add_widget(full_text="") widget.set("state", "refresh") - core.input.register(widget, button=1, cmd=self.update) - - self._initialized = True + core.input.register(widget, button=1, cmd=self._refresh) def state(self, widget): return widget.get("state", "off") - def _toggle_cmd(self): - if util.format.asbool(self.parameter("overwrite_i3config", False)): - return utility("toggle-display.sh") - else: - return "xrandr" - - def _disable_display(self, name): - if len(self._active_displays) > 1: - util.cli.execute("{} --output {} --off".format(self._toggle_cmd(), name)) - - def _enable_display(self, name, side=None): - # TODO: is there ever a case when there isn't a neighbor? - command = "{} --output {} --auto".format(self._toggle_cmd(), name) - if side and self._active_displays: - neighbor_index = 0 if side == "left" else -1 - command += " --{}-of {}".format(side, self._active_displays[neighbor_index]) - - util.cli.execute(command) + def _refresh(self, event): + self._needs_update = True def _toggle(self, event): - widget = self.widget(widget_id=event["instance"]) + self._refresh(self, event) + + if util.format.asbool(self.parameter("overwrite_i3config", False)) == True: + toggle_cmd = utility("toggle-display.sh") + else: + toggle_cmd = "xrandr" + + widget = self.widget_by_id(event["instance"]) if widget.get("state") == "on": - self._disable_display(widget.name) + util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) else: - side = "left" if event["button"] == core.input.LEFT_MOUSE else "right" - self._enable_display(widget.name, side) + first_neighbor = next( + (widget for widget in self.widgets() if widget.get("state") == "on"), + None, + ) + last_neighbor = next( + ( + widget + for widget in reversed(self.widgets()) + if widget.get("state") == "on" + ), + None, + ) - self.update(force=True) + neighbor = ( + first_neighbor + if event["button"] == core.input.LEFT_MOUSE + else last_neighbor + ) + + if neighbor is None: + util.cli.execute( + "{} --output {} --auto".format(toggle_cmd, widget.name) + ) + else: + util.cli.execute( + "{} --output {} --auto --{}-of {}".format( + toggle_cmd, + widget.name, + "left" + if event.get("button") == core.input.LEFT_MOUSE + else "right", + neighbor.name, + ) + ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/util/cli.py b/bumblebee_status/util/cli.py index 4ef25d9..3e4face 100644 --- a/bumblebee_status/util/cli.py +++ b/bumblebee_status/util/cli.py @@ -30,41 +30,19 @@ def execute( """ args = cmd if shell else shlex.split(cmd) logging.debug(cmd) - - if not env: - env = os.environ.copy() - - myenv = env.copy() - - myenv["LC_ALL"] = "C" - if "WAYLAND_SOCKET" in myenv: - del myenv["WAYLAND_SOCKET"] - try: proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT if include_stderr else subprocess.PIPE, - env=myenv, + env=env, shell=shell, ) except FileNotFoundError as e: raise RuntimeError("{} not found".format(cmd)) if wait: - timeout = 60 - try: - out, _ = proc.communicate(timeout=timeout) - except subprocess.TimeoutExpired as e: - logging.warning( - f""" - Communication with process pid={proc.pid} hangs for more - than {timeout} seconds. - If this is not expected, the process is stale, or - you might have run in stdout / stderr deadlock. - """ - ) - out, _ = proc.communicate() + out, _ = proc.communicate() if proc.returncode != 0: err = "{} exited with code {}".format(cmd, proc.returncode) logging.warning(err) diff --git a/bumblebee_status/util/format.py b/bumblebee_status/util/format.py index 3acf440..198e995 100644 --- a/bumblebee_status/util/format.py +++ b/bumblebee_status/util/format.py @@ -68,36 +68,25 @@ def astemperature(val, unit="metric"): :return: temperature representation of the input value :rtype: string """ - return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"])) + return "{}°{}".format(int(val), __UNITS.get(unit, __UNITS["default"])) -def byte(val, fmt="{:.2f}", sys="IEC"): +def byte(val, fmt="{:.2f}"): """Returns a byte representation of the input value :param val: value to format, must be convertible into a float :param fmt: optional output format string, defaults to {:.2f} - :param sys: optional unit system specifier - SI (kilo, Mega, Giga, ...) or - IEC (kibi, Mebi, Gibi, ...) - defaults to IEC :return: byte representation (e.g. KiB, GiB, etc.) of the input value :rtype: string """ - if sys == "IEC": - div = 1024.0 - units = ["", "Ki", "Mi", "Gi", "Ti"] - final = "TiB" - elif sys == "SI": - div = 1000.0 - units = ["", "K", "M", "G", "T"] - final = "TB" - val = float(val) - for unit in units: - if val < div: + for unit in ["", "Ki", "Mi", "Gi"]: + if val < 1024.0: return "{}{}B".format(fmt, unit).format(val) - val /= div - return "{}{}".format(fmt).format(val * div, final) + val /= 1024.0 + return "{}GiB".format(fmt).format(val * 1024.0) __seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?") diff --git a/bumblebee_status/util/location.py b/bumblebee_status/util/location.py index 00ac5fd..f8cb441 100644 --- a/bumblebee_status/util/location.py +++ b/bumblebee_status/util/location.py @@ -3,10 +3,8 @@ service and caches it for 12h (retries are done every 30m in case of problems) Right now, it uses (in order of preference): - - http://free.ipwhois.io/ - 10k free requests/month - - http://ipapi.co/ - 30k free requests/month - - http://ip-api.com/ - ~2m free requests/month - + - http://free.ipwhois.io/ + - http://ipapi.co/ """ @@ -23,34 +21,19 @@ __sources = [ "mapping": { "latitude": "latitude", "longitude": "longitude", - "country": "country_name", - "country_code": "country_code", - "city": "city_name", + "country": "country", "ip": "public_ip", }, }, - { - "url": "http://ip-api.com/json", - "mapping": { - "lat": "latitude", - "lon": "longitude", - "country": "country_name", - "countryCode": "country_code", - "city": "city_name", - "query": "public_ip", - }, - }, { "url": "http://ipapi.co/json", "mapping": { "latitude": "latitude", "longitude": "longitude", - "country_name": "country_name", - "country_code": "country_code", - "city": "city_name", + "country_name": "country", "ip": "public_ip", }, - } + }, ] @@ -76,22 +59,17 @@ def __load(): __next = time.time() + 60 * 30 # error - try again every 30m -def __get(name): +def __get(name, default=None): global __data if not __data or __expired(): __load() - if name in __data: - return __data[name] - else: - return None + return __data.get(name, default) def reset(): - """Resets the location library, ensuring that a new query will be started""" + """Resets the location library, ensuring that a new query will be started + """ global __next - global __data - - __data = None __next = 0 @@ -110,25 +88,7 @@ def country(): :return: country name :rtype: string """ - return __get("country_name") - - -def country_code(): - """Returns the current country code - - :return: country code - :rtype: string - """ - return __get("country_code") - - -def city_name(): - """Returns the current city name - - :return: city name - :rtype: string - """ - return __get("city_name") + return __get("country") def public_ip(): @@ -140,20 +100,4 @@ def public_ip(): return __get("public_ip") -def location_info(): - """Returns the current location information - - :return: public IP, country name, country code, city name & coordinates - :rtype: dictionary - """ - return { - "public_ip": __get("public_ip"), - "country": __get("country_name"), - "country_code": __get("country_code"), - "city_name": __get("city_name"), - "latitude": __get("latitude"), - "longitude": __get("longitude"), - } - - # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/util/popup.py b/bumblebee_status/util/popup.py index cd083f7..f162846 100644 --- a/bumblebee_status/util/popup.py +++ b/bumblebee_status/util/popup.py @@ -3,7 +3,6 @@ import logging import tkinter as tk -import tkinter.font as tkFont import functools @@ -11,27 +10,29 @@ import functools class menu(object): """Draws a hierarchical popup menu - :param config: Global config singleton, passed on from modules :param parent: If given, this menu is a leave of the "parent" menu :param leave: If set to True, close this menu when mouse leaves the area (defaults to True) """ - def __init__(self, config, parent=None, leave=True): + def __init__(self, parent=None, leave=True): self.running = True + self.parent = None + if not parent: + self._root = tk.Tk() + self._root.withdraw() + self._menu = tk.Menu(self._root, tearoff=0) + self._menu.bind("", self.__on_focus_out) - self.parent = parent - - self._root = parent.root() if parent else tk.Tk() - self._root.withdraw() - self._menu = tk.Menu(self._root, tearoff=0) - self._menu.bind("", self.__on_focus_out) - self._font_size = tkFont.Font(size=config.popup_font_size()) - - if leave: - self._menu.bind("", self.__on_focus_out) - elif not parent: self.add_menuitem("close", self.__on_focus_out) self.add_separator() + else: + self._root = parent.root() + self._root.withdraw() + self._menu = tk.Menu(self._root, tearoff=0) + self._menu.bind("", self.__on_focus_out) + self.parent = parent + if leave: + self._menu.bind("", self.__on_focus_out) self._menu.bind("", self.release) @@ -52,7 +53,6 @@ class menu(object): return self._menu def __on_focus_out(self, event=None): - self.running = False self._root.destroy() def __on_click(self, callback): @@ -71,7 +71,7 @@ class menu(object): """ def add_cascade(self, menuitem, submenu): - self._menu.add_cascade(label=menuitem, menu=submenu.menu(), font=self._font_size) + self._menu.add_cascade(label=menuitem, menu=submenu.menu()) """Adds an item to the current menu @@ -81,7 +81,7 @@ class menu(object): def add_menuitem(self, menuitem, callback): self._menu.add_command( - label=menuitem, command=functools.partial(self.__on_click, callback), font=self._font_size, + label=menuitem, command=functools.partial(self.__on_click, callback) ) """Adds a separator to the menu in the current location""" diff --git a/bumblebee_status/util/xresources.py b/bumblebee_status/util/xresources.py deleted file mode 100644 index 70665a9..0000000 --- a/bumblebee_status/util/xresources.py +++ /dev/null @@ -1,10 +0,0 @@ -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") - diff --git a/create-pkgbuild.py b/create-pkgbuild.py deleted file mode 100644 index 0ab79e3..0000000 --- a/create-pkgbuild.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -import sys -import json -import hashlib -import requests - -rv = requests.request( - "GET", - "https://api.github.com/repos/tobi-wan-kenobi/bumblebee-status/releases/latest", -) - -if rv.status_code != 200: - sys.exit(1) - -release = json.loads(rv.text) - -tar = requests.get(f"https://github.com/tobi-wan-kenobi/bumblebee-status/archive/{release['name']}.tar.gz") -checksum = hashlib.sha512(tar.content).hexdigest() - -template = "" -with open("./PKGBUILD.template") as f: - template = f.read() - -template = template.replace("", release["name"].lstrip("v")) -template = template.replace("", checksum) - -print(template) diff --git a/docs/FAQ.rst b/docs/FAQ.rst index 0aade9b..eeaff82 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -29,9 +29,9 @@ didn’t have background color support for the status bar. Some of the icons don’t render correctly ---------------------------------------- -Please check that you have `Font Awesome`_ installed (version 4). +Please check that you have |Font Awesome| installed (version 4). -.. note:: The `Font Awesome`_ is required for all themes that +.. note:: The |Font Awesome| is required for all themes that contain icons (because that is the font that includes these icons). Please refer to your distribution’s package management on how to install them, or get them from their website directly. Also, please note that @@ -52,15 +52,4 @@ Please check that you have `Font Awesome`_ installed (version 4). # Other # see https://github.com/gabrielelana/awesome-terminal-fonts -You might also need to add it to the `font` directive in your i3 configuration, for example: - -.. code-block:: - - bar { - font pango:FontAwesome, Fira mono 10 - status_command bumblebee-status -m title pasink pasource cpu memory battery datetime --iconset awesome-fonts - } - -If you are unsure about how the font is named, you can use the ``pango-list`` command line tool to look at the fonts installed on your computer. Also note how you can specify multiple fonts, separated by commas, in the above example. - -.. _Font Awesome: https://fontawesome.com/ +.. |Font Awesome| image:: https://fontawesome.com/ diff --git a/docs/conf.py b/docs/conf.py index 722e6f6..2097441 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,8 +49,6 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # a list of builtin themes. # html_theme = "sphinx_rtd_theme" -html_logo = "logo.png" -html_favicon = "favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/development/general.rst b/docs/development/general.rst index 0ec89a0..824d1a7 100644 --- a/docs/development/general.rst +++ b/docs/development/general.rst @@ -1,15 +1,34 @@ General guidelines ================== -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) +Writing unit tests +------------------ -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. +Some general hints: -Also, the (small) community that has gathered around ``bumblebee-status`` -is extremely friendly and helpful, so don't hesitate to create issues -with questions, somebody will always come up with a useful answer. +- 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`` 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``. diff --git a/docs/development/index.rst b/docs/development/index.rst index 6881fdb..179dee0 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -8,5 +8,4 @@ Developer's Guide general module theme - testing diff --git a/docs/development/module.rst b/docs/development/module.rst index 32a653a..1d6e716 100644 --- a/docs/development/module.rst +++ b/docs/development/module.rst @@ -11,12 +11,10 @@ Adding a new module to ``bumblebee-status`` is straight-forward: ``bumblebee-status`` (i.e. a module called ``bumblebee_status/modules/contrib/test.py`` will be loaded using ``bumblebee-status -m test``) -- Alternatively, you can put your module in ``~/.config/bumblebee-status/modules/`` -- The module name must follow the `Python Naming Conventions `_ - See below for how to actually write the module - Test (run ``bumblebee-status`` in the CLI) - Make sure your changes don’t break anything: ``./coverage.sh`` -- If you want to do me a favour, run your module through +- If you want to do me favour, run your module through ``black -t py34`` before submitting Pull requests @@ -24,7 +22,7 @@ Pull requests The project **gladly** accepts PRs for bugfixes, new functionality, new modules, etc. When you feel comfortable with what you’ve developed, -please just open a PR. Somebody will look at it eventually :) Thanks! +please just open a PR, somebody will look at it eventually :) Thanks! Coding guidelines ----------------- @@ -67,7 +65,7 @@ Of modules and widgets There are two important concepts for module writers: - A module is something that offers a single set of coherent functionality - A module -has 1 to n “widgets”, which translates to individual blocks in the i3bar. +has 1 to n “widgets”, which translates to individual blocks in the i3bar Very often, this is a 1:1 relationship, and a single module has a single widget. If that’s the case for you, you can stop reading now :) @@ -250,5 +248,5 @@ module using ``self.set()`` or via the CLI using the ``--parameter`` flag: - ``scrolling.width``: Integer, defaults to 30, determines the minimum width of the widgets, if ``makewide`` is specified - ``scrolling.makewide``: Boolean, defaults to true, determines whether the widgets should be expanded to their minwidth -- ``scrolling.bounce``: Boolean, defaults to true, determines whether the content should change directions when a scroll is completed, or just marquee through + ``scrolling.bounce``: Boolean, defaults to true, determines whether the content should change directions when a scroll is completed, or just marquee through diff --git a/docs/development/testing.rst b/docs/development/testing.rst deleted file mode 100644 index c5c9375..0000000 --- a/docs/development/testing.rst +++ /dev/null @@ -1,33 +0,0 @@ -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``. diff --git a/docs/development/theme.rst b/docs/development/theme.rst index 24e7cbe..7ea8af6 100644 --- a/docs/development/theme.rst +++ b/docs/development/theme.rst @@ -52,21 +52,6 @@ JSON file located in ``$(THEME_DIRECTORY)/icons/``. The format of the icon file is identical to the theme itself (as the two are essentially just merged into a single JSON. -To create an "icon-only" widget (e.g. the play/pause/forward/rewind buttons -of a media player), you need to do the following: - -1. In the module, create a widget, and set its state to a descriptive value - (for example `widget.set("state", "next")` -2. In the theme's icon definition JSON, define a `prefix` for that state: - -.. code:: json - - { - "spotify": { - "next": { "prefix": "" } - }, - } - Color definitions and pyWAL support ----------------------------------- diff --git a/docs/favicon.ico b/docs/favicon.ico deleted file mode 100644 index 2da8b14..0000000 Binary files a/docs/favicon.ico and /dev/null differ diff --git a/docs/features.rst b/docs/features.rst index 603c7fe..162f3c3 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1,25 +1,6 @@ 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 ------ @@ -106,19 +87,6 @@ attention, it will remain hidden. Note that this parameter is specified *in addition* to ``-m`` (i.e. to autohide the CPU module, you would use ``bumblebee-status -m cpu memory traffic -a cpu``). -Scrolling widget text ------------------------ -Some widgets support scrolling for long text (e.g. most music player -widgets, rss, etc.). Those have some additional settings for customizing -the scrolling behaviour, in particular: - - - ``scrolling.width``: Desired width of the scrolling panel - - ``scrolling.makewide``: If set to true, extends texts shorter than - ``scrolling.width`` to that width - - ``scrolling.bounce``: If set to true, bounces the text when it reaches - the end, otherwise, it behaves like marquee (scroll-through) text - - ``scrolling.speed``: Defines the scroll speed, in characters per update - Additional widget theme settings -------------------------------- @@ -142,10 +110,6 @@ An example: Configuration files ------------------- -Using a configuration file, it is possible to define a list of modules -that will be loaded if no modules are specified on the CLI, as well as -defining a default theme to use. - Any parameter that can be specified using ``-p =`` on the commandline, can alternatively be specified in one of the following configuration files: - ~/.bumblebee-status.conf - @@ -158,11 +122,6 @@ Configuration files have the following format: :: - [core] - modules = - autohide = - theme = - [module-parameters] = @@ -172,5 +131,3 @@ For example: [module-parameters] github.token=abcdefabcdef12345 - - diff --git a/docs/index.rst b/docs/index.rst index 1ba4303..084a8fd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,8 +9,6 @@ Welcome to bumblebee-status's documentation! bumblebee-status is a modular, theme-able status line generator for the `i3 window manager `__. -Logo courtesy of [kellya](https://github.com/kellya) - thank you! - Focus is on: - ease of use, sane defaults (no mandatory configuration file) @@ -22,15 +20,15 @@ feature requests, etc. :) Thanks a lot! -+------------------------------------+------------------------------+ -| **Required i3wm version** | 4.12+ | -+------------------------------------+------------------------------+ -| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 | -+------------------------------------+------------------------------+ -| **Supported FontAwesome versions** | 4 only | -+------------------------------------+------------------------------+ -| **Per-module requirements** | see :doc:`modules` | -+------------------------------------+------------------------------+ ++------------------------------------+-------------------------+ +| **Required i3wm version** | 4.12+ | ++------------------------------------+-------------------------+ +| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8 | ++------------------------------------+-------------------------+ +| **Supported FontAwesome versions** | 4 only | ++------------------------------------+-------------------------+ +| **Per-module requirements** | see :doc:`modules` | ++------------------------------------+-------------------------+ see :doc:`FAQ` for details on this diff --git a/docs/introduction.rst b/docs/introduction.rst index 891b14c..927a384 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -19,9 +19,6 @@ Installation # will install bumblebee-status into ~/.local/bin/bumblebee-status pip install --user bumblebee-status - -There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)! - Dependencies ------------ @@ -44,14 +41,6 @@ like this: -t } -Line continuations (breaking a single line into multiple lines) is allowed in -the i3 configuration, but please ensure that all lines except the final one need to have a trailing -"\". -This is explained in detail here: -[i3 user guide: line continuation](https://i3wm.org/docs/userguide.html#line_continuation) - - - You can retrieve a list of modules (and their parameters) and themes by entering: @@ -67,15 +56,12 @@ To change the update interval, use: $ ./bumblebee-status -m -p interval= -The update interval is the global "refresh" interval of the modules (i.e. how often -the bar will be updated with new data). The default interval is one second. It is -possible to use suffixes such as "m" (for minutes), or "h" for hours (e.g. -``-p interval=5m`` to update once every 5 minutes. +The update interval can also be changed on a per-module basis, like +this: -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. +.. code-block:: bash -For more details on that, please refer to :doc:`features`. + $ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m All modules can be given “aliases” using ``:``, by which they can be parametrized, for example: diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100644 index c0d96c9..0000000 Binary files a/docs/logo.png and /dev/null differ diff --git a/docs/modules.rst b/docs/modules.rst index 0b3ff9d..ed99936 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,5 +1,3 @@ -.. THIS DOCUMENT IS AUTO-GENERATED, DO NOT MODIFY -.. To change this document, please update the docstrings in the individual modules List of modules =============== @@ -12,17 +10,13 @@ cpu Displays CPU utilization across all CPUs. -By default, opens `gnome-system-monitor` on left mouse click. - Requirements: * the psutil Python module for the first three items from the list above - * gnome-system-monitor for default mouse click action Parameters: * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) * cpu.critical: Critical threshold in % of CPU usage (defaults to 80%) * cpu.format : Format string (defaults to '{:.01f}%') - * cpu.percpu : If set to true, show each individual cpu (defaults to false) .. image:: ../screenshots/cpu.png @@ -60,11 +54,10 @@ Shows free diskspace, total diskspace and the percentage of free disk space. Parameters: * disk.warning: Warning threshold in % of disk space (defaults to 80%) - * disk.critical: Critical threshold in % of disk space (defaults to 90%) + * disk.critical: Critical threshold in % of disk space (defaults ot 90%) * disk.path: Path to calculate disk usage from (defaults to /) * disk.open: Which application / file manager to launch (default xdg-open) * disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)') - * disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC') .. image:: ../screenshots/disk.png @@ -85,49 +78,11 @@ Requires: .. image:: ../screenshots/git.png -keys -~~~~ - -Shows when a key is pressed - -Parameters: - * keys.keys: Comma-separated list of keys to monitor (defaults to "") - -layout -~~~~~~ - -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. - -.. image:: ../screenshots/layout.png - layout-xkb ~~~~~~~~~~ Displays the current keyboard layout using libX11 -Requires the following library: - * libX11.so.6 -and python module: - * xkbgroup - -Parameters: - * layout-xkb.showname: Boolean that indicate whether the full name should be displayed. Defaults to false (only the symbol will be displayed) - * layout-xkb.show_variant: Boolean that indecates whether the variant name should be displayed. Defaults to true. - -layout_xkb -~~~~~~~~~~ - -Displays the current keyboard layout using libX11 - Requires the following library: * libX11.so.6 and python module: @@ -142,11 +97,6 @@ load Displays system load. -By default, opens `gnome-system-monitor` on left mouse click. - -Requirements: - * gnome-system-monitor for default mouse click action - Parameters: * load.warning : Warning threshold for the one-minute load average (defaults to 70% of the number of CPUs) * load.critical: Critical threshold for the one-minute load average (defaults to 80% of the number of CPUs) @@ -158,11 +108,6 @@ memory Displays available RAM, total amount of RAM and percentage available. -By default, opens `gnome-system-monitor` on left mouse click. - -Requirements: - * gnome-system-monitor for default mouse click action - Parameters: * memory.warning : Warning threshold in % of memory used (defaults to 80%) * memory.critical: Critical threshold in % of memory used (defaults to 90%) @@ -179,17 +124,11 @@ Displays the name, IP address(es) and status of each available network interface Requires the following python module: * netifaces -Requires the following executable: - * iw - * (until and including 2.0.5: iwgetid) - Parameters: - * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi') + * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') * nic.include: Comma-separated list of interfaces to include * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) - * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {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) + * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') .. image:: ../screenshots/nic.png @@ -215,8 +154,6 @@ pulseaudio Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. -!!! This module will eventually be deprecated (since it has bad performance and high CPU load) and be replaced with "pulsectl", which is a much better drop-in replacement !!! - Aliases: pasink (use this to control output instead of input), pasource Parameters: @@ -226,20 +163,6 @@ Parameters: Note: If the left and right channels have different volumes, the limit might not be reached exactly. * pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango; 0 for not showing volume bars (default) - * pulseaudio.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. - Per default, the sink/source name returned by "pactl list sinks short" is used as display name. - - As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"), - its possible to map the name to more a user friendly name. - - e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following - bumblebee-status config entry: pulseaudio.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset - - Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the - "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: - pulseaudio.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 - * Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current - default device add the following config entry to your bumblebee-status config: pulseaudio.left-click=select_default_device_popup Requires the following executable: * pulseaudio @@ -248,44 +171,6 @@ Requires the following executable: .. image:: ../screenshots/pulseaudio.png -pulsectl -~~~~~~~~ - -Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. - -**Please prefer this module over the "pulseaudio" module, which will eventually be deprecated - -Aliases: pulseout (for outputs, such as headsets, speakers), pulsein (for microphones) - -NOTE: Do **not** use this module directly, but rather use either pulseout or pulsein! -NOTE2: For the parameter names below, please also use pulseout or pulsein, instead of pulsectl - -Parameters: - * pulsectl.autostart: If set to 'true' (default is 'false'), automatically starts the pulsectl daemon if it is not running - * pulsectl.percent_change: How much to change volume by when scrolling on the module (default is 2%) - * pulsectl.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') - * pulsectl.popup-filter: Comma-separated list of device strings (if the device name contains it) to exclude - from the default device popup menu (e.g. Monitor for sources) - * pulsectl.showbars: 'true' for showing volume bars, requires --markup=pango; - 'false' for not showing volume bars (default) - * pulsectl.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. - Per default, the sink/source name returned by "pactl list sinks short" is used as display name. - - As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"), - its possible to map the name to more a user friendly name. - - e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following - bumblebee-status config entry: pulsectl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset - - Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the - "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: - pulsectl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 - * Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current - default device add the following config entry to your bumblebee-status config: pulsectl.left-click=select_default_device_popup - -Requires the following Python module: - * pulsectl - redshift ~~~~~~~~ @@ -301,18 +186,9 @@ Parameters: * redshift.lat : latitude if location is set to 'manual' * redshift.lon : longitude if location is set to 'manual' * redshift.show_transition: information about the transitions (x% day) defaults to True - * redshift.adjust: set this to 'true' (defaults to false) to let bumblebee-status adjust color temperature, instead of just showing the current settings .. image:: ../screenshots/redshift.png -scroll -~~~~~~ - -Displays two widgets that can be used to scroll the whole status bar - -Parameters: - * scroll.width: Width (in number of widgets) to display - sensors2 ~~~~~~~~ @@ -327,7 +203,7 @@ Parameters: * sensors2.showother: Enable or display 'other' sensor readings (default: false) * sensors2.showname: Enable or disable show of sensor name (default: false) * sensors2.chip_include: Comma-separated list of chip to include (defaults to '' will include all by default, example: 'coretemp,bat') - * sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exclude none by default) + * sensors2.chip_exclude:Comma separated list of chip to exclude (defaults to '' will exlude none by default) * sensors2.field_include: Comma separated list of chip to include (defaults to '' will include all by default, example: 'temp,fan') * sensors2.field_exclude: Comma separated list of chip to exclude (defaults to '' will exclude none by default) * sensors2.chip_field_exclude: Comma separated list of chip field to exclude (defaults to '' will exclude none by default, example: 'coretemp-isa-0000.temp1,coretemp-isa-0000.fan1') @@ -345,14 +221,6 @@ Parameters: .. image:: ../screenshots/spacer.png -speedtest -~~~~~~~~~ - -Performs a speedtest - only updates when the "play" button is clicked - -Requires the following python module: - * speedtest-cli - test ~~~~ @@ -376,15 +244,11 @@ Copy passwords from a password store into the clipboard (currently supports only Many thanks to [@bbernhard](https://github.com/bbernhard) for the idea! -Requires the following executable: - * pass (aka password-store) - Parameters: * vault.duration: Duration until password is cleared from clipboard (defaults to 30) * vault.location: Location of the password store (defaults to ~/.password-store) * vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0) - * vault.text: Text to display on the widget (defaults to ) Many thanks to `bbernhard `_ for the idea! @@ -401,9 +265,6 @@ Parameters: and appending a file '~/.config/i3/config.' for every screen. * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the module will only refresh when displays are enabled or disabled (defaults to true) - * xrandr.exclude: Comma-separated list of display name prefixes to exclude - * xrandr.autotoggle: Boolean flag to automatically enable new displays (defaults to false) - * xrandr.autotoggle_side: Which side to put autotoggled displays on ('right' or 'left', defaults to 'right') Requires the following python module: * (optional) i3 - if present, the need for updating the widget list is auto-detected @@ -422,11 +283,7 @@ amixer get volume level or control it -Requires the following executable: - * amixer - Parameters: - * amixer.card: Sound Card to use (default is 0) * amixer.device: Device to use (default is Master,0) * amixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) @@ -434,8 +291,6 @@ contributed by `zetxx `_ - many thanks! input handling contributed by `ardadem `_ - many thanks! -multiple audio cards contributed by `hugoeustaquio `_ - many thanks! - .. image:: ../screenshots/amixer.png apt @@ -459,9 +314,6 @@ saved screen layout as well as toggle on/off individual connected displays. Parameters: * No configuration parameters -Requires the following python modules: - * tkinter - Requires the following executable: * arandr * xrandr @@ -478,30 +330,6 @@ Requires the following executable: contributed by `lucassouto `_ - many thanks! -.. image:: ../screenshots/arch-update.png - -arch_update -~~~~~~~~~~~ - -Check updates to Arch Linux. - -Requires the following executable: - * checkupdates (from pacman-contrib) - -contributed by `lucassouto `_ - many thanks! - -aur-update -~~~~~~~~~~ - -Check updates for AUR. - -Requires the following executable: - * yay (https://github.com/Jguer/yay) - -contributed by `ishaanbhimwal `_ - many thanks! - -.. image:: ../screenshots/aur-update.png - battery ~~~~~~~ @@ -532,22 +360,10 @@ Parameters: contributed by `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 `_ - many thanks! - bluetooth ~~~~~~~~~ -Displays bluetooth status (Bluez). Left mouse click launches manager app `blueman-manager`, +Displays bluetooth status (Bluez). Left mouse click launches manager app, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state. Parameters: @@ -564,7 +380,7 @@ contributed by `brunosmmm `_ - many thanks! bluetooth2 ~~~~~~~~~~ -Displays bluetooth status. Left mouse click launches manager app `blueman-manager`, +Displays bluetooth status. Left mouse click launches manager app, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and python-dbus to count the number of connections @@ -573,40 +389,13 @@ Parameters: contributed by `martindoublem `_ - many thanks! -blugon -~~~~~~ - -Displays temperature of blugon and Controls it. - -Use wheel up and down to change temperature, middle click to toggle and right click to reset temperature. - -Default Values: - * Minimum temperature: 1000 (red) - * Maximum temperature: 20000 (blue) - * Default temperature: 6600 - -Requires the following executable: - * blugon - -Parameters: - * blugon.step: The amount of increase/decrease on scroll (default: 200) - -contributed by `DTan13 ` - brightness ~~~~~~~~~~ Displays the brightness of a display -The following executables can be used if `use_acpi` is not enabled: - * brightnessctl - * light - * xbacklight - Parameters: * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) - * brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true - * brightness.use_acpi: If set to true, read brightness directly from the sys ACPI interface, using the device specified in brightness.device_path (defaults to false) contributed by `TheEdgeOfRage `_ - many thanks! @@ -681,58 +470,15 @@ Parameters: * cpu2.fanspeed * cpu2.colored: 1 for colored per core load graph, 0 for mono (default) * cpu2.temp_pattern: pattern to look for in the output of 'sensors -u'; - required if cpu2.temp widget is used + required if cpu2.temp widged is used * cpu2.fan_pattern: pattern to look for in the output of 'sensors -u'; - required if cpu2.fanspeed widget is used + required if cpu2.fanspeed widged is used Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're lacking the aforementioned pattern settings or they have wrong values. contributed by `somospocos `_ - many thanks! -cpu3 -~~~~ - -Multiwidget CPU module - -Can display any combination of: - - * max CPU frequency - * total CPU load in percents (integer value) - * per-core CPU load as graph - either mono or colored - * CPU temperature (in Celsius degrees) - * CPU fan speed - -Requirements: - - * the psutil Python module for the first three items from the list above - * sensors executable for the rest - -Parameters: - * cpu3.layout: Space-separated list of widgets to add. - Possible widgets are: - - * cpu3.maxfreq - * cpu3.cpuload - * cpu3.coresload - * cpu3.temp - * cpu3.fanspeed - * cpu3.colored: 1 for colored per core load graph, 0 for mono (default) - * cpu3.temp_json: json path to look for in the output of 'sensors -j'; - required if cpu3.temp widget is used - * cpu3.fan_json: json path to look for in the output of 'sensors -j'; - required if cpu3.fanspeed widget is used - -Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're -lacking the aforementioned json path settings or they have wrong values. - -Example json paths: - * `cpu3.temp_json="coretemp-isa-0000.Package id 0.temp1_input"` - * `cpu3.fan_json="thinkpad-isa-0000.fan1.fan1_input"` - -contributed by `SuperQ ` -based on cpu2 by `` - currency ~~~~~~~~ @@ -760,10 +506,6 @@ datetimetz Displays the current date and time with timezone options. -Requires the following python packages: - * tzlocal - * pytz - Parameters: * datetimetz.format : strftime()-compatible formatting string * datetimetz.timezone : IANA timezone name @@ -794,6 +536,8 @@ some media control bindings. Left click toggles pause, scroll up skips the current song, scroll down returns to the previous song. +Requires the following library: + * subprocess Parameters: * deadbeef.format: Format string (defaults to '{artist} - {title}') Available values are: {artist}, {title}, {album}, {length}, @@ -849,6 +593,9 @@ Displays DNF package update information (///`_ - many thanks! .. image:: ../screenshots/dunst.png -dunstctl -~~~~~~~~ - -Toggle dunst notifications using dunstctl. - -When notifications are paused using this module dunst doesn't get killed and -you'll keep getting notifications on the background that will be displayed when -unpausing. This is specially useful if you're using dunst's scripting -(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to -be running. Scripts will be executed when dunst gets unpaused. - -Requires: - * dunst v1.5.0+ - -Parameters: - * dunstctl.disabled(Boolean): dunst state on start - -contributed by `cristianmiranda `_ - many thanks! -contributed by `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 [1] original created by AnwariasEu. - -[1] https://github.com/ultrabug/py3status/blob/master/py3status/modules/emerge_status.py - -.. image:: ../screenshots/emerge_status.png - -gcalendar -~~~~~~~~~ - -Displays first upcoming event in google calendar. - -Events that are set as 'all-day' will not be shown. - -Requires credentials.json from a google api application where the google calendar api is installed. -On first time run the browser will open and google will ask for permission for this app to access -the google calendar and then save a .gcalendar_token.json file to the credentials_path directory -which stores this permission. - -A refresh is done every 15 minutes. - -Parameters: - * gcalendar.time_format: Format time output. Defaults to "%H:%M". - * gcalendar.date_format: Format date output. Defaults to "%d.%m.%y". - * gcalendar.credentials_path: Path to credentials.json. Defaults to "~/". - * gcalendar.locale: locale to use rather than the system default. - -Requires these pip packages: - * google-api-python-client >= 1.8.0 - * google-auth-httplib2 - * google-auth-oauthlib - getcrypto ~~~~~~~~~ @@ -960,8 +643,6 @@ Displays the unread GitHub notifications count for a GitHub user using the follo * https://developer.github.com/v3/activity/notifications/#notification-reasons -Uses `xdg-open` or `x-www-browser` to open web-pages. - Requires the following library: * requests @@ -976,29 +657,6 @@ contributed by: .. image:: ../screenshots/github.png -gitlab -~~~~~~ - -Displays the GitLab todo count: - - * https://docs.gitlab.com/ee/user/todos.html - * https://docs.gitlab.com/ee/api/todos.html - -Uses `xdg-open` or `x-www-browser` to open web-pages. - -Requires the following library: - * requests - -Errors: - if the GitLab todo query failed, the shown value is `n/a` - -Parameters: - * gitlab.token: GitLab personal access token, the token needs to have the "read_api" scope. - * gitlab.host: Host of the GitLab instance, default is "gitlab.com". - * gitlab.actions: Comma separated actions to be parsed (e.g.: gitlab.actions=assigned,approval_required) - -.. image:: ../screenshots/gitlab.png - gpmdp ~~~~~ @@ -1012,7 +670,7 @@ contributed by `TheEdgeOfRage `_ - many thanks hddtemp ~~~~~~~ -Fetch hard drive temperature data from a hddtemp daemon +Fetch hard drive temeperature data from a hddtemp daemon that runs on localhost and default port (7634) contributed by `somospocos `_ - many thanks! @@ -1043,9 +701,6 @@ indicator Displays the indicator status, for numlock, scrolllock and capslock -Requires the following executable: - * xset - Parameters: * indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock') * indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning') @@ -1063,17 +718,19 @@ contributed by `pierre87 `_ - many thanks! .. image:: ../screenshots/kernel.png -layout-xkbswitch -~~~~~~~~~~~~~~~~ +layout +~~~~~~ Displays and changes the current keyboard layout Requires the following executable: - * xkb-switch + * setxkbmap -contributed by `somospocos `_ - many thanks! +contributed by `Pseudonick47 `_ - many thanks! -layout_xkbswitch +.. image:: ../screenshots/layout.png + +layout-xkbswitch ~~~~~~~~~~~~~~~~ Displays and changes the current keyboard layout @@ -1093,25 +750,6 @@ Required the following python packages: contributed by `maxpivo `_ - many thanks! -messagereceiver -~~~~~~~~~~~~~~~ - -Displays the message that's received via unix socket. - -Parameters: - * messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock) - -Example: - The following examples assume that /tmp/bumblebee_messagereceiver.sock is used as unix socket address. - - In order to send the string "I  bumblebee-status" to your status bar, use the following command: - echo -e '{"message":"I  bumblebee-status", "state": ""}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO - - In order to highlight the text, the state variable can be used: - echo -e '{"message":"I  bumblebee-status", "state": "warning"}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO - -contributed by `bbernhard `_ - many thanks! - mocp ~~~~ @@ -1183,22 +821,12 @@ Parameters: if {file} = '/foo/bar.baz', then {file2} = 'bar' * mpd.host: MPD host to connect to. (mpc behaviour by default) - * mpd.port: MPD port to connect to. (mpc behaviour by default) * mpd.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles mpd.prev, mpd.next, mpd.shuffle and mpd.repeat, and the main display with play/pause function mpd.main. contributed by `alrayyes `_ - many thanks! .. image:: ../screenshots/mpd.png -network -~~~~~~~ - -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! - network_traffic ~~~~~~~~~~~~~~~ @@ -1231,26 +859,19 @@ Displays GPU name, temperature and memory usage. Parameters: * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB') - Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct} + Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} Requires nvidia-smi contributed by `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. - octoprint ~~~~~~~~~ -Displays the Octorrint status and the printer's bed/tools temperature in the status bar. +Displays the Octorpint status and the printer's bed/tools temperature in the status bar. Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled). -Prerequisites: - * tk python library (usually python-tk or python3-tk, depending on your distribution) - Parameters: * octoprint.address : Octoprint address (e.q: http://192.168.1.3) * octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface) @@ -1258,21 +879,13 @@ Parameters: contributed by `bbernhard `_ - many thanks! -optman -~~~~~~ - -Displays currently active gpu by optimus-manager -Requires the following packages: - - * optimus-manager - pacman ~~~~~~ Displays update information per repository for pacman. Parameters: - * pacman.sum: If you prefer displaying updates with a single digit (defaults to 'False') + * pacman.sum: If you prefere displaying updates with a single digit (defaults to 'False') Requires the following executables: * fakeroot @@ -1282,31 +895,6 @@ contributed by `Pseudonick47 `_ - many thanks! .. image:: ../screenshots/pacman.png -pamixer -~~~~~~~ - -get volume level or control it - -Requires the following executable: - * pamixer - -Parameters: - * pamixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) - -heavily based on amixer module - -persian_date -~~~~~~~~~~~~ - -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" - pihole ~~~~~~ @@ -1314,30 +902,10 @@ Displays the pi-hole status (up/down) together with the number of ads that were Parameters: * pihole.address : pi-hole address (e.q: http://192.168.1.3) - - - * pihole.apitoken : pi-hole API token (can be obtained in the pi-hole webinterface (Settings -> API) - - OR (deprecated!) - - * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) - + * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) contributed by `bbernhard `_ - many thanks! -pipewire -~~~~~~~~ - -get volume level or control it - -Requires the following executable: - * wpctl - -Parameters: - * wpctl.percent_change: How much to change volume by when scrolling on the module (default is 4%) - -heavily based on amixer module - playerctl ~~~~~~~~~ @@ -1346,17 +914,6 @@ Displays information about the current song in vlc, audacious, bmp, xmms2, spoti Requires the following executable: * playerctl -Parameters: - * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}'). - The format string is passed to 'playerctl -f' as an argument. Read `the README `_ 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 `_. For example, it could be '-p vlc,%any'. - * playerctl.hide: Hide the widgets when no players are found. Defaults to "false". - -Parameters are inspired by the `spotify` module, many thanks to its developers! - contributed by `smitajit `_ - many thanks! .. image:: ../screenshots/playerctl.png @@ -1377,20 +934,10 @@ Parameters: Example: 'notify-send 'Time up!''. If you want to chain multiple commands, please use an external wrapper script and invoke that. The module itself does not support command chaining (see https://github.com/tobi-wan-kenobi/bumblebee-status/issues/532 - for a detailed explanation) + for a detailled explanation) contributed by `martindoublem `_, inspired by `karthink `_ - many thanks! -portage_status -~~~~~~~~~~~~~~ - -Displays the status of Gentoo portage operations. - -Parameters: - * portage_status.logfile: logfile for portage (default is /var/log/emerge.log) - -contributed by `andrewreisner `_ - many thanks! - prime ~~~~~ @@ -1414,8 +961,7 @@ Parameters: * prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel') * prime.intelstring: String to use when intel is selected (defaults to 'intel') -Requires the following executables: - * sudo +Requires the following executable: * prime-select contributed by `jeffeb3 `_ - many thanks! @@ -1441,43 +987,7 @@ contributed by `remi-dupre `_ - many thanks! publicip ~~~~~~~~ -Displays information about the public IP address associated with the default route: - * Public IP address - * Country Name - * Country Code - * City Name - * Geographic Coordinates - -Left mouse click on the widget forces immediate update. -Any change to the default route will cause the widget to update. - -Requirements: - * netifaces - -Parameters: - * publicip.format: Format string (defaults to ‘{ip} ({country_code})’) - * Available format strings - ip, country_name, country_code, city_name, coordinates - -Examples: - * bumblebee-status -m publicip -p publicip.format="{ip} ({country_code})" - * bumblebee-status -m publicip -p publicip.format="{ip} which is in {city_name}" - * bumblebee-status -m publicip -p publicip.format="Your packets are right here: {coordinates}" - -contributed by `tfwiii ` - many thanks! - -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 - -Parameters: -* rofication.regolith: Switch to regolith fork of rofication, see . +Displays public IP address rotation ~~~~~~~~ @@ -1508,7 +1018,6 @@ sensors Displays sensor temperature Parameters: - * sensors.use_sensors: whether to use the sensors command * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output of 'sensors -j' (i.e. //.../), for example, path could @@ -1557,12 +1066,12 @@ Shows a widget per user-defined shortcut and allows to define the behaviour when clicking on it. For more than one shortcut, the commands and labels are strings separated by -a delimiter (; semicolon by default). +a demiliter (; semicolon by default). For example in order to create two shortcuts labeled A and B with commands cmdA and cmdB you could do: - ./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)' + ./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B' Parameters: * shortcut.cmds : List of commands to execute @@ -1579,25 +1088,11 @@ smartstatus Displays HDD smart status of different drives or all drives -Requires the following executables: - * sudo - * smartctl - Parameters: - * smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'separate' or 'singles') + * smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') * smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all. -solaar -~~~~~~ - -Shows status and load percentage of logitech's unifying device - -Requires the following executable: - * solaar (from community) - -contributed by `cambid `_ - many thanks! - spaceapi ~~~~~~~~ @@ -1607,6 +1102,7 @@ an example. Requires the following libraries: * requests + * regex Parameters: * spaceapi.url: String representation of the api endpoint @@ -1614,7 +1110,7 @@ Parameters: Format Strings: * Format strings are indicated by double %% - * They represent a leaf in the JSON tree, layers separated by '.' + * They represent a leaf in the JSON tree, layers seperated by '.' * Boolean values can be overwritten by appending '%true%false' in the format string * Example: to reference 'open' in '{'state':{'open': true}}' @@ -1627,39 +1123,37 @@ contributed by `rad4day `_ - many thanks! spotify ~~~~~~~ -Displays the current song being played and allows pausing, skipping ahead, and skipping back. +Displays the current song being played Requires the following library: * python-dbus Parameters: * spotify.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber} - * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) - Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next - * spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget. - Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous. - * spotify.bus_name: String (defaults to `spotify`) - Available values: spotify, spotifyd + Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} + * spotify.previous: Change binding for previous song (default is left click) + * spotify.next: Change binding for next song (default is right click) + * spotify.pause: Change binding for toggling pause (default is middle click) + + Available options for spotify.previous, spotify.next and spotify.pause are: + LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN + contributed by `yvesh `_ - many thanks! -added controls by `LtPeriwinkle `_ - many thanks! - -fixed icons and layout parameter by `gkeep `_ - many thanks! - .. image:: ../screenshots/spotify.png stock ~~~~~ -Display a stock quote from finance.yahoo.com +Display a stock quote from worldtradingdata.com + +Requires the following python packages: + * requests Parameters: * stock.symbols : Comma-separated list of symbols to fetch - * stock.apikey : API key created on https://alphavantage.co - * stock.url : URL to use, defaults to "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apikey}" - * stock.fields : Fields from the response to show, defaults to "01. symbol,05. price,10. change percent" + * stock.change : Should we fetch change in stock value (defaults to True) contributed by `msoulier `_ - many thanks! @@ -1674,11 +1168,10 @@ Displays sunrise and sunset times Requires the following python packages: * requests * suntime - * python-dateutil Parameters: - * sun.lat : Latitude of your location - * sun.lon : Longitude of your location + * cpu.lat : Latitude of your location + * cpu.lon : Longitude of your location (if none of those are set, location is determined automatically via location APIs) @@ -1694,11 +1187,11 @@ adds the possibility to * reboot the system. - + Per default a confirmation dialog is shown before the actual action is performed. - + Parameters: - * system.confirm: show confirmation dialog before performing any action (default: true) + * system.confirm: show confirmation dialog before performing any action (default: true) * system.reboot: specify a reboot command (defaults to 'reboot') * system.shutdown: specify a shutdown command (defaults to 'shutdown -h now') * system.logout: specify a logout command (defaults to 'i3exit logout') @@ -1707,9 +1200,6 @@ Parameters: * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') -Requirements: - tkinter (python3-tk package on debian based systems either you can install it as python package) - contributed by `bbernhard `_ - many thanks! taskwarrior @@ -1722,31 +1212,12 @@ Requires the following library: Parameters: * taskwarrior.taskrc : path to the taskrc file (defaults to ~/.taskrc) - * taskwarrior.show_active: true/false(default) to show the active task ID and description when one is active, otherwise show the total number pending. contributed by `chdorb `_ - many thanks! .. image:: ../screenshots/taskwarrior.png -thunderbird -~~~~~~~~~~~ - -Displays the unread emails count for one or more Thunderbird inboxes - -Parameters: - * thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird) - * thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf) - -Tips: - * You can run the following command in order to list all your Thunderbird inboxes - - find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}' - -contributed by `cristianmiranda `_ - many thanks! - -.. image:: ../screenshots/thunderbird.png - timetz ~~~~~~ @@ -1768,7 +1239,6 @@ Parameters: * title.max : Maximum character length for title before truncating. Defaults to 64. * title.placeholder : Placeholder text to be placed if title was truncated. Defaults to '...'. * title.scroll : Boolean flag for scrolling title. Defaults to False - * title.short : Boolean flag for short title. Defaults to False contributed by `UltimatePancake `_ - many thanks! @@ -1788,36 +1258,6 @@ contributed by `codingo `_ - many thanks! .. image:: ../screenshots/todo.png -todo_org -~~~~~~~~ - -Displays the number of todo items from an org-mode file -Parameters: - * todo_org.file: File to read TODOs from (defaults to ~/org/todo.org) - * todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed) -Based on the todo module by `codingo ` - -todoist -~~~~~~~ - -Displays the nº of Todoist tasks that are due: - - * https://developer.todoist.com/rest/v2/#get-active-tasks - -Uses `xdg-open` or `x-www-browser` to open web-pages. - -Requires the following library: - * requests - -Errors: - if the Todoist get active tasks query failed, the shown value is `n/a` - -Parameters: - * todoist.token: Todoist api token, you can get it in https://todoist.com/app/settings/integrations/developer. - * todoist.filter: a filter statement defined by Todoist (https://todoist.com/help/articles/introduction-to-filters), eg: "!assigned to: others & (Overdue | due: today)" - -.. image:: ../screenshots/todoist.png - traffic ~~~~~~~ @@ -1829,7 +1269,7 @@ Parameters: * traffic.showname: If set to False, hide network interface name (defaults to True) * traffic.format: Format string for download/upload speeds. Defaults to '{:.2f}' - * traffic.graphlen: Graph length in seconds. Positive even integer. Each + * traffic.graphlen: Graph lenth in seconds. Positive even integer. Each char shows 2 seconds. If set, enables up/down traffic graphs @@ -1842,9 +1282,6 @@ twmn Toggle twmn notifications. -Requires the following executable: - * systemctl - contributed by `Pseudonick47 `_ - many thanks! uptime @@ -1856,27 +1293,6 @@ contributed by `ccoors `_ - many thanks! .. image:: ../screenshots/uptime.png -usage -~~~~~ - -Module for ActivityWatch (https://activitywatch.net/) -Displays the amount of time the system was used actively. - -Requirements: - * sqlite3 module for python - * ActivityWatch - -Errors: - * when you get 'error: unable to open database file', modify the parameter 'database' to your ActivityWatch database file - -> often found by running 'locate aw-server/peewee-sqlite.v2.db' - -Parameters: - * usage.database: path to your database file - * usage.format: Specify what gets printed to the bar - -> use 'HH', 'MM' or 'SS', they will get replaced by the number of hours, minutes and seconds, respectively - -contributed by lasnikr (https://github.com/lasnikr) - vpn ~~~ @@ -1896,34 +1312,6 @@ Displays the VPN profile that is currently in use. contributed by `bbernhard `_ - many thanks! -wakatime -~~~~~~~~ - -Displays the WakaTime daily/weekly/monthly times: - - * https://wakatime.com/developers#stats - -Uses `xdg-open` or `x-www-browser` to open web-pages. - -Requires the following library: - * requests - -Errors: - if the Wakatime status query failed, the shown value is `n/a` - -Parameters: - * wakatime.token: Wakatime secret api key, you can get it in https://wakatime.com/settings/account. - * wakatime.range: Range of the output, default is "Today". Can be one of “Today”, “Yesterday”, “Last 7 Days”, “Last 7 Days from Yesterday”, “Last 14 Days”, “Last 30 Days”, “This Week”, “Last Week”, “This Month”, or “Last Month”. - * wakatime.format: Format of the output, default is "digital" - Valid inputs are: - * "decimal" -> 1.37 - * "digital" -> 1:22 - * "seconds" -> 4931.29 - * "text" -> 1 hr 22 mins - * "%H:%M:%S" -> 01:22:31 (or any other valid format) - -.. image:: ../screenshots/wakatime.png - watson ~~~~~~ @@ -1932,10 +1320,6 @@ Displays the status of watson (time-tracking tool) Requires the following executable: * watson -Parameters: - * watson.format: Output format, defaults to "{project} [{tags}]" - Supported fields are: {project}, {tags}, {relative_start}, {absolute_start} - contributed by `bendardenne `_ - many thanks! weather @@ -1953,7 +1337,7 @@ Parameters: * weather.unit: metric (default), kelvin, imperial * weather.showcity: If set to true, show location information, otherwise hide it (defaults to true) * weather.showminmax: If set to true, show the minimum and maximum temperature, otherwise hide it (defaults to false) - * weather.apikey: API key from https://api.openweathermap.org + * weather.apikey: API key from http://api.openweathermap.org contributed by `TheEdgeOfRage `_ - many thanks! @@ -1985,9 +1369,6 @@ zpool Displays info about zpools present on the system -Requires the following executable: - * sudo (if `zpool.sudo` is explicitly set to `true`) - Parameters: * zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools is displayed. (Default: '') diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 93120e6..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -docutils<0.18 diff --git a/docs/themes.rst b/docs/themes.rst index 16f69a6..759ea86 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -97,8 +97,3 @@ List of available themes :alt: Default Default (nothing or -t default) - -.. figure:: ../screenshots/themes/moonlight-powerline.png - :alt: Moonlight Powerline - - Moonlight Powerline (-t moonlight-powerline) (contributed by `Ramon Saraiva `__) diff --git a/generate-base-tests.py b/generate-base-tests.py deleted file mode 100755 index 3f94905..0000000 --- a/generate-base-tests.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import sys -import glob - - -def is_psl(module): - lib_path = os.path.dirname(os.__file__) - old_sys = sys.path - sys.path = [lib_path] - is_psl = True - try: - __import__(module) - except Exception as e: - is_psl = False - sys.path = old_sys - - return is_psl - - -def is_internal(module): - if module.startswith("core.") or module == "core": - return True - if module.startswith("util.") or module == "util": - return True - if module.startswith("bumblebee_status."): - return True - if module.startswith("."): - return True - - return is_psl(module) - - -def dependencies(filename): - deps = [] - with open(filename) as f: - for line in f: - if "import" in line: - match = re.match("\s*(from (\S+) )?import (\S+)", line) - if not match: - continue - dep = match.group(2) or match.group(3) - if "util.popup" in dep or ("util" in line and "popup" in line): - deps.append("tkinter") - if ".datetimetz" in line: - deps.extend( - dependencies("bumblebee_status/modules/contrib/datetimetz.py") - ) - elif not is_internal(dep): - deps.append(dep) - return deps - - -def write_test(testname, modname, deps): - fqmn = ".".join(["modules", testname.split(os.sep)[2], modname]) - if not os.path.exists(testname): - with open(testname, "w") as f: - f.writelines( - ["import pytest\n\n",] - ) - for dep in deps: - f.write('pytest.importorskip("{}")\n\n'.format(dep)) - - with open(testname) as f: - for line in f: - if "def test_load_module(" in line: - print("skipping {}, already contains test".format(modname)) - return - - print("writing base test for {}".format(modname)) - with open(testname, "a+") as f: - f.writelines( - ["def test_load_module():\n", ' __import__("{}")\n\n'.format(fqmn),] - ) - - -def main(): - for f in glob.glob("bumblebee_status/modules/*/*.py"): - if os.path.basename(f) == "__init__.py": - continue - - modname = os.path.splitext(os.path.basename(f))[0] - - modpath = os.path.dirname(f) - deps = dependencies(f) - testname = os.path.join( - "tests", "modules", modpath.split(os.sep)[2], "test_{}.py".format(modname) - ) - - write_test(testname, modname, deps) - - -if __name__ == "__main__": - main() - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/man/bumblebee-ctl.1 b/man/bumblebee-ctl.1 deleted file mode 100644 index 33c50aa..0000000 --- a/man/bumblebee-ctl.1 +++ /dev/null @@ -1,29 +0,0 @@ -.TH BUMBLEBEE-CTL "1" "June 2022" "bumblebee-status" -.SH NAME -bumblebee-ctl \- Send commands to bumblebee-status -.SH SYNOPSIS -.B bumblebee-ctl -[\fB\-h\fR] [\fB\-b\fR \fIbutton\fR] [\fB\-i\fR \fIID\fR] \fB-m\fR \fImodule\fR -.SH DESCRIPTION -.B bumblebee-ctl -can be used to send commands to bumblebee-status. -.SH OPTIONS -.TP -\fB\-h\fR, \fB\-\-help\fR -show this help message and exit -.TP -\fB\-b\fR, \fB\-\-button\fR