pax_global_header00006660000000000000000000000064145025626050014517gustar00rootroot0000000000000052 comment=fcda13cac05cd6b7245debf99573f950e6007740 bumblebee-status-2.2.0/000077500000000000000000000000001450256260500147635ustar00rootroot00000000000000bumblebee-status-2.2.0/.codeclimate.yml000066400000000000000000000004601450256260500200350ustar00rootroot00000000000000version: "2" plugins: duplication: enabled: true config: languages: python: fixme: enabled: true radon: enabled: true config: python_version: 3 threshold: "D" exclude_patterns: - "tests/" - "versioneer.py" - "bumblebee_status/_version.py" - "setup.py" bumblebee-status-2.2.0/.coveragerc000066400000000000000000000002141450256260500171010ustar00rootroot00000000000000[run] omit = versioneer.* bumblebee_status/_version.* setup.* tests/* pytests/* *mock* *funcsigs* *pbr* *six* /usr/lib* [report] bumblebee-status-2.2.0/.github/000077500000000000000000000000001450256260500163235ustar00rootroot00000000000000bumblebee-status-2.2.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000007421450256260500210330ustar00rootroot00000000000000Affected module: Description: If you are having problems with fonts, please read: https://github.com/tobi-wan-kenobi/bumblebee-status/issues/228 https://github.com/tobi-wan-kenobi/bumblebee-status/issues/210 https://github.com/tobi-wan-kenobi/bumblebee-status/issues/197 https://github.com/tobi-wan-kenobi/bumblebee-status/issues/233 Please note FontAwesome 5 is currently not supported: https://github.com/tobi-wan-kenobi/bumblebee-status/issues/239 bumblebee-status-2.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001450256260500205065ustar00rootroot00000000000000bumblebee-status-2.2.0/.github/ISSUE_TEMPLATE/Bug.md000066400000000000000000000014011450256260500215410ustar00rootroot00000000000000--- name: Bug Report about: Something doesn't work as expected title: '' labels: '' assignees: '' --- ### Bug Report #### Description Affected module: Version used: #### How to reproduce bumblebee-status-2.2.0/.github/ISSUE_TEMPLATE/Feature.md000066400000000000000000000003331450256260500224220ustar00rootroot00000000000000--- name: Feature Request about: You have a neat idea that should be implemented? title: '' labels: '' assignees: '' --- ### Feature Request bumblebee-status-2.2.0/.github/PULL_REQUEST_TEMPLATE/000077500000000000000000000000001450256260500216025ustar00rootroot00000000000000bumblebee-status-2.2.0/.github/PULL_REQUEST_TEMPLATE/Improvement.md000066400000000000000000000002751450256260500244350ustar00rootroot00000000000000--- name: Improvement about: You have some improvement to make bumblebee-status bar better? --- ### Improvement bumblebee-status-2.2.0/.github/PULL_REQUEST_TEMPLATE/Other.md000066400000000000000000000013241450256260500232050ustar00rootroot00000000000000--- name: Other about: You have some other ideas you want to introduce? --- ### Description **What kind of change does this PR introduce?** **Summary** **Does this PR introduce a breaking change?** **Other information** bumblebee-status-2.2.0/.github/workflows/000077500000000000000000000000001450256260500203605ustar00rootroot00000000000000bumblebee-status-2.2.0/.github/workflows/aurpublish.yml000066400000000000000000000014611450256260500232630ustar00rootroot00000000000000--- 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 bumblebee-status-2.2.0/.github/workflows/autotest.yml000066400000000000000000000027331450256260500227600ustar00rootroot00000000000000name: 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 | 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 bumblebee-status-2.2.0/.github/workflows/codeql-analysis.yml000066400000000000000000000044351450256260500242010ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/.github/workflows/pythonpublish.yml000066400000000000000000000012171450256260500240140ustar00rootroot00000000000000--- name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* bumblebee-status-2.2.0/.gitignore000066400000000000000000000021661450256260500167600ustar00rootroot00000000000000*.o # Vim swap files *swp *~ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg build/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # Visual studio project files .vscode/ # mypy cache .mypy_cache bumblebee-status-2.2.0/.readthedocs.yaml000066400000000000000000000001771450256260500202170ustar00rootroot00000000000000version: 2 python: install: - requirements: docs/requirements.txt build: os: ubuntu-22.04 tools: python: "3.11" bumblebee-status-2.2.0/CODE_OF_CONDUCT.md000066400000000000000000000062261450256260500175700ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at github@tobi-wan-kenobi.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ bumblebee-status-2.2.0/CONTRIBUTING.md000066400000000000000000000023371450256260500172210ustar00rootroot00000000000000Most importantly: Many thanks for considering contributing to bumblebee-status! I am continuously amazed by the quality and creativity of the Pull Requests I get - many thanks! One thing I need to mention: This is a project I am working on in my (limited) spare time. I try very hard to answer bug tickets and review Pull Requests as quickly as possible, but it might take days, in some cases even weeks, until I get around to doing so. I want to give every contribution the attention it deserves. Really: I am not ignoring you, I'm simply slow :-) ### Filing a bug If you want to file a bug, simply open an issue and describe your problem. Things that help narrow down the problem are: - Steps to reproduce - Relevant section of the i3 configuration - Debug logs and console output of bumblebee-status But even if you can't provide those, any indicator that something is not working as it should is much appreciated! ### 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. Thanks for reading until here! :) bumblebee-status-2.2.0/LICENSE000066400000000000000000000020601450256260500157660ustar00rootroot00000000000000MIT License Copyright (c) 2016 tobi-wan-kenobi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bumblebee-status-2.2.0/MANIFEST.in000066400000000000000000000002311450256260500165150ustar00rootroot00000000000000include versioneer.py include bumblebee_status/_version.py include requirements/* include requirements/modules/* include themes/* include themes/icons/* bumblebee-status-2.2.0/PKGBUILD.template000066400000000000000000000040021450256260500177150ustar00rootroot00000000000000# 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 } bumblebee-status-2.2.0/README.md000066400000000000000000000145251450256260500162510ustar00rootroot00000000000000bumblebee-status ===================================================== logo courtesy of [kellya](https://github.com/kellya) - thank you! [![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) [![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.** [Click here for a list of available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) ![Solarized Powerline](screenshots/themes/powerline-solarized.png) bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). Focus is on: * ease of use, sane defaults (no mandatory configuration file) * [easy creation of custom themes](https://bumblebee-status.readthedocs.io/en/main/development/theme.html) * [easy creation of custom modules](https://bumblebee-status.readthedocs.io/en/main/development/module.html) I hope you like it and I appreciate any kind of feedback: bug reports, feature requests, etc. :) 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 FontAwesome version: 4 (free version of 5 doesn't include some of the icons) --- ***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/) --- Example usage: ``` bar { status_command /bumblebee-status -m cpu memory battery time \ pasink pasource -p time.format="%H:%M" -t solarized } ``` # Documentation See [the docs](https://bumblebee-status.readthedocs.io) for detailed documentation. See [FAQ](https://bumblebee-status.readthedocs.io/en/main/FAQ.html) for. well, FAQs. Other resources: * A list of [available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) * [How to write a module](https://bumblebee-status.readthedocs.io/en/main/development/module.html) * [How to write a theme](https://bumblebee-status.readthedocs.io/en/main/development/theme.html) # Installation ``` # from git (development snapshot) $ git clone git://github.com/tobi-wan-kenobi/bumblebee-status # from AUR: git clone https://aur.archlinux.org/bumblebee-status.git cd bumblebee-status makepkg -sicr # from PyPI (thanks @tony): # 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)! 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: ```bash bar { status_command \ -m \ -p \ -t } ``` You can retrieve a list of modules (and their parameters) and themes by entering: ```bash $ cd bumblebee-status $ ./bumblebee-status -l themes $ ./bumblebee-status -l modules ``` To change the update interval, use: ```bash $ ./bumblebee-status -m -p interval= ``` The update interval can also be changed on a per-module basis, like this: ```bash $ ./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: ```bash $ ./bumblebee-status -m disk:root disk:home -p root.path=/ home.path=/home ``` As a simple example, this is what my i3 configuration looks like: ```bash bar { font pango:Inconsolata 10 position top tray_output none status_command ~/.i3/bumblebee-status/bumblebee-status -m nic disk:root cpu \ memory battery date time pasink pasource dnf \ -p root.path=/ time.format="%H:%M CW %V" date.format="%a, %b %d %Y" \ -t solarized-powerline } ``` Restart i3wm and - that's it! # Examples [List of themes](https://bumblebee-status.readthedocs.io/en/main/themes.html) bumblebee-status-2.2.0/bin/000077500000000000000000000000001450256260500155335ustar00rootroot00000000000000bumblebee-status-2.2.0/bin/load-i3-bars.sh000077500000000000000000000014621450256260500202520ustar00rootroot00000000000000#!/usr/bin/env bash if [ ! -f ~/.config/i3/config.template ]; then cp ~/.config/i3/config ~/.config/i3/config.template else cp ~/.config/i3/config.template ~/.config/i3/config fi if [ -f ~/.config/i3/config.template.private ]; then cat ~/.config/i3/config.template.private >> ~/.config/i3/config fi screens=$(xrandr -q|grep ' connected'| grep -P '\d+x\d+' |cut -d' ' -f1) echo "screens: $screens" while read -r line; do screen=$(echo $line | cut -d' ' -f1) others=$(echo $screens|tr ' ' '\n'|grep -v $screen|tr '\n' '-'|sed 's/.$//') if [ -f ~/.config/i3/config.$screen-$others ]; then cat ~/.config/i3/config.$screen-$others >> ~/.config/i3/config else if [ -f ~/.config/i3/config.$screen ]; then cat ~/.config/i3/config.$screen >> ~/.config/i3/config fi fi done <<< "$screens" i3-msg restart bumblebee-status-2.2.0/bin/pacman-updates000077500000000000000000000011241450256260500203610ustar00rootroot00000000000000#!/usr/bin/bash if ! type -P fakeroot >/dev/null; then error 'Cannot find the fakeroot binary.' exit 1 fi if [[ -z $CHECKUPDATES_DB ]]; then CHECKUPDATES_DB="${TMPDIR:-/tmp}/checkup-db-${USER}/" fi trap 'rm -f $CHECKUPDATES_DB/db.lck' INT TERM EXIT DBPath="${DBPath:-/var/lib/pacman/}" eval $(awk -F' *= *' '$1 ~ /DBPath/ { print $1 "=" $2 }' /etc/pacman.conf) mkdir -p "$CHECKUPDATES_DB" ln -s "${DBPath}/local" "$CHECKUPDATES_DB" &> /dev/null fakeroot -- pacman -Sy --dbpath "$CHECKUPDATES_DB" --logfile /dev/null &> /dev/null fakeroot pacman -Su -p --dbpath "$CHECKUPDATES_DB" exit 0 bumblebee-status-2.2.0/bin/toggle-display.sh000077500000000000000000000004501450256260500210150ustar00rootroot00000000000000#!/usr/bin/env bash echo $(dirname $(readlink -f "$0")) i3bar_update=$(dirname $(readlink -f "$0"))/load-i3-bars.sh xrandr "$@" if [ -f $i3bar_update ]; then sleep 1 if [ -f ~/.config/i3/images/background.png ]; then feh --bg-fill ~/.config/i3/images/background.png fi $i3bar_update fi bumblebee-status-2.2.0/bumblebee-ctl000077500000000000000000000025141450256260500174150ustar00rootroot00000000000000#!/usr/bin/env python import os import argparse import json import glob import socket button = { "left-mouse": 1, "middle-mouse": 2, "right-mouse": 3, "wheel-up": 4, "wheel-down": 5, "update": -1, } def main(): parser = argparse.ArgumentParser(description="send commands to bumblebee-status") parser.add_argument( "-b", "--button", choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down", "update"], help="button to emulate", default="left-mouse", ) parser.add_argument("-i", "--id", help="ID of widget to trigger") parser.add_argument( "-m", "--module", help="name of the module to trigger", required=True ) args = parser.parse_args() for f in glob.glob("/tmp/.bumblebee-status.*"): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: s.connect(f) s.sendall( json.dumps( { "name": args.module, "instance": args.id, "button": button[args.button], } ).encode("ascii") ) except Exception as e: os.remove(f) if __name__ == "__main__": main() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee-status000077500000000000000000000122111450256260500201510ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys import json import time import signal import socket import logging import threading import bumblebee_status.discover bumblebee_status.discover.discover() import core.config import core.output import core.module import core.input import core.event import util.format started = False class CommandSocket(object): def __init__(self): self.__name = "/tmp/.bumblebee-status.{}".format(os.getpid()) self.__socket = None def __enter__(self): self.__socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.__socket.bind(self.__name) self.__socket.listen(5) return self.__socket def __exit__(self, type, value, traceback): 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): with CommandSocket() as cmdsocket: while True: tmp, _ = cmdsocket.accept() line = tmp.recv(4096).decode() tmp.close() logging.debug("socket event {}".format(line)) process_event(line, config, update_lock) def handle_events(config, update_lock): while True: 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) def main(): global started config = core.config.Config(sys.argv[1:]) level = logging.DEBUG if config.debug() else logging.ERROR if config.logfile(): logging.basicConfig( level=level, format="[%(asctime)s] %(module)-16s %(levelname)-8s %(message)s", filename=os.path.abspath(os.path.expanduser(config.logfile())), ) else: logging.basicConfig( level=level, format="[%(asctime)s] %(module)-16s %(levelname)-8s %(message)s", stream=sys.stderr, ) theme = core.theme.Theme(config.theme(), config.iconset()) output = core.output.i3(theme, config) modules = [] 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() if config.debug(): modules.append(core.module.load("debug", config, theme)) for module in config.modules(): modules.append(core.module.load(module, config, theme)) modules[-1].register_callbacks() if config.reverse(): 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) while True: if update_lock.acquire(blocking=False) == True: core.event.trigger("update") core.event.trigger("draw") update_lock.release() output.wait(config.interval()) core.event.trigger("stop") if __name__ == "__main__": try: if sys.version_info.major < 3: raise Exception("at least Python 3.4 required (Python 2.x not supported)") 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("[") while True: sys.stdout.write( json.dumps( [ { "full_text": " {} ".format(e), "background": "#ff0000", "color": "#ffffff", "name": "error", "instance": "the-only-one", } ] ) ) sys.stdout.write(",\n") sys.stdout.flush() time.sleep(5) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/000077500000000000000000000000001450256260500203105ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/__init__.py000066400000000000000000000001071450256260500224170ustar00rootroot00000000000000import bumblebee_status.discover bumblebee_status.discover.discover() bumblebee-status-2.2.0/bumblebee_status/_version.py000066400000000000000000000440521450256260500225130ustar00rootroot00000000000000# This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. Generated by # versioneer-0.18 (https://github.com/warner/python-versioneer) """Git implementation of _version.py.""" import errno import os import re import subprocess import sys def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = "$Format:%d$" git_full = "$Format:%H$" git_date = "$Format:%ci$" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440" cfg.tag_prefix = "" cfg.parentdir_prefix = "bumblebee" cfg.versionfile_source = "bumblebee/_version.py" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY = {} HANDLERS = {} def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen( [c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), ) break except EnvironmentError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, p.returncode return stdout, p.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { "version": dirname[len(parentdir_prefix) :], "full-revisionid": None, "dirty": False, "error": None, "date": None, } else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print( "Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix) ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") date = keywords.get("date") if date is not None: # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r"\d", r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix) :] if verbose: print("picking %s" % r) return { "version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date, } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return { "version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None, } @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = run_command( GITS, [ "describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix, ], cwd=root, ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( full_tag, tag_prefix, ) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ 0 ].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%d" % pieces["distance"] else: # exception #1 rendered = "0.post.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return { "version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None, } if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return { "version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date"), } def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for i in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: return { "version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None, } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return { "version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None, } bumblebee-status-2.2.0/bumblebee_status/core/000077500000000000000000000000001450256260500212405ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/core/__init__.py000066400000000000000000000000001450256260500233370ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/core/config.py000066400000000000000000000260341450256260500230640ustar00rootroot00000000000000import os import ast from configparser import RawConfigParser import sys import glob import textwrap import argparse import logging import core.theme import util.store import util.format import modules.core import modules.contrib log = logging.getLogger(__name__) MODULE_HELP = "Specify a space-separated list of modules to load. The order of the list determines their order in the i3bar (from left to right). Use : to provide an alias in case you want to load the same module multiple times, but specify different parameters." PARAMETER_HELP = ( "Provide configuration parameters in the form of .=" ) THEME_HELP = "Specify the theme to use for drawing modules" def all_modules(): """Returns a list of all available modules (either core or contrib) :return: list of modules :rtype: list of strings """ result = {} for path in [modules.core.__file__, modules.contrib.__file__]: path = os.path.dirname(path) for mod in glob.iglob("{}/*.py".format(path)): result[os.path.basename(mod).replace(".py", "")] = 1 res = list(result.keys()) res.sort() return res class print_usage(argparse.Action): def __init__(self, option_strings, dest, nargs=None, **kwargs): argparse.Action.__init__(self, option_strings, dest, nargs, **kwargs) self._indent = " " * 2 def __call__(self, parser, namespace, value, option_string=None): if value == "modules": self._args = namespace self._format = "plain" self.print_modules() elif value == "modules-rst": self._args = namespace self._format = "rst" self.print_modules() elif value == "themes": self.print_themes() sys.exit(0) def print_themes(self): print(", ".join(core.theme.themes())) def print_modules(self): basepath = os.path.abspath( os.path.join(os.path.dirname(os.path.realpath(__file__)), "..") ) 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" filename = os.path.join(basepath, "modules", "core", "{}.py".format(m)) if not os.path.exists(filename): filename = os.path.join( basepath, "modules", "contrib", "{}.py".format(m) ) module_type = "contrib" if not os.path.exists(filename): log.warning("module {} not found".format(m)) continue doc = None with open(filename) as f: tree = ast.parse(f.read()) doc = ast.get_docstring(tree) if not doc: log.warning("failed to find docstring for {}".format(m)) continue if self._format == "rst": if os.path.exists( os.path.join(basepath, "..", "screenshots", "{}.png".format(m)) ): doc = "{}\n\n.. image:: ../screenshots/{}.png".format(doc, m) rst[module_type] = rst.get(module_type, []) rst[module_type].append({"module": m, "content": doc}) else: print( textwrap.fill( "{}:".format(m), 80, initial_indent=self._indent * 2, subsequent_indent=self._indent * 2, ) ) for line in doc.split("\n"): print( textwrap.fill( line, 80, initial_indent=self._indent * 3, subsequent_indent=self._indent * 6, ) ) except Exception as e: log.warning(e) if self._format == "rst": print("List of modules\n===============") for k in ["core", "contrib"]: print("\n{}\n{}\n".format(k, "-" * len(k))) for mod in rst[k]: print("\n{}\n{}\n".format(mod["module"], "~" * len(mod["module"]))) print(mod["content"]) class Config(util.store.Store): """Represents the configuration of bumblebee-status (either via config file or via CLI) :param args: The arguments passed via the commandline """ def __init__(self, args): super(Config, self).__init__() 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 ) parser.add_argument( "-p", "--parameters", nargs="+", action="append", default=[], help=PARAMETER_HELP, ) parser.add_argument("-t", "--theme", default=None, help=THEME_HELP) parser.add_argument( "-i", "--iconset", default="auto", help="Specify the name of an iconset to use (overrides theme default)", ) parser.add_argument( "-a", "--autohide", nargs="+", 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" ) parser.add_argument( "-f", "--logfile", help="destination for the debug log file, if -d|--debug is specified; defaults to stderr", ) parser.add_argument( "-r", "--right-to-left", action="store_true", help="Draw widgets from right to left, rather than left to right (which is the default)", ) parser.add_argument( "-l", "--list", choices=["modules", "themes", "modules-rst"], help="Display a list of available themes or available modules, along with their parameters", action=print_usage, ) self.__args = parser.parse_args(args) if self.__args.config_file: cfg = self.__args.config_file cfg = os.path.expanduser(cfg) self.load_config(cfg) else: for cfg in [ "~/.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: if not "=" in param: log.error( 'missing value for parameter "{}" - ignoring this parameter'.format( param ) ) continue key, value = param.split("=", 1) self.set(key, value) """Loads parameters from an init-style configuration file :param filename: path to the file to load """ def load_config(self, filename, content=None): if os.path.exists(filename) or content != None: log.info("loading {}".format(filename)) tmp = RawConfigParser() tmp.optionxform = str if content: tmp.read_string(content) else: 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 :return: list of configured (active) modules :rtype: list of strings """ 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 """Returns the global update interval :return: update interval in seconds :rtype: float """ 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 :rtype: boolean """ def debug(self): return self.__args.debug """Returns whether module order should be reversed/inverted :return: True if modules should be reversed, False otherwise :rtype: boolean """ def reverse(self): return self.__args.right_to_left """Returns the logfile location :return: location where the logfile should be written :rtype: string """ def logfile(self): return self.__args.logfile """Returns the configured theme name :return: name of the configured theme :rtype: string """ def theme(self): return self.__args.theme or self.get("theme") or "default" """Returns the configured iconset name :return: name of the configured iconset :rtype: string """ def iconset(self): return self.__args.iconset """Returns whether a module should be hidden if their state is not warning/critical :return: True if module should be hidden automatically, False otherwise :rtype: bool """ def autohide(self, name): return name in self.__args.autohide or name in util.format.aslist(self.get("autohide", [])) """Returns which modules should be hidden if they are in state error :return: returns True if name should be hidden, False otherwise :rtype: bool """ def errorhide(self, name): return name in self.__args.errorhide # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/decorators.py000066400000000000000000000066631450256260500237720ustar00rootroot00000000000000import difflib import logging import util.format log = logging.getLogger(__name__) """Specifies that a module should never update (i.e. has static content). This means that its update() method will never be invoked :param init: The __init__() method of the module :return: Wrapped method that sets the module's interval to "never" """ def never(init): def call_init(obj, *args, **kwargs): init(obj, *args, **kwargs) if obj.parameter("interval") is None: obj.set("interval", "never") return call_init """Specifies the interval for executing the module's update() method :param hours: Hours between two update() invocations, defaults to 0 :param minutes: Minutes between two update() invocations, defaults to 0 :param seconds: Seconds between two update() invocations, defaults to 0 :return: Wrapped method that sets the module's interval correspondingly """ def every(hours=0, minutes=0, seconds=0): def decorator_init(init): def call_init(obj, *args, **kwargs): init(obj, *args, **kwargs) if obj.parameter("interval") is None: obj.set("interval", hours * 3600 + minutes * 60 + seconds) return call_init return decorator_init """Specifies that the module's content should scroll, if required The exact behaviour of this method is governed by a number of parameters, specifically: The module's parameter "scrolling.width" specifies the width when scrolling starts, "scrolling.makewide" defines whether the module should be expanded to "scrolling.width" automatically, if the content is shorter, the parameter "scrolling.bounce" defines whether it scrolls like a marquee (False) or should bounce when the end of the content is reached. "scrolling.speed" defines the number of characters to scroll each iteration. :param func: Function for which the result should be scrolled """ def scrollable(func): def wrapper(module, widget): text = func(module, widget) if not text: return text if ( difflib.SequenceMatcher(a=text, b=widget.get("__content__", text)).ratio() < 0.9 ): widget.set("scrolling.start", 0) widget.set("scrolling.direction", "right") widget.set("__content__", text) width = util.format.asint(module.parameter("scrolling.width", 30)) if util.format.asbool(module.parameter("scrolling.makewide", True)): widget.set("theme.minwidth", "A" * width) if width < 0 or len(text) <= width: return text start = widget.get("scrolling.start", 0) bounce = util.format.asbool(module.parameter("scrolling.bounce", True)) scroll_speed = util.format.asint(module.parameter("scrolling.speed", 1)) direction = widget.get("scrolling.direction", "right") if direction == "left": if start - scroll_speed < 0: # bounce back widget.set("scrolling.direction", "right") else: scroll_speed = -scroll_speed next_start = start + scroll_speed if next_start + width > len(text): if not bounce: next_start = 0 else: next_start = start - scroll_speed widget.set("scrolling.direction", "left") widget.set("scrolling.start", next_start) return text[start : start + width] return wrapper # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/event.py000066400000000000000000000014651450256260500227410ustar00rootroot00000000000000__callbacks = {} def register(event, callback, *args, **kwargs): cb = callback if args or kwargs: cb = lambda: 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() def trigger(event, *args, **kwargs): cb = __callbacks.get(event, []) if len(cb) == 0: return False for callback in cb: if args or kwargs: callback(*args, **kwargs) else: callback() return True # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/input.py000066400000000000000000000040631450256260500227540ustar00rootroot00000000000000import uuid import logging import core.event import util.cli LEFT_MOUSE = 1 MIDDLE_MOUSE = 2 RIGHT_MOUSE = 3 WHEEL_UP = 4 WHEEL_DOWN = 5 UPDATE = -1 def button_name(button): if button == LEFT_MOUSE: return "left-mouse" if button == RIGHT_MOUSE: return "right-mouse" if button == MIDDLE_MOUSE: return "middle-mouse" if button == WHEEL_UP: return "wheel-up" if button == WHEEL_DOWN: return "wheel-down" if button == UPDATE: return "update" return "n/a" class Object(object): def __init__(self): super(Object, self).__init__() self.id = str(uuid.uuid4()) def __event_id(obj_id, button): return "{}::{}".format(obj_id, button_name(button)) def __execute(event, cmd, wait=False): try: util.cli.execute( cmd.format(instance=event.get("instance", ""), name=event.get("name", ""),), wait=wait, shell=True, ) except Exception as e: logging.error("failed to invoke callback: {}".format(e)) 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)) else: core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait)) def trigger(event): if not "button" in event: return triggered = False for field in ["instance", "name"]: if not field in event: continue if core.event.trigger(__event_id(event[field], event["button"]), event): triggered = True if not triggered: core.event.trigger(__event_id("", event["button"]), event) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/module.py000066400000000000000000000253161450256260500231060ustar00rootroot00000000000000import os import importlib import importlib.util import logging import threading import core.config import core.input import core.widget import core.decorators import util.format try: error = ModuleNotFoundError("") except Exception as e: ModuleNotFoundError = Exception 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 :param module_name: Name of the module to load :param config: Configuration to apply to the module (defaults to an empty configuration) :param theme: Theme for this module, defaults to None, which means whatever is configured in "config" :return: A module object representing the module, or an Error module if loading failed :rtype: class bumblebee_status.module.Module """ 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: 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)) return getattr(mod, "Module")(config, theme) except ImportError as e: try: log.warning("failed to import {} from system: {}".format(module_short, e)) return import_user(module_short, config, theme) except ImportError as e: log.fatal("import failed: {}".format(e)) log.fatal("failed to import {}".format(module_short)) return Error(config=config, module=module_name, error="unable to load module") class Module(core.input.Object): """Represents a module (single piece of functionality) of the bar :param config: Configuration to apply to the module (defaults to an empty configuration) :param theme: Theme for this module, defaults to None, which means whatever is configured in "config" :param widgets: A list of bumblebee_status.widget.Widget objects that the module is comprised of """ def __init__(self, config=core.config.Config([]), theme=None, widgets=[]): super().__init__() self.background = False self.__thread = None self.__config = config self.__widgets = widgets if isinstance(widgets, list) else [widgets] self.module_name = self.__module__.split(".")[-1] self.name = self.module_name 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 for widget in self.__widgets: widget.module = self """Override this to determine when to show this module :return: True if the module should be hidden, False otherwise :rtype: boolean """ 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. :param key: Name of the parameter to retrieve :param default: Default value, if parameter is not set by user (defaults to None) :return: Parameter value, or default :rtype: string """ def parameter(self, key, default=None): value = default 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 :param key: Name of the parameter to set :param value: New value of the parameter """ def set(self, key, value): self.__config.set("{}.{}".format(self.name, key), value) """Override this method to define tasks that should be done during each update interval (for instance, querying an API, calling a CLI tool to get new date, etc. """ def update(self): pass def update_wrapper(self): if self.background == True: if self.__thread and self.__thread.is_alive(): return # skip this update interval self.__thread = threading.Thread(target=self.internal_update, args=(True,)) self.__thread.start() else: self.internal_update(False) """Wrapper method that ensures that all exceptions thrown by the update() method are caught and displayed in a bumblebee_status.module.Error module """ def internal_update(self, trigger_redraw=False): try: self.update() if trigger_redraw: core.event.trigger("update", [self.id], redraw_only=True) except Exception as e: self.set("interval", 1) module = Error(config=self.__config, module="error", error=str(e)) self.__widgets = [module.widget()] self.update = module.update """Retrieves the list of widgets for this module :return: A list of widgets :rtype: list of bumblebee_status.widget.Widgets """ def widgets(self): return self.__widgets """Removes all widgets of this module""" def clear_widgets(self): del self.__widgets[:] """Adds a widget to the module :param full_text: Text or callable (method) that defines the text of the widget (defaults to "") :param name: Name of the widget, defaults to None, which means autogenerate :return: The new widget :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) self.widgets().append(widget) widget.module = self return widget """Convenience method to retrieve a named widget :param name: Name of widget to retrieve, defaults to None (in which case the first widget is returned) :return: The widget with the corresponding name, None if not found :rtype: bumblebee_status.widget.Widget """ def widget(self, name=None, widget_id=None): if not name and not widget_id: return self.widgets()[0] for w in self.widgets(): if name and w.name == name: return w if w.id == widget_id: return w return None """Override this method to define states for the module :param widget: Widget for which state should be returned :return: a list of states for this widget :rtype: list of strings """ def state(self, widget): return [] """Convenience method that sets warning and critical state for numbers :param value: Current value to calculate state against :param warn: Warning threshold :parm crit: Critical threshold :return: None if value is below both thresholds, "critical", "warning" as appropriate otherwise :rtype: string """ def threshold_state(self, value, warn, crit): if value > float(self.parameter("critical", crit)): return "critical" if value > float(self.parameter("warning", warn)): return "warning" return None def register_callbacks(self): actions = [ {"name": "left-click", "id": core.input.LEFT_MOUSE}, {"name": "right-click", "id": core.input.RIGHT_MOUSE}, {"name": "middle-click", "id": core.input.MIDDLE_MOUSE}, {"name": "wheel-up", "id": core.input.WHEEL_UP}, {"name": "wheel-down", "id": core.input.WHEEL_DOWN}, ] for action in actions: if self.parameter(action["name"]): core.input.register( self, action["id"], self.parameter(action["name"]), util.format.asbool( self.parameter("{}-wait".format(action["name"]), False) ), ) class Error(Module): """Represents an "error" module :param module: The module name that produced the error :param error: The error message to display :param config: Configuration to apply to the module (defaults to an empty configuration) :param theme: Theme for this module, defaults to None, which means whatever is configured in "config" """ def __init__(self, module, error, config=core.config.Config([]), theme=None): super().__init__(config, theme, core.widget.Widget(self.full_text)) self.__module = module self.__error = error """Returns the error message :param widget: the error widget to display """ def full_text(self, widget): return "{}: {}".format(self.__module, self.__error) """Overridden state, always returns critical (it *is* an error, after all""" def state(self, widget): return ["critical"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/output.py000066400000000000000000000240461450256260500231600ustar00rootroot00000000000000import sys import json import time import threading import core.theme import core.event import util.format def dump_json(obj): return obj.dict() def assign(src, dst, key, src_key=None, default=None): if not src_key: if key.startswith("_"): src_key = key else: src_key = key.replace("_", "-") # automagically replace _ with - for k in src_key if isinstance(src_key, list) else [src_key]: if k in src: dst[key] = src[k] return if default is not None: dst[key] = default class block(object): __COMMON_THEME_FIELDS = [ "separator", "separator-block-width", "default-separators", "border-top", "border-left", "border-right", "border-bottom", "fg", "bg", "padding", "prefix", "suffix", ] def __init__(self, theme, module, widget): self.__attributes = {} for key in self.__COMMON_THEME_FIELDS: tmp = theme.get(key, widget) if tmp is not None: self.__attributes[key] = tmp self.__attributes["name"] = module.id self.__attributes["instance"] = widget.id self.__attributes["prev-bg"] = theme.get("bg", "previous") 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 return False def pangoize(self, text): if not self.is_pango(text): return text self.__attributes["markup"] = "pango" attr = dict(text["pango"]) text = attr.get("full_text", "") if "full_text" in attr: del attr["full_text"] result = " 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"] ): 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): 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: 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() def statusline(self): blocks = [] self.__widgetcount = 0 for module in self.__modules: blocks.extend(self.blocks(module)) return {"blocks": blocks, "suffix": ","} def wait(self, interval): time.sleep(interval) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/theme.py000066400000000000000000000133661450256260500227250ustar00rootroot00000000000000import os import io import json import logging import copy 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.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(): themes_dict = {} for path in PATHS: for filename in glob.iglob("{}/*.json".format(path)): if "test" not in filename: themes_dict[os.path.basename(filename).replace(".json", "")] = 1 result = list(themes_dict.keys()) result.sort() return result def merge_replace(value, new_value, key): if not isinstance(value, dict): return new_value if isinstance(new_value, dict): return util.algorithm.merge(new_value, value) # right now, merging needs explicit pango support :( if "pango" in value: value["pango"]["full_text"] = new_value return value class Theme(object): def __init__(self, name="default", iconset="auto", raw_data=None): self.name = name self.__widget_count = 0 self.__previous = {} self.__current = {} self.__keywords = {} self.__value_idx = {} 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")) if iconset != "auto": self.__data = util.algorithm.merge(self.load(iconset, "icons"), self.__data) for colors in self.__data.get("colors", []): util.algorithm.merge(self.__keywords, self.load_keywords(colors)) core.event.register("draw", self.__start) core.event.register("next-widget", self.__next_widget) def keywords(self): return self.__keywords def color(self, name, default=None): return self.keywords().get(name, default) def load(self, name, subdir=""): if isinstance(name, dict): return name # support plain data for path in PATHS: theme_file = os.path.join(path, subdir, "{}.json".format(name)) result = self.__load_json(theme_file) if result != {}: return result raise RuntimeError("unable to find theme {}".format(name)) def __load_json(self, filename): filename = os.path.expanduser(filename) if not os.path.isfile(filename): return {} with io.open(filename) as data: return json.load(data) def load_keywords(self, name): try: if isinstance(name, dict): return name result = {} if name.lower() == "wal": wal = self.__load_json("~/.cache/wal/colors.json") 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 except Exception as e: log.error("failed to load colors: {}", e) def __start(self): self.__widget_count = 0 self.__current.clear() self.__previous.clear() for key, value in self.__value_idx.items(): self.__value_idx[key] = value + 1 def __next_widget(self): self.__widget_count = self.__widget_count + 1 self.__previous = dict(self.__current) self.__current.clear() def get(self, key, widget=None, default=None): if not widget: widget = core.widget.Widget("") # special handling if widget == "previous": return self.__previous.get(key, None) value = default for option in ["defaults", "cycle"]: if option in self.__data: tmp = self.__data[option] if isinstance(tmp, list): tmp = tmp[self.__widget_count % len(tmp)] value = merge_replace(value, tmp.get(key, value), key) if isinstance(value, dict): value = copy.deepcopy(value) value = merge_replace(value, self.__data.get(key, value), key) if widget.module: value = merge_replace( value, self.get(widget.module.name, None, {}).get(key, value), key ) value = merge_replace( value, self.get(widget.module.id, None, {}).get(key, value), key ) if not key in widget.state(): for state in widget.state(): theme = self.get(state, widget, {}) value = merge_replace(value, theme.get(key, value), key) if not type(value) in (list, dict): value = self.__keywords.get(value, value) if isinstance(value, list): idx = self.__value_idx.get("{}::{}".format(widget.id, key), 0) % len(value) self.__value_idx["{}::{}".format(widget.id, key)] = idx widget.set(key, idx) value = value[idx] self.__current[key] = value return value # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/core/widget.py000066400000000000000000000045011450256260500230750ustar00rootroot00000000000000import logging import core.input import core.decorators import util.store import util.format log = logging.getLogger(__name__) class Widget(util.store.Store, core.input.Object): def __init__(self, full_text="", name=None, widget_id=None, hidden=False): super(Widget, self).__init__() self.__full_text = full_text self.module = None self.name = name self.id = widget_id or self.id self.hidden = hidden @property def module(self): return self.__module @module.setter def module(self, module): self.__module = module if self.index() < 0: return if module: custom_ids = util.format.aslist(module.parameter("id")) if len(custom_ids) > self.index(): self.id = custom_ids[self.index()] if util.format.asbool(module.parameter("scrolling", False)) == True: if callable(self.__full_text): self.__full_text = core.decorators.scrollable( self.__full_text.__func__ ).__get__(module) else: log.warning("unable to make scrollable: {}".format(module.name)) def index(self): if not self.module: return 0 idx = 0 for w in self.module.widgets(): if w.id == self.id: return idx idx = idx + 1 return -1 # not found def theme(self, attribute): attr = "theme.{}".format(attribute) if self.module: param = util.format.aslist(self.module.parameter(attr)) if param and len(param) > self.index(): return param[self.index()] return self.get(attr) def full_text(self, value=None): if value: self.__full_text = value else: if callable(self.__full_text): return self.__full_text(self) return self.__full_text def state(self): rv = [] if self.get("state", None): tmp = self.get("state") rv = tmp[:] if isinstance(tmp, list) else [tmp] if self.module: tmp = self.module.state(self) rv.extend(tmp if isinstance(tmp, list) else [tmp]) return rv # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/discover.py000066400000000000000000000013411450256260500224770ustar00rootroot00000000000000import os import sys def discover(): libdir = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "bumblebee_status") ) sys.path.append(libdir) def utility(name): current_path = os.path.dirname(os.path.abspath(__file__)) for path in [ os.path.join(current_path, "..", "bin"), os.path.join( current_path, "..", "..", "..", "..", "share", "bumblebee-status", "utility" ), "/usr/share/bumblebee-status/bin/", ]: if os.path.exists(os.path.abspath(os.path.join(path, name))): return os.path.abspath(os.path.join(path, name)) raise Exception("{} not found".format(name)) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/000077500000000000000000000000001450256260500217605ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/__init__.py000066400000000000000000000000001450256260500240570ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/contrib/000077500000000000000000000000001450256260500234205ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/contrib/__init__.py000066400000000000000000000000001450256260500255170ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/contrib/amixer.py000066400000000000000000000055231450256260500252640ustar00rootroot00000000000000"""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 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.__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 ) 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") def increase_volume(self, event): self.set_parameter("{}%+".format(self.__change)) def decrease_volume(self, event): self.set_parameter("{}%-".format(self.__change)) def set_parameter(self, parameter): util.cli.execute("amixer -c {} -q set {} {}".format(self.__card, self.__device, parameter)) def volume(self, widget): if self.__level == "n/a": return self.__level m = re.search(r"([\d]+)\%", self.__level) self.__muted = True if m: if m.group(1) != "0" and "[on]" in self.__level: self.__muted = False return "{}%".format(m.group(1)) else: return "0%" def update(self): try: self.__level = util.cli.execute( "amixer -c {} get {}".format(self.__card, self.__device) ) except Exception as e: self.__level = "n/a" def state(self, widget): if self.__muted: return ["warning", "muted"] return ["unmuted"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/apt.py000066400000000000000000000047701450256260500245660ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays APT package update information (/) Requires the following packages: * aptitude contributed by `qba10 `_ - many thanks! """ import re import threading import core.module import core.widget import core.decorators import core.input import util.cli PATTERN = "{} packages upgraded, {} newly installed, {} to remove and {} not upgraded." def parse_result(to_parse): # We want the line with the package upgrade information line_to_parse = to_parse.split("\n")[-4] result = re.search( r"(.+) packages upgraded, (.+) newly installed, (.+) to remove", line_to_parse ) return int(result.group(1)), int(result.group(3)) def get_apt_check_info(module): widget = module.widget() try: res = util.cli.execute("aptitude full-upgrade --simulate --assume-yes") widget.set("error", None) except (RuntimeError, FileNotFoundError) as e: widget.set("error", "unable to query APT: {}".format(e)) return to_upgrade = 0 to_remove = 0 try: to_upgrade, to_remove = parse_result(res) widget.set("to_upgrade", to_upgrade) widget.set("to_remove", to_remove) except Exception as e: widget.set("error", "parse error: {}".format(e)) core.event.trigger("update", [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.__thread = None core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.updates) def updates(self, widget): if widget.get("error"): return widget.get("error") return "{} to upgrade, {} to remove".format( widget.get("to_upgrade", 0), widget.get("to_remove", 0) ) def update(self): if self.__thread and self.__thread.is_alive(): return self.__thread = threading.Thread(target=get_apt_check_info, args=(self,)) self.__thread.start() def state(self, widget): cnt = 0 ret = "good" for t in ["to_upgrade", "to_remove"]: cnt += widget.get(t, 0) if cnt > 50: ret = "critical" elif cnt > 0: ret = "warning" if widget.get("error"): ret = "critical" return ret # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/arandr.py000066400000000000000000000141461450256260500252470ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Enables handy interaction with arandr for display management. Left-clicking will execute arandr for interactive display management. Right-clicking will bring up a context- and state-sensitive menu that will allow you to switch to a 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 contributed by `zerorust `_ - many thanks! """ import fnmatch from functools import partial import logging import os import re import core.module import core.widget import core.input import core.decorators from util import popup from util.cli import execute log = logging.getLogger(__name__) __screenlayout_dir__ = os.path.expanduser("~/.screenlayout") class Module(core.module.Module): @core.decorators.never def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget('')) self.manager = self.parameter("manager", "arandr") self.toggle_cmd = "xrandr" core.input.register( self, button=core.input.LEFT_MOUSE, cmd=self.popup, ) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.popup) @staticmethod def activate_layout(layout_path): log.debug("activating layout") log.debug(layout_path) execute(layout_path, ignore_errors=True) def popup(self, widget): """Create Popup that allows the user to control their displays in one of three ways: launch arandr, select a pre-set screenlayout, toggle a display. """ menu = popup.menu() menu.add_menuitem( "arandr", callback=partial(execute, self.manager, ignore_errors=True) ) menu.add_separator() displays = Module._get_displays() log.debug(displays) layouts = Module._get_layouts() available_layouts = Module._prune_layouts(layouts, displays) log.debug("Available layouts:") log.debug(available_layouts) if len(available_layouts) > 0: for layout in available_layouts: sh = os.path.join(__screenlayout_dir__, layout) sh_name = os.path.splitext(layout)[0] menu.add_menuitem(sh_name, callback=partial(self.activate_layout, sh)) menu.add_separator() count_on = 0 for display, state in displays.items(): if state[1]: count_on += 1 for display, state in displays.items(): if not state[0]: continue on_off = "On" if state[1] else "Off" menu_line = "{}: {}".format(display, on_off) menu.add_menuitem(menu_line, callback=partial(self.toggle_display, display, state[1], count_on)) menu.show(widget, 0, 0) def toggle_display(self, display, current_state, count_on): """Toggle a display on or off based on its current state.""" if current_state: log.debug("toggling off {}".format(display)) if count_on == 1: log.info("attempted to turn off last display") return execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True) else: log.debug("toggling on {}".format(display)) execute( "{} --output {} --auto".format(self.toggle_cmd, display), ignore_errors=True ) @staticmethod def _get_displays(): """Queries xrandr and builds a dict of the displays and their state. The dict entries are key by the display and are bools (True if connected). """ displays = {} for line in execute("xrandr -q", ignore_errors=True).split("\n"): if "connected" not in line: continue is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line)) parts = line.split(" ", 2) display = parts[0] displays[display] = ( (True, is_on) if parts[1] == "connected" else (False, is_on) ) return displays @staticmethod 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)) return layouts @staticmethod def _parse_layout(line): """Parses a single xrandr line to find what displays are active in the command. Returns them as a list. """ active_displays = [] to_check = line[7:].split("--output ") for check in to_check: if not check or "off" in check: continue active_displays.append(check.split(" ")[0]) return active_displays @staticmethod def _prune_layouts(layouts, displays): """Return a list of layouts whose displays are actually connected.""" available = [] for layout, needs in layouts.items(): still_valid = True for need in needs: if need not in displays or not displays[need][0]: still_valid = False break if still_valid: available.append(layout) return available # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/arch-update.py000066400000000000000000000027521450256260500261750ustar00rootroot00000000000000"""Check updates to Arch Linux. Requires the following executable: * checkupdates (from pacman-contrib) contributed by `lucassouto `_ - many thanks! """ import logging from time import sleep 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 Arch: {}") def utilization(self, widget): return self.__format.format(self.__packages) def hidden(self): return self.__packages == 0 and not self.__error 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")) elif code == 2: self.__packages = 0 else: self.__error = True logging.error("checkupdates 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/arch_update.py000077700000000000000000000000001450256260500310132arch-update.pyustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/contrib/aur-update.py000066400000000000000000000027051450256260500260450ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/battery-upower.py000066400000000000000000000322331450256260500267660ustar00rootroot00000000000000# UPowerManger Class Copyright (C) 2017 Oscar Svensson (wogscpar) under MIT licence from upower-python """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! """ import dbus import logging import core.module import core.widget import core.input import util.format class UPowerManager: def __init__(self): self.UPOWER_NAME = "org.freedesktop.UPower" self.UPOWER_PATH = "/org/freedesktop/UPower" self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties" self.bus = dbus.SystemBus() def detect_devices(self): upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH) upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME) devices = upower_interface.EnumerateDevices() return devices def get_display_device(self): upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH) upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME) dispdev = upower_interface.GetDisplayDevice() return dispdev def get_critical_action(self): upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH) upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME) critical_action = upower_interface.GetCriticalAction() return critical_action def get_device_percentage(self, battery): battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery) battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES) return battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Percentage") def get_full_device_information(self, battery): battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery) battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES) hasHistory = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "HasHistory" ) hasStatistics = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "HasStatistics" ) isPresent = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "IsPresent" ) isRechargeable = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "IsRechargeable" ) online = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Online") powersupply = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "PowerSupply" ) capacity = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Capacity") energy = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Energy") energyempty = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "EnergyEmpty" ) energyfull = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "EnergyFull" ) energyfulldesign = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "EnergyFullDesign" ) energyrate = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "EnergyRate" ) luminosity = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "Luminosity" ) percentage = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "Percentage" ) temperature = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "Temperature" ) voltage = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Voltage") timetoempty = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "TimeToEmpty" ) timetofull = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "TimeToFull" ) iconname = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "IconName") model = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Model") nativepath = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "NativePath" ) serial = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Serial") vendor = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Vendor") state = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State") technology = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "Technology" ) battype = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Type") warninglevel = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "WarningLevel" ) updatetime = battery_proxy_interface.Get( self.UPOWER_NAME + ".Device", "UpdateTime" ) information_table = { "HasHistory": hasHistory, "HasStatistics": hasStatistics, "IsPresent": isPresent, "IsRechargeable": isRechargeable, "Online": online, "PowerSupply": powersupply, "Capacity": capacity, "Energy": energy, "EnergyEmpty": energyempty, "EnergyFull": energyfull, "EnergyFullDesign": energyfulldesign, "EnergyRate": energyrate, "Luminosity": luminosity, "Percentage": percentage, "Temperature": temperature, "Voltage": voltage, "TimeToEmpty": timetoempty, "TimeToFull": timetofull, "IconName": iconname, "Model": model, "NativePath": nativepath, "Serial": serial, "Vendor": vendor, "State": state, "Technology": technology, "Type": battype, "WarningLevel": warninglevel, "UpdateTime": updatetime, } return information_table def is_lid_present(self): upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH) upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES) is_lid_present = bool(upower_interface.Get(self.UPOWER_NAME, "LidIsPresent")) return is_lid_present def is_lid_closed(self): upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH) upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES) is_lid_closed = bool(upower_interface.Get(self.UPOWER_NAME, "LidIsClosed")) return is_lid_closed def on_battery(self): upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH) upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES) on_battery = bool(upower_interface.Get(self.UPOWER_NAME, "OnBattery")) return on_battery def has_wakeup_capabilities(self): upower_proxy = self.bus.get_object( self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups" ) upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES) has_wakeup_capabilities = bool( upower_interface.Get(self.UPOWER_NAME + ".Wakeups", "HasCapability") ) return has_wakeup_capabilities def get_wakeups_data(self): upower_proxy = self.bus.get_object( self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups" ) upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME + ".Wakeups") data = upower_interface.GetData() return data def get_wakeups_total(self): upower_proxy = self.bus.get_object( self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups" ) upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME + ".Wakeups") 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) state = int(battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State")) if state == 1: return True else: return False def get_state(self, battery): battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery) battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES) state = int(battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State")) if state == 0: return "Unknown" elif state == 1: return "Loading" elif state == 2: return "Discharging" elif state == 3: return "Empty" elif state == 4: return "Fully charged" elif state == 5: return "Pending charge" elif state == 6: return "Pending discharge" class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.capacity)) try: self.power = UPowerManager() self.device = self.power.get_display_device() except Exception as e: logging.exception("unable to get battery display device: {}".format(str(e))) core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics" ) self._showremaining = util.format.asbool(self.parameter("showremaining", True)) def capacity(self, widget): 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 widget.set("capacity", capacity) output = "{}%".format(capacity) widget.set("theme.minwidth", "100%") except Exception as e: logging.exception("unable to get battery capacity: {}".format(str(e))) if self._showremaining: try: p = self.power # an alias to make each line of code shorter proxy = p.bus.get_object(p.UPOWER_NAME, self.device) interface = dbus.Interface(proxy, p.DBUS_PROPERTIES) state = int(interface.Get(p.UPOWER_NAME + ".Device", "State")) # state: 1 => charging, 2 => discharging, other => don't care remain = int( interface.Get( p.UPOWER_NAME + ".Device", ["TimeToFull", "TimeToEmpty"][state - 1], ) ) remain = util.format.duration(remain, compact=True, unit=True) output = "{} {}".format(output, remain) except IndexError: pass except Exception as e: logging.exception( "unable to get battery remaining time: {}".format(str(e)) ) return output def state(self, widget): state = [] capacity = widget.get("capacity", -1) if capacity < 0: return ["critical", "unknown"] if widget.get("ac"): state.append("AC") else: charge = "Unknown" try: charge = self.power.get_state(self.device) except Exception as e: logging.exception("unable to get charge value: {}".format(str(e))) if charge == "Discharging": state.append( "discharging-{}".format( 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: if capacity > 95: 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 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/battery.py000066400000000000000000000171241450256260500254510ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays battery status, remaining percentage and charging information. Parameters: * battery.device : Comma-separated list of battery devices to read information from (defaults to auto for auto-detection) * battery.warning : Warning threshold in % of remaining charge (defaults to 20) * battery.critical : Critical threshold in % of remaining charge (defaults to 10) * battery.showdevice : If set to 'true', add the device name to the widget (defaults to False) * battery.decorate : If set to 'false', hides additional icons (charging, etc.) (defaults to True) * battery.showpowerconsumption: If set to 'true', show current power consumption (defaults to False) * battery.compact-devices : If set to 'true', compacts multiple batteries into a single entry (default to False) (partially) contributed by `martindoublem `_ - many thanks! """ import os import glob import logging log = logging.getLogger(__name__) try: import power except ImportError: log.warning('unable to import module "power": Time estimates will not be available') import core.module import core.input import util.format class BatteryManager(object): def remaining(self): try: estimate = power.PowerManagement().get_time_remaining_estimate() # do not show remaining if on AC if estimate == power.common.TIME_REMAINING_UNLIMITED: return None return estimate * 60 # return value in seconds except Exception as e: return -1 return -1 def read(self, battery, component, default=None): path = "/sys/class/power_supply/{}".format(battery) if not os.path.exists(path): return default try: with open("{}/{}".format(path, component)) as f: return f.read().strip() except IOError: return "n/a" return default def capacity(self, battery): capacity = self.read(battery, "capacity", 100) if capacity != "n/a": capacity = int(capacity) return capacity if capacity < 100 else 100 def capacity_all(self, batteries): now = 0 full = 0 for battery in batteries: try: with open( "/sys/class/power_supply/{}/energy_full".format(battery) ) as f: full += int(f.read()) with open("/sys/class/power_supply/{}/energy_now".format(battery)) as f: now += int(f.read()) except IOError: return "n/a" return int(float(now) / float(full) * 100.0) def isac(self, battery): path = "/sys/class/power_supply/{}".format(battery) return not os.path.exists(path) def isac_any(self, batteries): for battery in batteries: if self.isac(battery): return True return False def consumption(self, battery): consumption = self.read(battery, "power_now", "n/a") if consumption == "n/a": return "n/a" return "{}W".format(int(consumption) / 1000000) def charge(self, battery): return self.read(battery, "status", "n/a") def charge_any(self, batteries): for battery in batteries: if self.charge(battery) == "Discharging": return "Discharging" return "Charged" class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self.__manager = BatteryManager() self._batteries = util.format.aslist(self.parameter("device", "auto")) if self._batteries[0] == "auto": self._batteries = [ os.path.basename(battery) for battery in glob.glob("/sys/class/power_supply/BAT*") ] core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics" ) if len(self._batteries) == 0: widget = self.add_widget(full_text=self.ac, name="ac") widget.set("ac", True) widget.set("capacity", 100) else: if util.format.asbool(self.parameter("compact-devices", False)): widget = self.add_widget(full_text=self.capacity, name="all-batteries") else: for battery in self._batteries: log.debug("adding new widget for {}".format(battery)) widget = self.add_widget(full_text=self.capacity, name=battery) 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" def capacity(self, widget): if widget.name == "all-batteries": capacity = self.__manager.capacity_all(self._batteries) else: 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) ) else: output = "{}%".format(capacity) if ( util.format.asbool(self.parameter("showremaining", True)) and self.__manager.charge(widget.name) == "Discharging" ): remaining = self.__manager.remaining() if remaining >= 0: output = "{} {}".format( output, util.format.duration(remaining, compact=True, unit=True) ) if util.format.asbool(self.parameter("showdevice", False)): output = "{} ({})".format(output, widget.name) return output def state(self, widget): state = [] capacity = widget.get("capacity") if capacity < 0: log.debug("battery state: {}".format(state)) return ["critical", "unknown"] if widget.get("ac"): state.append("AC") else: if widget.name == "all-batteries": charge = self.__manager.charge_any(self._batteries) else: charge = self.__manager.charge(widget.name) if charge == "Discharging": state.append( "discharging-{}".format( 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: if capacity > 95: 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 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/battery_upower.py000077700000000000000000000000001450256260500324032battery-upower.pyustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/contrib/bluetooth.py000066400000000000000000000074171450256260500260100ustar00rootroot00000000000000"""Displays bluetooth status (Bluez). Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state. Parameters: * bluetooth.device : the device to read state from (default is hci0) * bluetooth.manager : application to launch on click (blueman-manager) * bluetooth.dbus_destination : dbus destination (defaults to org.blueman.Mechanism) * bluetooth.dbus_destination_path : dbus destination path (defaults to /) * bluetooth.right_click_popup : use popup menu when right-clicked (defaults to True) contributed by `brunosmmm `_ - many thanks! """ import os import re import logging import core.module import core.widget import core.input import util.cli import util.format import util.popup class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.status)) device = self.parameter("device", "hci0") self.manager = self.parameter("manager", "blueman-manager") self._path = "/sys/class/bluetooth/{}".format(device) self._status = "Off" core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager) # determine whether to use pop-up menu or simply toggle the device on/off right_click_popup = util.format.asbool( self.parameter("right_click_popup", True) ) if right_click_popup: core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.popup) else: core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle) def status(self, widget): """Get status.""" return self._status def update(self): """Update current state.""" if not os.path.exists(self._path): self._status = "?" return # search for whichever rfkill directory available try: dirnames = next(os.walk(self._path))[1] for dirname in dirnames: m = re.match(r"rfkill[0-9]+", dirname) if m is not None: with open(os.path.join(self._path, dirname, "state"), "r") as f: state = int(f.read()) if state == 1: self._status = "On" else: self._status = "Off" return except IOError: self._status = "?" def popup(self, widget): """Show a popup menu.""" menu = util.popup.menu(self.__config) if self._status == "On": menu.add_menuitem("Disable Bluetooth", callback=self._toggle) elif self._status == "Off": menu.add_menuitem("Enable Bluetooth", callback=self._toggle) else: return menu.show(widget) def _toggle(self, widget=None): """Toggle bluetooth state.""" if self._status == "On": state = "false" else: state = "true" dst = self.parameter("dbus_destination", "org.blueman.Mechanism") dst_path = self.parameter("dbus_destination_path", "/") cmd = ( "dbus-send --system --print-reply --dest={}" " {} org.blueman.Mechanism.SetRfkillState" " boolean:{}".format(dst, dst_path, state) ) logging.debug("bt: toggling bluetooth") util.cli.execute(cmd, ignore_errors=True) def state(self, widget): """Get current state.""" state = [] if self._status == "?": state = ["unknown"] elif self._status == "On": state = ["ON"] else: state = ["OFF"] return state # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/bluetooth2.py000066400000000000000000000063211450256260500260630ustar00rootroot00000000000000"""Displays bluetooth status. Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and python-dbus to count the number of connections Parameters: * bluetooth.manager : application to launch on click (blueman-manager) contributed by `martindoublem `_ - many thanks! """ import os import re import subprocess import dbus import dbus.mainloop.glib import logging 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(self.status)) self.manager = self.parameter("manager", "blueman-manager") self._status = "Off" dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) self._bus = dbus.SystemBus() core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle) def status(self, widget): """Get status.""" return self._status def update(self): """Update current state.""" state = len( subprocess.run(["bluetoothctl", "list"], stdout=subprocess.PIPE).stdout ) if state > 0: connected_devices = self.get_connected_devices() self._status = "On - {}".format(connected_devices) else: self._status = "Off" adapters_cmd = "rfkill list | grep Bluetooth" if not len( subprocess.run(adapters_cmd, shell=True, stdout=subprocess.PIPE).stdout ): self._status = "No Adapter Found" return def _toggle(self, widget=None): """Toggle bluetooth state.""" if "On" in self._status: state = "false" else: state = "true" 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") util.cli.execute(cmd, ignore_errors=True) def state(self, widget): """Get current state.""" state = [] if self._status == "No Adapter Found": state.append("critical") 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("critical") return state def get_connected_devices(self): devices = 0 objects = dbus.Interface( self._bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager" ).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"): devices += 1 return devices # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/blugon.py000066400000000000000000000051711450256260500252640ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/brightness.py000066400000000000000000000070731450256260500261510ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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! """ import glob import shutil import core.module import core.widget import core.input 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.brightness)) 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"): self.__readcmd = self.__light self.register_cmd("light -A {}%".format(step), "light -U {}%".format(step)) elif shutil.which("brightnessctl"): self.__readcmd = self.__brightnessctl self.register_cmd( "brightnessctl s {}%+".format(step), "brightnessctl s {}%-".format(step) ) else: self.__readcmd = self.__xbacklight self.register_cmd( "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) 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() def __brightnessctl(self): m = util.cli.execute("brightnessctl m").strip() g = util.cli.execute("brightnessctl g").strip() return float(g) / float(m) * 100.0 def __xbacklight(self): return util.cli.execute("xbacklight -get").strip() def update(self): try: tmp = self.__readcmd() if isinstance(tmp, str): self.__brightness = tmp else: self.__brightness = "{:3.0f}%".format(float(tmp)) except: self.__brightness = "n/a" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/caffeine.py000066400000000000000000000057721450256260500255450ustar00rootroot00000000000000# pylint: disable=C0111,R0903,W0212 """Enable/disable automatic screen locking. Requires the following executables: * xdg-screensaver * xdotool * xprop (as dependency for xdotool) * notify-send contributed by `TheEdgeOfRage `_ - many thanks! """ import logging import os import shutil import psutil import core.module import core.widget import core.input import core.decorators import util.cli class Module(core.module.Module): @core.decorators.every(minutes=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget("")) self.__active = False self.__xid = None core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__toggle) def __check_requirements(self): requirements = ["xdotool", "xprop", "xdg-screensaver"] missing = [] for tool in requirements: if not shutil.which(tool): missing.append(tool) return missing def __get_i3bar_xid(self): xid = ( util.cli.execute("xdotool search --class 'i3bar'") .partition("\n")[0] .strip() ) if xid.isdigit(): return xid logging.warning("Module caffeine: xdotool couldn't get X window ID of 'i3bar'.") return None def __notify(self): if not shutil.which("notify-send"): return if self.__active: util.cli.execute("notify-send 'Consuming caffeine'") else: util.cli.execute("notify-send 'Out of coffee'") def _suspend_screensaver(self): self.__xid = self.__get_i3bar_xid() if self.__xid is None: return False pid = os.fork() if pid == 0: os.setsid() util.cli.execute("xdg-screensaver suspend {}".format(self.__xid)) os._exit(0) else: os.waitpid(pid, 0) return True def __resume_screensaver(self): success = True xprop_path = shutil.which("xprop") pids = [ p.pid for p in psutil.process_iter() if p.cmdline() == [xprop_path, "-id", str(self.__xid), "-spy"] ] for pid in pids: try: os.kill(pid, 9) except OSError: success = False return success def state(self, _): if self.__active: return "activated" return "deactivated" def __toggle(self, _): missing = self.__check_requirements() if missing: logging.warning("Could not run caffeine - missing %s!", ", ".join(missing)) return self.__active = not self.__active if self.__active: success = self._suspend_screensaver() else: success = self.__resume_screensaver() if success: self.__notify() else: self.__active = not self.__active # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/cmus.py000066400000000000000000000130701450256260500247420ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays information about the current song in cmus. Requires the following executable: * cmus-remote Parameters: * cmus.format: Format string for the song information. Tag values can be put in curly brackets (i.e. {artist}) Additional tags: * {file} - full song file name * {file1} - song file name without path prefix if {file} = '/foo/bar.baz', then {file1} = 'bar.baz' * {file2} - song file name without path prefix and extension suffix if {file} = '/foo/bar.baz', then {file2} = 'bar' * cmus.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles cmus.prev, cmus.next, cmus.shuffle and cmus.repeat, and the main display with play/pause function cmus.main. * cmus.server: The address of the cmus server, either a UNIX socket or host[:port]. Connects to the local instance by default. * cmus.passwd: The password to use for the TCP/IP connection. contributed by `TheEdgeOfRage `_ - many thanks! """ from collections import defaultdict import os import string import core.module import core.input import core.decorators import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self._layout = self.parameter( "layout", "cmus.prev cmus.main cmus.next cmus.shuffle cmus.repeat" ) self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}") self._server = self.parameter("server", None) self._passwd = self.parameter("passwd", None) self._status = None self._shuffle = False self._repeat = False self._tags = defaultdict(lambda: "") # Create widgets widget_map = {} for widget_name in self._layout.split(): widget = self.add_widget(name=widget_name) self._cmd = "cmus-remote" if self._server is not None: self._cmd = "{cmd} --server {server}".format( cmd=self._cmd, server=self._server ) if self._passwd is not None: self._cmd = "{cmd} --passwd {passwd}".format( cmd=self._cmd, passwd=self._passwd ) if widget_name == "cmus.prev": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "{cmd} -r".format(cmd=self._cmd), } elif widget_name == "cmus.main": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "{cmd} -u".format(cmd=self._cmd), } widget.full_text(self.description) elif widget_name == "cmus.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "{cmd} -n".format(cmd=self._cmd), } elif widget_name == "cmus.shuffle": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "{cmd} -S".format(cmd=self._cmd), } elif widget_name == "cmus.repeat": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "{cmd} -R".format(cmd=self._cmd), } else: raise KeyError( "The cmus module does not support a {widget_name!r} widget".format( widget_name=widget_name ) ) # Register input callbacks for widget, callback_options in widget_map.items(): core.input.register(widget, **callback_options) def hidden(self): return self._status is None @core.decorators.scrollable def description(self, widget): return string.Formatter().vformat(self._fmt, (), self._tags) def update(self): self._load_song() def state(self, widget): returns = { "cmus.shuffle": "shuffle-on" if self._shuffle else "shuffle-off", "cmus.repeat": "repeat-on" if self._repeat else "repeat-off", "cmus.prev": "prev", "cmus.next": "next", } return returns.get(widget.name, self._status) def _eval_line(self, line): if line.startswith("file "): full_file = line[5:] file1 = os.path.basename(full_file) file2 = os.path.splitext(file1)[0] self._tags.update({"file": full_file}) self._tags.update({"file1": file1}) self._tags.update({"file2": file2}) return name, key, value = (line.split(" ", 2) + [None, None])[:3] if name == "status": self._status = key if name == "tag": self._tags.update({key: value}) if name in ["duration", "position"]: self._tags.update({name: util.format.duration(int(key))}) if name == "set" and key == "repeat": self._repeat = value == "true" if name == "set" and key == "shuffle": self._shuffle = value == "true" def _load_song(self): info = "" try: info = util.cli.execute("{cmd} -Q".format(cmd=self._cmd)) except RuntimeError: self._status = None self._tags = defaultdict(lambda: "") for line in info.split("\n"): self._eval_line(line) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/cpu2.py000066400000000000000000000131471450256260500246510ustar00rootroot00000000000000"""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: * cpu2.layout: Space-separated list of widgets to add. Possible widgets are: * cpu2.maxfreq * cpu2.cpuload * cpu2.coresload * cpu2.temp * 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 * cpu2.fan_pattern: pattern to look for in the output of 'sensors -u'; required if cpu2.fanspeed widget 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! """ 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", "cpu2.maxfreq cpu2.cpuload cpu2.coresload cpu2.temp cpu2.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 == "cpu2.maxfreq": widget = self.add_widget(name=widget_name, full_text=self.maxfreq) widget.set("type", "freq") elif widget_name == "cpu2.cpuload": widget = self.add_widget(name=widget_name, full_text=self.cpuload) widget.set("type", "load") elif widget_name == "cpu2.coresload": widget = self.add_widget(name=widget_name, full_text=self.coresload) widget.set("type", "loads") elif widget_name == "cpu2.temp": widget = self.add_widget(name=widget_name, full_text=self.temp) widget.set("type", "temp") elif widget_name == "cpu2.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_pattern = self.parameter("temp_pattern") if self.__temp_pattern is None: self.__temp = "n/a" self.__fan_pattern = self.parameter("fan_pattern") if self.__fan_pattern is None: self.__fan = "n/a" # maxfreq is loaded only once at startup if "cpu2.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 -u") lines = output.split("\n") temp = "n/a" fan = "n/a" temp_line = None fan_line = None for line in lines: if self.__temp_pattern is not None and self.__temp_pattern in line: temp_line = line if self.__fan_pattern is not None and self.__fan_pattern in line: fan_line = line if temp_line is not None and fan_line is not None: break if temp_line is not None: temp = round(float(temp_line.split(":")[1].strip())) if fan_line is not None: fan = int(fan_line.split(":")[1].strip()[:-4]) return temp, fan def update(self): if "cpu2.maxfreq" in self.__widget_names: self.__maxfreq = psutil.cpu_freq().max / 1000 if "cpu2.cpuload" in self.__widget_names: self.__cpuload = round(psutil.cpu_percent(percpu=False)) if "cpu2.coresload" in self.__widget_names: self.__coresload = psutil.cpu_percent(percpu=True) if "cpu2.temp" in self.__widget_names or "cpu2.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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/cpu3.py000066400000000000000000000131161450256260500246460ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/currency.py000066400000000000000000000437101450256260500256310ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # pylint: disable=C0111,R0903 """Displays currency exchange rates. Currently, displays currency between GBP and USD/EUR only. Requires the following python packages: * requests Parameters: * currency.interval: Interval in minutes between updates, default is 1. * currency.source: Source currency (ex. 'GBP', 'EUR'). Defaults to 'auto', which infers the local one from IP address. * currency.destination: Comma-separated list of destination currencies (defaults to 'USD,EUR') * currency.sourceformat: String format for source formatting; Defaults to '{}: {}' and has two variables, the base symbol and the rate list * currency.destinationdelimiter: Delimiter used for separating individual rates (defaults to '|') Note: source and destination names right now must correspond to the names used by the API of https://markets.ft.com contributed by `AntouanK `_ - many thanks! """ import requests try: from babel.numbers import format_currency except ImportError: format_currency = None import json import os import core.module import core.widget import core.decorators import util.format import util.location SYMBOL = {"GBP": "£", "EUR": "€", "USD": "$", "JPY": "¥", "KRW": "₩"} DEFAULT_DEST = "USD,EUR,auto" DEFAULT_SRC = "GBP" API_URL = "https://markets.ft.com/data/currencies/ajax/conversion?baseCurrency={}&comparison={}" def load_country_to_currency(): return [ {"country": "Afghanistan", "currency_code": "AFN"}, {"country": "Albania", "currency_code": "ALL"}, {"country": "Algeria", "currency_code": "DZD"}, {"country": "American Samoa", "currency_code": "USD"}, {"country": "Andorra", "currency_code": "EUR"}, {"country": "Angola", "currency_code": "AOA"}, {"country": "Anguilla", "currency_code": "XCD"}, {"country": "Antarctica", "currency_code": "XCD"}, {"country": "Antigua and Barbuda", "currency_code": "XCD"}, {"country": "Argentina", "currency_code": "ARS"}, {"country": "Armenia", "currency_code": "AMD"}, {"country": "Aruba", "currency_code": "AWG"}, {"country": "Australia", "currency_code": "AUD"}, {"country": "Austria", "currency_code": "EUR"}, {"country": "Azerbaijan", "currency_code": "AZN"}, {"country": "Bahamas", "currency_code": "BSD"}, {"country": "Bahrain", "currency_code": "BHD"}, {"country": "Bangladesh", "currency_code": "BDT"}, {"country": "Barbados", "currency_code": "BBD"}, {"country": "Belarus", "currency_code": "BYR"}, {"country": "Belgium", "currency_code": "EUR"}, {"country": "Belize", "currency_code": "BZD"}, {"country": "Benin", "currency_code": "XOF"}, {"country": "Bermuda", "currency_code": "BMD"}, {"country": "Bhutan", "currency_code": "BTN"}, {"country": "Bolivia", "currency_code": "BOB"}, {"country": "Bosnia and Herzegovina", "currency_code": "BAM"}, {"country": "Botswana", "currency_code": "BWP"}, {"country": "Bouvet Island", "currency_code": "NOK"}, {"country": "Brazil", "currency_code": "BRL"}, {"country": "British Indian Ocean Territory", "currency_code": "USD"}, {"country": "Brunei", "currency_code": "BND"}, {"country": "Bulgaria", "currency_code": "BGN"}, {"country": "Burkina Faso", "currency_code": "XOF"}, {"country": "Burundi", "currency_code": "BIF"}, {"country": "Cambodia", "currency_code": "KHR"}, {"country": "Cameroon", "currency_code": "XAF"}, {"country": "Canada", "currency_code": "CAD"}, {"country": "Cape Verde", "currency_code": "CVE"}, {"country": "Cayman Islands", "currency_code": "KYD"}, {"country": "Central African Republic", "currency_code": "XAF"}, {"country": "Chad", "currency_code": "XAF"}, {"country": "Chile", "currency_code": "CLP"}, {"country": "China", "currency_code": "CNY"}, {"country": "Christmas Island", "currency_code": "AUD"}, {"country": "Cocos (Keeling) Islands", "currency_code": "AUD"}, {"country": "Colombia", "currency_code": "COP"}, {"country": "Comoros", "currency_code": "KMF"}, {"country": "Congo", "currency_code": "XAF"}, {"country": "Cook Islands", "currency_code": "NZD"}, {"country": "Costa Rica", "currency_code": "CRC"}, {"country": "Croatia", "currency_code": "HRK"}, {"country": "Cuba", "currency_code": "CUP"}, {"country": "Cyprus", "currency_code": "EUR"}, {"country": "Czech Republic", "currency_code": "CZK"}, {"country": "Denmark", "currency_code": "DKK"}, {"country": "Djibouti", "currency_code": "DJF"}, {"country": "Dominica", "currency_code": "XCD"}, {"country": "Dominican Republic", "currency_code": "DOP"}, {"country": "East Timor", "currency_code": "USD"}, {"country": "Ecuador", "currency_code": "ECS"}, {"country": "Egypt", "currency_code": "EGP"}, {"country": "El Salvador", "currency_code": "SVC"}, {"country": "England", "currency_code": "GBP"}, {"country": "Equatorial Guinea", "currency_code": "XAF"}, {"country": "Eritrea", "currency_code": "ERN"}, {"country": "Estonia", "currency_code": "EUR"}, {"country": "Ethiopia", "currency_code": "ETB"}, {"country": "Falkland Islands", "currency_code": "FKP"}, {"country": "Faroe Islands", "currency_code": "DKK"}, {"country": "Fiji Islands", "currency_code": "FJD"}, {"country": "Finland", "currency_code": "EUR"}, {"country": "France", "currency_code": "EUR"}, {"country": "French Guiana", "currency_code": "EUR"}, {"country": "French Polynesia", "currency_code": "XPF"}, {"country": "French Southern territories", "currency_code": "EUR"}, {"country": "Gabon", "currency_code": "XAF"}, {"country": "Gambia", "currency_code": "GMD"}, {"country": "Georgia", "currency_code": "GEL"}, {"country": "Germany", "currency_code": "EUR"}, {"country": "Ghana", "currency_code": "GHS"}, {"country": "Gibraltar", "currency_code": "GIP"}, {"country": "Greece", "currency_code": "EUR"}, {"country": "Greenland", "currency_code": "DKK"}, {"country": "Grenada", "currency_code": "XCD"}, {"country": "Guadeloupe", "currency_code": "EUR"}, {"country": "Guam", "currency_code": "USD"}, {"country": "Guatemala", "currency_code": "QTQ"}, {"country": "Guinea", "currency_code": "GNF"}, {"country": "Guinea-Bissau", "currency_code": "CFA"}, {"country": "Guyana", "currency_code": "GYD"}, {"country": "Haiti", "currency_code": "HTG"}, {"country": "Heard Island and McDonald Islands", "currency_code": "AUD"}, {"country": "Holy See (Vatican City State)", "currency_code": "EUR"}, {"country": "Honduras", "currency_code": "HNL"}, {"country": "Hong Kong", "currency_code": "HKD"}, {"country": "Hungary", "currency_code": "HUF"}, {"country": "Iceland", "currency_code": "ISK"}, {"country": "India", "currency_code": "INR"}, {"country": "Indonesia", "currency_code": "IDR"}, {"country": "Iran", "currency_code": "IRR"}, {"country": "Iraq", "currency_code": "IQD"}, {"country": "Ireland", "currency_code": "EUR"}, {"country": "Israel", "currency_code": "ILS"}, {"country": "Italy", "currency_code": "EUR"}, {"country": "Ivory Coast", "currency_code": "XOF"}, {"country": "Jamaica", "currency_code": "JMD"}, {"country": "Japan", "currency_code": "JPY"}, {"country": "Jordan", "currency_code": "JOD"}, {"country": "Kazakhstan", "currency_code": "KZT"}, {"country": "Kenya", "currency_code": "KES"}, {"country": "Kiribati", "currency_code": "AUD"}, {"country": "Kuwait", "currency_code": "KWD"}, {"country": "Kyrgyzstan", "currency_code": "KGS"}, {"country": "Laos", "currency_code": "LAK"}, {"country": "Latvia", "currency_code": "LVL"}, {"country": "Lebanon", "currency_code": "LBP"}, {"country": "Lesotho", "currency_code": "LSL"}, {"country": "Liberia", "currency_code": "LRD"}, {"country": "Libyan Arab Jamahiriya", "currency_code": "LYD"}, {"country": "Liechtenstein", "currency_code": "CHF"}, {"country": "Lithuania", "currency_code": "LTL"}, {"country": "Luxembourg", "currency_code": "EUR"}, {"country": "Macao", "currency_code": "MOP"}, {"country": "North Macedonia", "currency_code": "MKD"}, {"country": "Madagascar", "currency_code": "MGF"}, {"country": "Malawi", "currency_code": "MWK"}, {"country": "Malaysia", "currency_code": "MYR"}, {"country": "Maldives", "currency_code": "MVR"}, {"country": "Mali", "currency_code": "XOF"}, {"country": "Malta", "currency_code": "EUR"}, {"country": "Marshall Islands", "currency_code": "USD"}, {"country": "Martinique", "currency_code": "EUR"}, {"country": "Mauritania", "currency_code": "MRO"}, {"country": "Mauritius", "currency_code": "MUR"}, {"country": "Mayotte", "currency_code": "EUR"}, {"country": "Mexico", "currency_code": "MXN"}, {"country": "Micronesia, Federated States of", "currency_code": "USD"}, {"country": "Moldova", "currency_code": "MDL"}, {"country": "Monaco", "currency_code": "EUR"}, {"country": "Mongolia", "currency_code": "MNT"}, {"country": "Montserrat", "currency_code": "XCD"}, {"country": "Morocco", "currency_code": "MAD"}, {"country": "Mozambique", "currency_code": "MZN"}, {"country": "Myanmar", "currency_code": "MMR"}, {"country": "Namibia", "currency_code": "NAD"}, {"country": "Nauru", "currency_code": "AUD"}, {"country": "Nepal", "currency_code": "NPR"}, {"country": "Netherlands", "currency_code": "EUR"}, {"country": "Netherlands Antilles", "currency_code": "ANG"}, {"country": "New Caledonia", "currency_code": "XPF"}, {"country": "New Zealand", "currency_code": "NZD"}, {"country": "Nicaragua", "currency_code": "NIO"}, {"country": "Niger", "currency_code": "XOF"}, {"country": "Nigeria", "currency_code": "NGN"}, {"country": "Niue", "currency_code": "NZD"}, {"country": "Norfolk Island", "currency_code": "AUD"}, {"country": "North Korea", "currency_code": "KPW"}, {"country": "Northern Ireland", "currency_code": "GBP"}, {"country": "Northern Mariana Islands", "currency_code": "USD"}, {"country": "Norway", "currency_code": "NOK"}, {"country": "Oman", "currency_code": "OMR"}, {"country": "Pakistan", "currency_code": "PKR"}, {"country": "Palau", "currency_code": "USD"}, {"country": "Palestine", "currency_code": null}, {"country": "Panama", "currency_code": "PAB"}, {"country": "Papua New Guinea", "currency_code": "PGK"}, {"country": "Paraguay", "currency_code": "PYG"}, {"country": "Peru", "currency_code": "PEN"}, {"country": "Philippines", "currency_code": "PHP"}, {"country": "Pitcairn", "currency_code": "NZD"}, {"country": "Poland", "currency_code": "PLN"}, {"country": "Portugal", "currency_code": "EUR"}, {"country": "Puerto Rico", "currency_code": "USD"}, {"country": "Qatar", "currency_code": "QAR"}, {"country": "Reunion", "currency_code": "EUR"}, {"country": "Romania", "currency_code": "RON"}, {"country": "Russian Federation", "currency_code": "RUB"}, {"country": "Rwanda", "currency_code": "RWF"}, {"country": "Saint Helena", "currency_code": "SHP"}, {"country": "Saint Kitts and Nevis", "currency_code": "XCD"}, {"country": "Saint Lucia", "currency_code": "XCD"}, {"country": "Saint Pierre and Miquelon", "currency_code": "EUR"}, {"country": "Saint Vincent and the Grenadines", "currency_code": "XCD"}, {"country": "Samoa", "currency_code": "WST"}, {"country": "San Marino", "currency_code": "EUR"}, {"country": "Sao Tome and Principe", "currency_code": "STD"}, {"country": "Saudi Arabia", "currency_code": "SAR"}, {"country": "Scotland", "currency_code": "GBP"}, {"country": "Senegal", "currency_code": "XOF"}, {"country": "Seychelles", "currency_code": "SCR"}, {"country": "Sierra Leone", "currency_code": "SLL"}, {"country": "Singapore", "currency_code": "SGD"}, {"country": "Slovakia", "currency_code": "EUR"}, {"country": "Slovenia", "currency_code": "EUR"}, {"country": "Solomon Islands", "currency_code": "SBD"}, {"country": "Somalia", "currency_code": "SOS"}, {"country": "South Africa", "currency_code": "ZAR"}, { "country": "South Georgia and the South Sandwich Islands", "currency_code": "GBP", }, {"country": "South Korea", "currency_code": "KRW"}, {"country": "South Sudan", "currency_code": "SSP"}, {"country": "Spain", "currency_code": "EUR"}, {"country": "SriLanka", "currency_code": "LKR"}, {"country": "Sudan", "currency_code": "SDG"}, {"country": "Suriname", "currency_code": "SRD"}, {"country": "Svalbard and Jan Mayen", "currency_code": "NOK"}, {"country": "Swaziland", "currency_code": "SZL"}, {"country": "Sweden", "currency_code": "SEK"}, {"country": "Switzerland", "currency_code": "CHF"}, {"country": "Syria", "currency_code": "SYP"}, {"country": "Tajikistan", "currency_code": "TJS"}, {"country": "Tanzania", "currency_code": "TZS"}, {"country": "Thailand", "currency_code": "THB"}, {"country": "The Democratic Republic of Congo", "currency_code": "CDF"}, {"country": "Togo", "currency_code": "XOF"}, {"country": "Tokelau", "currency_code": "NZD"}, {"country": "Tonga", "currency_code": "TOP"}, {"country": "Trinidad and Tobago", "currency_code": "TTD"}, {"country": "Tunisia", "currency_code": "TND"}, {"country": "Turkey", "currency_code": "TRY"}, {"country": "Turkmenistan", "currency_code": "TMT"}, {"country": "Turks and Caicos Islands", "currency_code": "USD"}, {"country": "Tuvalu", "currency_code": "AUD"}, {"country": "Uganda", "currency_code": "UGX"}, {"country": "Ukraine", "currency_code": "UAH"}, {"country": "United Arab Emirates", "currency_code": "AED"}, {"country": "United Kingdom", "currency_code": "GBP"}, {"country": "United States", "currency_code": "USD"}, {"country": "United States Minor Outlying Islands", "currency_code": "USD"}, {"country": "Uruguay", "currency_code": "UYU"}, {"country": "Uzbekistan", "currency_code": "UZS"}, {"country": "Vanuatu", "currency_code": "VUV"}, {"country": "Venezuela", "currency_code": "VEF"}, {"country": "Vietnam", "currency_code": "VND"}, {"country": "Virgin Islands, British", "currency_code": "USD"}, {"country": "Virgin Islands, U.S.", "currency_code": "USD"}, {"country": "Wales", "currency_code": "GBP"}, {"country": "Wallis and Futuna", "currency_code": "XPF"}, {"country": "Western Sahara", "currency_code": "MAD"}, {"country": "Yemen", "currency_code": "YER"}, {"country": "Yugoslavia", "currency_code": null}, {"country": "Zambia", "currency_code": "ZMW"}, {"country": "Zimbabwe", "currency_code": "ZWD"}, ] class Module(core.module.Module): @core.decorators.every(minutes=5) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.price)) self.__data = [] src = self.parameter("source", DEFAULT_SRC) if src == "auto": self.__base = self.find_local_currency() else: self.__base = src self.__symbols = [] for d in util.format.aslist(self.parameter("destination", DEFAULT_DEST)): if d == "auto": new = self.find_local_currency() else: new = d if new != self.__base: self.__symbols.append(new) def price(self, widget): if len(self.__data) == 0: return "?" rates = [] for sym, rate in self.__data: rate_float = float(rate.replace(",", "")) if format_currency: rates.append(format_currency(rate_float, sym)) else: rate = self.fmt_rate(rate) rates.append("{}{}".format(rate, SYMBOL[sym] if sym in SYMBOL else sym)) basefmt = "{}".format(self.parameter("sourceformat", "{}={}")) ratefmt = "{}".format(self.parameter("destinationdelimiter", "=")) if format_currency: base_val = format_currency(1, self.__base) else: base_val = "1{}".format( SYMBOL[self.__base] if self.__base in SYMBOL else self.__base ) return basefmt.format(base_val, ratefmt.join(rates)) def update(self): self.__data = [] for symbol in self.__symbols: url = API_URL.format(self.__base, symbol) try: response = requests.get(url).json() self.__data.append((symbol, response["data"]["exchangeRate"])) except Exception: pass def find_local_currency(self): """Use geolocation lookup to find local currency""" try: country = util.location.country() currency_map = load_country_to_currency() return currency_map.get(country, DEFAULT_SRC) except: return DEFAULT_SRC def fmt_rate(self, rate): float_rate = float(rate.replace(",", "")) if not 0.01 < float_rate < 100: ret = rate else: ret = "%.3g" % float_rate return ret # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/datetimetz.py000066400000000000000000000070661450256260500261550ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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 * datetz.format : alias for datetimetz.format * timetz.format : alias for datetimetz.format * timetz.timezone : alias for datetimetz.timezone * datetimetz.locale : locale to use rather than the system default * datetz.locale : alias for datetimetz.locale * timetz.locale : alias for datetimetz.locale * timetz.timezone : alias for datetimetz.timezone contributed by `frankzhao `_ - many thanks! """ from __future__ import absolute_import import datetime import locale import logging import pytz import tzlocal import core.module import core.widget import core.input import util.format def default_format(module): default = "%x %X %Z" if module == "datetz": default = "%x %Z" if module == "timetz": default = "%X %Z" return default class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.get_time)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_tz) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.prev_tz) self.__fmt = self.parameter("format", self.default_format()) default_timezone = "" try: default_timezone = tzlocal.get_localzone().zone except Exception as e: logging.error("unable to get default timezone: {}".format(str(e))) try: self._timezones = util.format.aslist( self.parameter("timezone", default_timezone) ) except: self._timezones = [default_timezone] self._current_tz = 0 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")) def default_format(self): return "%x %X %Z" def get_time(self, widget): try: try: tz = pytz.timezone(self._timezones[self._current_tz].strip()) retval = ( datetime.datetime.now(tz=tzlocal.get_localzone()) .astimezone(tz) .strftime(self.__fmt) ) except pytz.exceptions.UnknownTimeZoneError: retval = "[Unknown timezone: {}]".format( self._timezones[self._current_tz].strip() ) except Exception as e: logging.error("unable to get time: {}".format(str(e))) retval = "[n/a]" enc = locale.getpreferredencoding() if hasattr(retval, "decode"): return retval.decode(enc) return retval def next_tz(self, event): next_timezone = self._current_tz + 1 if next_timezone >= len(self._timezones): next_timezone = 0 # wraparound self._current_tz = next_timezone def prev_tz(self, event): previous_timezone = self._current_tz - 1 if previous_timezone < 0: previous_timezone = 0 # wraparound self._current_tz = previous_timezone # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/datetz.py000066400000000000000000000010021450256260500252560ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current date and time. Parameters: * date.format: strftime()-compatible formatting string * date.locale: locale to use rather than the system default """ import core.decorators from .datetimetz import Module class Module(Module): @core.decorators.every(hours=1) def __init__(self, config, theme): super().__init__(config, theme) def default_format(self): return "%x %Z" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/deadbeef.py000066400000000000000000000125541450256260500255200ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current song being played in DeaDBeeF and provides some media control bindings. Left click toggles pause, scroll up skips the current song, scroll down returns to the previous song. Parameters: * deadbeef.format: Format string (defaults to '{artist} - {title}') Available values are: {artist}, {title}, {album}, {length}, {trackno}, {year}, {comment}, {copyright}, {time} This is deprecated, but much simpler. * deadbeef.tf_format: A foobar2000 title formatting-style format string. These can be much more sophisticated than the standard format strings. This is off by default, but specifying any tf_format will enable it. If both deadbeef.format and deadbeef.tf_format are specified, deadbeef.tf_format takes priority. * deadbeef.tf_format_if_stopped: Controls whether or not the tf_format format string should be displayed even if no song is paused or playing. This could be useful if you want to implement your own stop strings with the built in logic. Any non- null value will enable this (by default the module will hide itself when the player is stopped). * deadbeef.previous: Change binding for previous song (default is left click) * deadbeef.next: Change binding for next song (default is right click) * deadbeef.pause: Change binding for toggling pause (default is middle click) Available options for deadbeef.previous, deadbeef.next and deadbeef.pause are: LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN contributed by `joshbarrass `_ - many thanks! """ import sys import subprocess import logging import core.module import core.widget import core.input import core.decorators import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.deadbeef)) 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._song = "" self._format = self.parameter("format", "{artist} - {title}") self._tf_format = self.parameter("tf_format", "") self._show_tf_when_stopped = util.format.asbool( self.parameter("tf_format_if_stopped", False) ) prev_button = self.parameter("previous", "LEFT_CLICK") next_button = self.parameter("next", "RIGHT_CLICK") pause_button = self.parameter("pause", "MIDDLE_CLICK") self.now_playing = "deadbeef --nowplaying %a;%t;%b;%l;%n;%y;%c;%r;%e" self.now_playing_tf = "deadbeef --nowplaying-tf " cmd = "deadbeef " core.input.register(self, button=buttons[prev_button], cmd=cmd + "--prev") core.input.register(self, button=buttons[next_button], cmd=cmd + "--next") core.input.register( self, button=buttons[pause_button], cmd=cmd + "--play-pause" ) # modify the tf_format if we don't want it to show on stop # this adds conditions to the query itself, rather than # polling to see if deadbeef is running # doing this reduces the number of calls we have to make if self._tf_format and not self._show_tf_when_stopped: self._tf_format = "$if($or(%isplaying%,%ispaused%),{query})".format( query=self._tf_format ) @core.decorators.scrollable def deadbeef(self, widget): return self.string_song def hidden(self): return self.string_song == "" def update(self): widgets = self.widgets() try: if self._tf_format == "": # no tf format set, use the old style return self.update_standard(widgets) return self.update_tf(widgets) except Exception as e: logging.exception(e) self._song = "error" def update_tf(self, widgets): ## ensure that deadbeef is actually running ## easiest way to do this is to check --nowplaying for ## the string 'nothing' if util.cli.execute(self.now_playing) == "nothing": self._song = "" return ## perform the actual query -- these can be much more sophisticated data = util.cli.execute(self.now_playing_tf + '"'+self._tf_format+'"') self._song = data def update_standard(self, widgets): data = util.cli.execute(self.now_playing) if data == "nothing": self._song = "" else: data = data.split(";") self._song = self._format.format( artist=data[0], title=data[1], album=data[2], length=data[3], trackno=data[4], year=data[5], comment=data[6], copyright=data[7], time=data[8], ) @property def string_song(self): """\ Returns the current song as a string, either as a unicode() (Python < 3) or a regular str() (Python >= 3) """ if sys.version_info.major < 3: return unicode(self._song) return str(self._song) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/deezer.py000066400000000000000000000061511450256260500252530ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current song being played Requires the following library: * python-dbus Parameters: * deezer.format: Format string (defaults to '{artist} - {title}') Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} * deezer.previous: Change binding for previous song (default is left click) * deezer.next: Change binding for next song (default is right click) * deezer.pause: Change binding for toggling pause (default is middle click) Available options for deezer.previous, deezer.next and deezer.pause are: LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN contributed by `wwmoraes `_ - many thanks! """ import dbus import core.module import core.widget import core.input class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.deezer)) 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._song = "" 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") cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.deezer \ /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") def deezer(self, widget): return str(self._song) def hidden(self): return str(self._song) == "" def update(self): try: bus = dbus.SessionBus() deezer = bus.get_object( "org.mpris.MediaPlayer2.deezer", "/org/mpris/MediaPlayer2" ) deezer_iface = dbus.Interface(deezer, "org.freedesktop.DBus.Properties") props = deezer_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") playback_status = str( deezer_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 "", ) except Exception: self._song = "" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/dnf.py000066400000000000000000000040721450256260500245440ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays DNF package update information (///) Requires the following executable: * dnf """ import core.event import core.module import core.widget import core.decorators import util.cli 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"]: result.append(str(widget.get(t, 0))) 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) def state(self, widget): cnt = 0 for t in ["security", "bugfixes", "enhancements", "other"]: cnt += widget.get(t, 0) if cnt == 0: return "good" if cnt > 50 or widget.get("security", 0) > 0: return "critical" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/docker_ps.py000066400000000000000000000023571450256260500257520ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Displays the number of docker containers running Requires the following python packages: * docker contributed by `jlopezzarza `_ - many thanks! """ import docker from requests.exceptions import ConnectionError import core.module import core.widget import core.decorators class Module(core.module.Module): @core.decorators.every(seconds=5) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.docker_info)) self.__info = "" def state(self, widget): state = [] if self.__info == "OK - 0": state.append("warning") elif self.__info in ["n/a", "off"]: state.append("critical") return state def docker_info(self, widget): try: cli = docker.DockerClient(base_url="unix://var/run/docker.sock") cli.ping() self.__info = "OK - {}".format( len(cli.containers.list(filters={"status": "running"})) ) except ConnectionError: self.__info = "off" except Exception: self.__info = "n/a" return self.__info # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/dunst.py000066400000000000000000000020361450256260500251300ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Toggle dunst notifications. contributed by `eknoes `_ - 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("")) self._paused = False # Make sure that dunst is currently not paused util.cli.execute("killall -s SIGUSR2 dunst", ignore_errors=True) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status) def toggle_status(self, event): self._paused = not self._paused try: if self._paused: util.cli.execute("killall -s SIGUSR1 dunst") else: util.cli.execute("killall -s SIGUSR2 dunst") except: self._paused = not self._paused # toggling failed def state(self, widget): if self._paused: return ["muted", "warning"] return ["unmuted"] bumblebee-status-2.2.0/bumblebee_status/modules/contrib/dunstctl.py000066400000000000000000000032701450256260500256340ustar00rootroot00000000000000# 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" bumblebee-status-2.2.0/bumblebee_status/modules/contrib/emerge_status.py000066400000000000000000000066401450256260500266470ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/gcalendar.py000066400000000000000000000150171450256260500257160ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/getcrypto.py000066400000000000000000000056061450256260500260210ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the price of a cryptocurrency. Requires the following python packages: * requests Parameters: * getcrypto.interval: Interval in seconds for updating the price, default is 120, less than that will probably get your IP banned. * getcrypto.getbtc: 0 for not getting price of BTC, 1 for getting it (default). * getcrypto.geteth: 0 for not getting price of ETH, 1 for getting it (default). * getcrypto.getltc: 0 for not getting price of LTC, 1 for getting it (default). * getcrypto.getcur: Set the currency to display the price in, usd is the default. contributed by `Ryunaq `_ - many thanks! """ import requests from requests.exceptions import RequestException import time import core.module import core.widget import core.input import core.decorators import util.format def getfromkrak(coin, currency): abbrev = { "Btc": ["xbt", "XXBTZ"], "Eth": ["eth", "XETHZ"], "Ltc": ["ltc", "XLTCZ"], } data = abbrev.get(coin, None) if not data: return epair = "{}{}".format(data[0], currency) tickname = "{}{}".format(data[1], currency.upper()) try: krakenget = requests.get( "https://api.kraken.com/0/public/Ticker?pair=" + epair ).json() except (RequestException, Exception): return "No connection" if not "result" in krakenget: return "No data" kethusdask = float(krakenget["result"][tickname]["a"][0]) kethusdbid = float(krakenget["result"][tickname]["b"][0]) return coin + ": " + str((kethusdask + kethusdbid) / 2)[0:6] class Module(core.module.Module): @core.decorators.every(minutes=30) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.curprice)) self.__curprice = "" self.__getbtc = util.format.asbool(self.parameter("getbtc", True)) self.__geteth = util.format.asbool(self.parameter("geteth", True)) self.__getltc = util.format.asbool(self.parameter("getltc", True)) self.__getcur = self.parameter("getcur", "usd") core.input.register( self, button=core.input.LEFT_MOUSE, cmd="xdg-open https://cryptowat.ch/" ) def curprice(self, widget): return self.__curprice def update(self): currency = self.__getcur btcprice, ethprice, ltcprice = "", "", "" if self.__getbtc: btcprice = getfromkrak("Btc", currency) if self.__geteth: ethprice = getfromkrak("Eth", currency) if self.__getltc: ltcprice = getfromkrak("Ltc", currency) self.__curprice = ( btcprice + " " * (self.__getbtc * self.__geteth) + ethprice + " " * (self.__getltc * max(self.__getbtc, self.__geteth)) + ltcprice ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/github.py000066400000000000000000000064551450256260500252660ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """ Displays the unread GitHub notifications count for a GitHub user using the following reasons: * 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 Parameters: * github.token: GitHub user access token, the token needs to have the 'notifications' scope. * github.interval: Interval in minutes between updates, default is 5. * github.reasons: Comma separated reasons to be parsed (e.g.: github.reasons=mention,team_mention,review_requested) contributed by: * v1 - `yvesh `_ - many thanks! * v2 - `cristianmiranda `_ - many thanks! """ import shutil import requests import core.module import core.widget import core.decorators import core.input import util.format class Module(core.module.Module): @core.decorators.every(minutes=5) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.github)) self.background = True self.__count = 0 self.__label = "" self.__requests = requests.Session() self.__requests.headers.update( {"Authorization": "token {}".format(self.parameter("token", ""))} ) self.__reasons = [] reasons = self.parameter("reasons", "") if reasons: self.__reasons = util.format.aslist(reasons) cmd = "xdg-open" if not shutil.which(cmd): cmd = "x-www-browser" core.input.register( self, button=core.input.LEFT_MOUSE, cmd="{} https://github.com/notifications".format(cmd), ) def github(self, _): return str(self.__label) def update(self): try: url = "https://api.github.com/notifications" notifications = self.__requests.get(url) total = self.__getTotalUnreadNotificationsCount(notifications) self.__count = total self.__label = str(total) counts = [] if len(self.__reasons) > 0: for reason in self.__reasons: unread = self.__getUnreadNotificationsCountByReason( notifications, reason ) counts.append(str(unread)) self.__label += " - " self.__label += "/".join(counts) except Exception as err: self.__label = "n/a" def __getUnreadNotificationsCountByReason(self, notifications, reason): return len( list( filter( lambda notification: notification["unread"] and notification["reason"] == reason, notifications.json(), ) ) ) def __getTotalUnreadNotificationsCount(self, notifications): return len( list( filter( lambda notification: notification["unread"], notifications.json() ) ) ) def state(self, widget): state = [] if self.__count > 0: state.append("warning") return state # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/gitlab.py000066400000000000000000000045251450256260500252420ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/gpmdp.py000066400000000000000000000033031450256260500251000ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays information about the current song in Google Play music player. Requires the following executable: * gpmdp-remote contributed by `TheEdgeOfRage `_ - many thanks! """ import core.module import core.widget import core.input import util.cli class Module(core.module.Module): def __init__(self, config, theme): widgets = [ core.widget.Widget(name="gpmdp.prev"), core.widget.Widget(name="gpmdp.main", full_text=self.description), core.widget.Widget(name="gpmdp.next"), ] super().__init__(config, theme, widgets) 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.__status = None self.__tags = None def description(self, widget): return self.__tags if self.__tags else "n/a" def update(self): self.__load_song() def state(self, widget): if widget.name == "gpmdp.prev": return "prev" if widget.name == "gpmdp.next": return "next" return self.__status def __load_song(self): info = util.cli.execute("gpmdp-remote current", ignore_errors=True) status = util.cli.execute("gpmdp-remote status", ignore_errors=True) self.__status = status.split("\n")[0].lower() self.__tags = info.split("\n")[0] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/hddtemp.py000066400000000000000000000051531450256260500254230ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Fetch hard drive temperature data from a hddtemp daemon that runs on localhost and default port (7634) contributed by `somospocos `_ - many thanks! """ import socket import core.module import core.widget HOST = "localhost" PORT = 7634 CHUNK_SIZE = 1024 RECORD_SIZE = 5 SEPARATOR = "|" class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.hddtemps)) self.__hddtemps = self.__get_hddtemps() def hddtemps(self, _): return self.__hddtemps def __fetch_data(self): """fetch data from hddtemp service""" try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((HOST, PORT)) data = "" while True: chunk = sock.recv(CHUNK_SIZE) if chunk: data += str(chunk) else: break return data except (AttributeError, socket.error) as e: pass @staticmethod def __get_parts(data): """ split data using | separator and remove first item (because the first item is empty) """ parts = data.split("|")[1:] return parts @staticmethod def __partition_parts(parts): """ partition parts: one device record is five (5) items """ per_disk = [ parts[i : i + RECORD_SIZE] for i in range(len(parts))[::RECORD_SIZE] ] return per_disk @staticmethod def __get_name_and_temp(device_record): """ get device name (without /dev part, to save space on bar) and temperature (in °C) as tuple """ device_name = device_record[0].split("/")[-1] device_temp = device_record[2] return (device_name, device_temp) @staticmethod def __get_hddtemp(device_record): name, temp = device_record hddtemp = "{}+{}°C".format(name, temp) return hddtemp def __get_hddtemps(self): data = self.__fetch_data() if data is None: return "n/a" parts = self.__get_parts(data) per_disk = self.__partition_parts(parts) names_and_temps = [self.__get_name_and_temp(x) for x in per_disk] hddtemps = [self.__get_hddtemp(x) for x in names_and_temps] return SEPARATOR.join(hddtemps) def update(self): self.__hddtemps = self.__get_hddtemps() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/hostname.py000066400000000000000000000011621450256260500256100ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the system hostname. contributed by `varkokonyi `_ - many thanks! """ import platform import core.module import core.widget import core.decorators class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.output)) self.__hname = "" def output(self, _): return self.__hname + " " + "\uf233" def update(self): self.__hname = platform.node() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/http_status.py000066400000000000000000000034321450256260500263560ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Display HTTP status code Parameters: * http__status.label: Prefix label (optional) * http__status.target: Target to retrieve the HTTP status from * http__status.expect: Expected HTTP status contributed by `valkheim `_ - many thanks! """ from requests import head import psutil import core.module import core.widget import core.decorators class Module(core.module.Module): UNK = "UNK" @core.decorators.every(seconds=30) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.output)) self.__label = self.parameter("label") self.__target = self.parameter("target") self.__expect = self.parameter("expect", "200") def labelize(self, s): if self.__label is None: return s return "{}: {}".format(self.__label, s) def getStatus(self): try: res = head(self.__target) except Exception as e: print(e) return self.UNK else: status = str(res.status_code) return status def getOutput(self): if self.__status == self.__expect: return self.labelize(self.__status) else: reason = " != {}".format(self.__expect) return self.labelize("{}{}".format(self.__status, reason)) def output(self, widget): return self.__output def update(self): self.__status = self.getStatus() self.__output = self.getOutput() def state(self, widget): if self.__status == self.UNK: return "warning" if self.__status != self.__expect: return "critical" return self.__output # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/indicator.py000066400000000000000000000035351450256260500257540ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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') contributed by `freed00m `_ - many thanks! """ import core.module import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self.__include = tuple( filter( len, util.format.aslist(self.parameter("include", "NumLock,CapsLock")) ) ) self.__signalType = ( self.parameter("signaltype") if not self.parameter("signaltype") is None else "warning" ) def update(self): status_line = "" for line in ( util.cli.execute("xset q", ignore_errors=True).replace(" ", "").split("\n") ): if "capslock" in line.lower(): status_line = line break for indicator in self.__include: widget = self.widget(indicator) if not widget: widget = self.add_widget(name=indicator, full_text=indicator) widget.set( "status", True if "{}:on".format(indicator.lower()) in status_line.lower() else False, ) def state(self, widget): states = [] if widget.get("status", False): states.append(self.__signalType) else: states.append("normal") return states # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/kernel.py000066400000000000000000000010401450256260500252450ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Shows Linux kernel version information contributed by `pierre87 `_ - many thanks! """ import platform import core.module import core.widget import core.decorators class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.full_text)) def full_text(self, widgets): return platform.release() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/layout-xkbswitch.py000066400000000000000000000021451450256260500273150ustar00rootroot00000000000000"""Displays and changes the current keyboard layout Requires the following executable: * xkb-switch contributed by `somospocos `_ - 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(seconds=60) 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) self.__current_layout = self.__get_current_layout() def current_layout(self, _): return self.__current_layout def next_keymap(self, event): util.cli.execute("xkb-switch -n", ignore_errors=True) def __get_current_layout(self): try: res = util.cli.execute("xkb-switch") return res.split("\n")[0] except RuntimeError: return ["n/a"] def update(self): self.__current_layout = self.__get_current_layout() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/layout.py000066400000000000000000000043501450256260500253110ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays and changes the current keyboard layout Requires the following executable: * setxkbmap contributed by `Pseudonick47 `_ - 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(self.current_layout)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_keymap) def __next_keymap(self, event): self._set_keymap(1) def __prev_keymap(self, event): self._set_keymap(-1) def _set_keymap(self, rotation): layouts = self.get_layouts() if len(layouts) == 1: return # nothing to do layouts = layouts[rotation:] + layouts[:rotation] layout_list = [] variant_list = [] for l in layouts: tmp = l.split(":") layout_list.append(tmp[0]) variant_list.append(tmp[1] if len(tmp) > 1 else "") util.cli.execute( "setxkbmap -layout {} -variant {}".format( ",".join(layout_list), ",".join(variant_list) ), ignore_errors=True, ) def get_layouts(self): try: res = util.cli.execute("setxkbmap -query") except RuntimeError: return ["n/a"] layouts = [] variants = [] for line in res.split("\n"): if not line: continue if "layout" in line: layouts = line.split(":")[1].strip().split(",") if "variant" in line: variants = line.split(":")[1].strip().split(",") result = [] for idx, layout in enumerate(layouts): if len(variants) > idx and variants[idx]: layout = "{}:{}".format(layout, variants[idx]) result.append(layout) return result if len(result) > 0 else ["n/a"] def current_layout(self, widget): layouts = self.get_layouts() return layouts[0] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/layout_xkbswitch.py000077700000000000000000000000001450256260500332632layout-xkbswitch.pyustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/contrib/libvirtvms.py000066400000000000000000000014611450256260500261750ustar00rootroot00000000000000"""Displays count of running libvirt VMs. Required the following python packages: * libvirt contributed by `maxpivo `_ - many thanks! """ import sys import libvirt import core.module import core.widget import core.input import core.decorators class Module(core.module.Module): @core.decorators.every(seconds=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.status)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="virt-manager") def status(self, _): conn = libvirt.openReadOnly(None) if conn == None: return "Failed to open connection to the hypervisor" return "VMs %s" % (conn.numOfDomains()) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/messagereceiver.py000066400000000000000000000053631450256260500271520ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/mocp.py000066400000000000000000000034101450256260500247260ustar00rootroot00000000000000# pylint: disable=C0111,R0903 # -*- coding: utf-8 -*- """Displays information about the current song in mocp. Left click toggles play/pause. Right click toggles shuffle. Requires the following executable: * mocp Parameters: * mocp.format: Format string for the song information. Replace string sequences with the actual information: * %state State * %file File * %title Title, includes track, artist, song title and album * %artist Artist * %song SongTitle * %album Album * %tt TotalTime * %tl TimeLeft * %ts TotalSec * %ct CurrentTime * %cs CurrentSec * %b Bitrate * %r Sample rate contributed by `chrugi `_ - 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(self.description)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="mocp -G") core.input.register(self, button=core.input.RIGHT_MOUSE, cmd="mocp -t shuffle") self.__format = self.parameter("format", "%state %artist - %song | %ct/%tt") self.__running = False def description(self, widget): return self.__info if self.__running == True else "Music On Console Player" def update(self): self.__load_song() def __load_song(self): try: self.__info = util.cli.execute("mocp -Q '{}'".format(self.__format)).strip() self.__running = True except RuntimeError: self.__running = False # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/mpd.py000066400000000000000000000162601450256260500245570ustar00rootroot00000000000000# pylint: disable=C0111,R0903 # -*- coding: utf-8 -*- """Displays information about the current song in mpd. Requires the following executable: * mpc Parameters: * mpd.format: Format string for the song information. Supported tags (see `man mpc` for additional information) * {name} * {artist} * {album} * {albumartist} * {comment} * {composer} * {date} * {originaldate} * {disc} * {genre} * {performer} * {title} * {track} * {time} * {file} * {id} * {prio} * {mtime} * {mdate} Additional tags: * {position} - position of currently playing song not to be confused with %position% mpc tag * {duration} - duration of currently playing song * {file1} - song file name without path prefix if {file} = '/foo/bar.baz', then {file1} = 'bar.baz' * {file2} - song file name without path prefix and extension suffix 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! """ from collections import defaultdict import string import os import core.module import core.input import core.decorators import util.cli class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self._layout = self.parameter( "layout", "mpd.prev mpd.main mpd.next mpd.shuffle mpd.repeat" ) self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}") self._status = None self._shuffle = False 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")) # Create widgets widget_map = {} for widget_name in self._layout.split(): widget = self.add_widget(name=widget_name) if widget_name == "mpd.prev": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "mpc prev" + self._hostcmd, } elif widget_name == "mpd.main": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "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, "cmd": "mpc next" + self._hostcmd, } elif widget_name == "mpd.shuffle": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "mpc random" + self._hostcmd, } elif widget_name == "mpd.repeat": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": "mpc repeat" + self._hostcmd, } else: raise KeyError( "The mpd module does not support a {widget_name!r} widget".format( widget_name=widget_name ) ) # Register input callbacks for widget, callback_options in widget_map.items(): core.input.register(widget, **callback_options) def hidden(self): return self._status is None @core.decorators.scrollable 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() def state(self, widget): if widget.name == "mpd.shuffle": return "shuffle-on" if self._shuffle else "shuffle-off" if widget.name == "mpd.repeat": return "repeat-on" if self._repeat else "repeat-off" if widget.name == "mpd.prev": return "prev" if widget.name == "mpd.next": return "next" return self._status def _load_song(self): info = "" tags = [ "name", "artist", "album", "albumartist", "comment", "composer", "date", "originaldate", "disc", "genre", "performer", "title", "track", "time", "file", "id", "prio", "mtime", "mdate", ] joinedtags = "\n".join(["tag {0} %{0}%".format(tag) for tag in tags]) info = util.cli.execute( 'mpc -f "{}"{}'.format(joinedtags, self._hostcmd), ignore_errors=True ) self._tags = defaultdict(lambda: "") self._status = None for line in info.split("\n"): if line.startswith("[playing]"): self._status = "playing" elif line.startswith("[paused]"): self._status = "paused" if line.startswith("["): timer = line.split()[2] position = timer.split("/")[0] dur = timer.split("/")[1] duration = dur.split(" ")[0] self._tags.update({"position": position}) self._tags.update({"duration": duration}) if line.startswith("volume"): value = line.split(" ", 2)[1:] for option in value: if option.startswith("repeat: on"): self._repeat = True elif option.startswith("repeat: off"): self._repeat = False elif option.startswith("random: on"): self._shuffle = True elif option.startswith("random: off"): self._shuffle = False if line.startswith("tag"): key, value = line.split(" ", 2)[1:] self._tags.update({key: value}) if key == "file": self._tags.update({"file1": os.path.basename(value)}) self._tags.update( {"file2": os.path.splitext(os.path.basename(value))[0]} ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/network.py000066400000000000000000000101751450256260500254670ustar00rootroot00000000000000""" 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/network_traffic.py000066400000000000000000000055761450256260500271760ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Displays network traffic * No extra configuration needed contributed by `izn `_ - many thanks! """ import psutil import netifaces import core.module import core.widget import util.format WIDGET_NAME = "network_traffic" class Module(core.module.Module): def __init__(self, config, theme): widgets = [ core.widget.Widget( name="{0}.rx".format(WIDGET_NAME), full_text=self.download_rate, ), core.widget.Widget( name="{0}.tx".format(WIDGET_NAME), full_text=self.upload_rate, ), ] super().__init__(config, theme, widgets) self.widgets()[0].set("theme.minwidth", "0000000KiB/s") self.widgets()[1].set("theme.minwidth", "0000000KiB/s") try: self._bandwidth = BandwidthInfo() self._rate_recv = 0 self._rate_sent = 0 self._bytes_recv = self._bandwidth.bytes_recv() self._bytes_sent = self._bandwidth.bytes_sent() except Exception: """ We do not want do explode anything """ pass def state(self, widget): """Return the widget state""" if widget.name == "{}.rx".format(WIDGET_NAME): return "rx" elif widget.name == "{}.tx".format(WIDGET_NAME): return "tx" return None def update(self): try: bytes_recv = self._bandwidth.bytes_recv() bytes_sent = self._bandwidth.bytes_sent() self._rate_recv = bytes_recv - self._bytes_recv self._rate_sent = bytes_sent - self._bytes_sent self._bytes_recv, self._bytes_sent = bytes_recv, bytes_sent except Exception: """ We do not want do explode anything """ pass def download_rate(self, _): return "{}/s".format(util.format.byte(self._rate_recv)) def upload_rate(self, _): return "{}/s".format(util.format.byte(self._rate_sent)) class BandwidthInfo(object): """Get received/sent bytes from network adapter""" def bytes_recv(self): """Return received bytes""" return self.bandwidth().bytes_recv def bytes_sent(self): """Return sent bytes""" return self.bandwidth().bytes_sent def bandwidth(self): """Return bandwidth information""" io_counters = self.io_counters() return io_counters[self.default_network_adapter()] @classmethod def default_network_adapter(cls): """Return default active network adapter""" gateway = netifaces.gateways()["default"] return gateway[netifaces.AF_INET][1] @classmethod def io_counters(cls): """Return IO counters""" return psutil.net_io_counters(pernic=True) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/notmuch_count.py000066400000000000000000000024361450256260500266640ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the result of a notmuch count query default : unread emails which path do not contained 'Trash' (notmuch count 'tag:unread AND NOT path:/.*Trash.*/') Parameters: * notmuch_count.query: notmuch count query to show result Errors: if the notmuch query failed, the shown value is -1 Dependencies: notmuch (https://notmuchmail.org/) contributed by `abdoulayeYATERA `_ - many thanks! """ import os 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.__notmuch_count_query = self.parameter( "query", "tag:unread AND NOT path:/.*Trash.*/" ) def output(self, widget): return self.__notmuch_count def state(self, widgets): if self.__notmuch_count == 0: return "empty" return "items" def update(self): try: self.__notmuch_count = util.cli.execute( "notmuch count {}".format(self.__notmuch_count_query) ).strip() except Exception: self.__notmuch_count = "n/a" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/nvidiagpu.py000066400000000000000000000061161450256260500257640ustar00rootroot00000000000000# -*- coding: utf-8 -*- """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} 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 import core.widget import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.utilization)) self.__utilization = "Not found: 0 0/0" def utilization(self, widget): return self.__utilization def hidden(self): return "not found" in self.__utilization def update(self): sp = util.cli.execute("nvidia-smi -q", ignore_errors=True) title = "" usedMem = "" totalMem = "" temp = "" name = "not found" clockMem = "" clockGpu = "" fanspeed = "" gpuUsagePct = "" memIoPct = "" memUsage = "not found" for item in sp.split("\n"): try: key, val = item.split(":") key, val = key.strip(), val.strip() if title == "Clocks": if key == "Graphics": clockGpu = val.split(" ")[0] elif key == "Memory": clockMem = val.split(" ")[0] if title == "FB Memory Usage": if key == "Total": totalMem = val.split(" ")[0] elif key == "Used": usedMem = val.split(" ")[0] elif key == "GPU Current Temp": temp = val.split(" ")[0] elif key == "Product Name": 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" ) self.__utilization = str_format.format( name=name, temp=temp, mem_used=usedMem, mem_total=totalMem, clock_gpu=clockGpu, clock_mem=clockMem, fanspeed=fanspeed, gpu_usage_pct=gpuUsagePct, mem_io_pct=memIoPct, mem_usage_pct=memUsage, ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/octoprint.py000066400000000000000000000210221450256260500260100ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the Octorrint 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) * octoprint.webcam : Set to True if a webcam is connected (default: False) contributed by `bbernhard `_ - many thanks! """ import urllib import logging import threading import queue import tkinter as tk from io import BytesIO from PIL import Image, ImageTk import requests import simplejson import core.module import core.widget import core.input def get_frame(url): img_bytes = b"" stream = urllib.request.urlopen(url) while True: img_bytes += stream.read(1024) a = img_bytes.find(b"\xff\xd8") b = img_bytes.find(b"\xff\xd9") if a != -1 and b != -1: jpg = img_bytes[a : b + 2] img_bytes = img_bytes[b + 2 :] img = Image.open(BytesIO(jpg)) return img return None class WebcamImagesWorker(threading.Thread): def __init__(self, url, queue): threading.Thread.__init__(self) self.__url = url self.__queue = queue self.__running = True def run(self): while self.__running: img = get_frame(self.__url) self.__queue.put(img) def stop(self): self.__running = False class Module(core.module.Module): @core.decorators.every(seconds=5) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.octoprint_status)) self.__octoprint_state = "Unknown" self.__octoprint_address = self.parameter("address", "") self.__octoprint_api_token = self.parameter("apitoken", "") self.__octoprint_webcam = self.parameter("webcam", False) self.__webcam_images_worker = None self.__webcam_image_url = self.__octoprint_address + "/webcam/?action=stream" self.__webcam_images_queue = None self.__printer_bed_temperature = "-" self.__tool1_temperature = "-" 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 ) return ( self.__octoprint_state + " | B: " + str(self.__printer_bed_temperature) + "°C" + " | T1: " + str(self.__tool1_temperature) + "°C" ) def __get(self, endpoint): url = self.__octoprint_address + "/api/" + endpoint headers = {"X-Api-Key": self.__octoprint_api_token} resp = requests.get(url, headers=headers) try: return resp.json(), resp.status_code except simplejson.errors.JSONDecodeError: return None, resp.status_code def __get_printer_bed_temperature(self): printer_info, status_code = self.__get("printer") if status_code == 200: return ( printer_info["temperature"]["bed"]["actual"], printer_info["temperature"]["bed"]["target"], ) return None, None def __get_octoprint_state(self): job_info, status_code = self.__get("job") return job_info["state"] if status_code == 200 else "Unknown" def __get_tool_temperatures(self): tool_temperatures = [] printer_info, status_code = self.__get("printer") if status_code == 200: temperatures = printer_info["temperature"] tool_id = 0 while True: try: tool = temperatures["tool" + str(tool_id)] tool_temperatures.append((tool["actual"], tool["target"])) except KeyError: break tool_id += 1 return tool_temperatures def update(self): try: self.__octoprint_state = self.__get_octoprint_state() actual_temp, _ = self.__get_printer_bed_temperature() if actual_temp is None: actual_temp = "-" self.__printer_bed_temperature = str(actual_temp) tool_temps = self.__get_tool_temperatures() if len(tool_temps) > 0: self.__tool1_temperature = tool_temps[0][0] else: self.__tool1_temperature = "-" except Exception as e: logging.exception("Couldn't get data") def __refresh_image(self, root, webcam_image, webcam_image_container): try: img = self.__webcam_images_queue.get() webcam_image = ImageTk.PhotoImage(img) webcam_image_container.config(image=webcam_image) except queue.Empty as e: pass except Exception as e: logging.exception("Couldn't refresh image") root.after(5, self.__refresh_image, root, webcam_image, webcam_image_container) def __refresh_temperatures( self, root, printer_bed_temperature_label, tools_temperature_label ): actual_bed_temp, target_bed_temp = self.__get_printer_bed_temperature() if actual_bed_temp is None: actual_bed_temp = "-" if target_bed_temp is None: target_bed_temp = "-" bed_temp = "Bed: " + str(actual_bed_temp) + "/" + str(target_bed_temp) + " °C" printer_bed_temperature_label.config(text=bed_temp) tool_temperatures = self.__get_tool_temperatures() tools_temp = "Tools: " if len(tool_temperatures) == 0: tools_temp += "-/- °C" else: for i, tool_temperature in enumerate(tool_temperatures): tools_temp += ( str(tool_temperature[0]) + "/" + str(tool_temperature[1]) + "°C" ) if i != len(tool_temperatures) - 1: tools_temp += "\t" tools_temperature_label.config(text=tools_temp) root.after( 500, self.__refresh_temperatures, root, printer_bed_temperature_label, tools_temperature_label, ) def __show_popup(self, widget): root = tk.Tk() root.attributes("-type", "dialog") root.title("Octoprint") frame = tk.Frame(root) if self.__octoprint_webcam: # load first image synchronous before popup is shown, otherwise tkinter isn't able to layout popup properly img = get_frame(self.__webcam_image_url) webcam_image = ImageTk.PhotoImage(img) webcam_image_container = tk.Button(frame, image=webcam_image) webcam_image_container.pack() self.__webcam_images_queue = queue.Queue() self.__webcam_images_worker = WebcamImagesWorker( self.__webcam_image_url, self.__webcam_images_queue ) self.__webcam_images_worker.start() else: logging.debug( "Not using webcam, as webcam is disabled. Enable with --webcam." ) frame.pack() temperatures_label = tk.Label(frame, text="Temperatures", font=("", 25)) temperatures_label.pack() printer_bed_temperature_label = tk.Label( frame, text="Bed: -/- °C", font=("", 15) ) printer_bed_temperature_label.pack() tools_temperature_label = tk.Label(frame, text="Tools: -/- °C", font=("", 15)) tools_temperature_label.pack() root.after(10, self.__refresh_image, root, webcam_image, webcam_image_container) root.after( 500, self.__refresh_temperatures, root, printer_bed_temperature_label, tools_temperature_label, ) root.bind("", self.__on_close_popup) root.eval("tk::PlaceWindow . center") root.mainloop() def __on_close_popup(self, event): self.__webcam_images_queue = None self.__webcam_images_worker.stop() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/optman.py000066400000000000000000000013411450256260500252670ustar00rootroot00000000000000"""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" bumblebee-status-2.2.0/bumblebee_status/modules/contrib/pacman.py000066400000000000000000000041761450256260500252410ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays update information per repository for pacman. Parameters: * pacman.sum: If you prefer displaying updates with a single digit (defaults to 'False') Requires the following executables: * fakeroot * pacman contributed by `Pseudonick47 `_ - many thanks! """ import os import threading import core.module import core.widget import core.decorators import util.cli import util.format from bumblebee_status.discover import utility # list of repositories. # the last one should always be other repos = ["core", "extra", "community", "multilib", "testing", "other"] def get_pacman_info(widget, path): cmd = utility("pacman-updates") result = util.cli.execute(cmd, ignore_errors=True) count = len(repos) * [0] for line in result.splitlines(): if line.startswith(("http", "rsync")): for i in range(len(repos) - 1): if "/" + repos[i] + "/" in line: count[i] += 1 break else: result[-1] += 1 for i in range(len(repos)): widget.set(repos[i], count[i]) 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)) def updates(self, widget): if util.format.asbool(self.parameter("sum")): return str(sum(map(lambda x: widget.get(x, 0), repos))) return "/".join(map(lambda x: str(widget.get(x, 0)), repos)) def update(self): path = os.path.dirname(os.path.abspath(__file__)) thread = threading.Thread(target=get_pacman_info, args=(self.widget(), path)) thread.start() def state(self, widget): weightedCount = sum( map(lambda x: (len(repos) - x[0]) * widget.get(x[1], 0), enumerate(repos)) ) if weightedCount < 10: return "good" return self.threshold_state(weightedCount, 100, 150) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/pamixer.py000066400000000000000000000046541450256260500254500ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/persian_date.py000066400000000000000000000014151450256260500264310ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/pihole.py000066400000000000000000000065651450256260500252660ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the pi-hole status (up/down) together with the number of ads that were blocked today 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) contributed by `bbernhard `_ - many thanks! """ import requests import logging import core.module import core.widget import core.input class Module(core.module.Module): @core.decorators.every(minutes=1) def __init__(self, config, theme): 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_status = None self._ads_blocked_today = "-" self.update_pihole_status() core.input.register( self, button=core.input.LEFT_MOUSE, cmd=self.toggle_pihole_status ) def pihole_status(self, widget): if self._pihole_status is None: return "pi-hole unknown" return "pi-hole {}".format( "up {} blocked".format(self._ads_blocked_today) if self._pihole_status else "down" ) def update_pihole_status(self): try: data = requests.get( self._pihole_address + "/admin/api.php?summary&auth=" + self._pihole_secret ).json() self._pihole_status = True if data["status"] == "enabled" else False self._ads_blocked_today = data["ads_blocked_today"] except Exception as e: self._pihole_status = None def toggle_pihole_status(self, widget): if self._pihole_status is not None: try: req = None if self._pihole_status: req = requests.get( self._pihole_address + "/admin/api.php?disable&auth=" + self._pihole_secret ) else: req = requests.get( self._pihole_address + "/admin/api.php?enable&auth=" + self._pihole_secret ) if req is not None: if req.status_code == 200: status = req.json()["status"] self._pihole_status = False if status == "disabled" else True except: pass def update(self): self.update_pihole_status() def state(self, widget): if self._pihole_status is None: return [] elif self._pihole_status: return ["enabled"] return ["disabled", "warning"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/pipewire.py000066400000000000000000000051311450256260500256160ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/playerctl.py000066400000000000000000000124051450256260500257730ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays information about the current song in vlc, audacious, bmp, xmms2, spotify and others 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, []) self.background = True self.__hide = util.format.asbool(self.parameter("hide", "false")); self.__hidden = self.__hide 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 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(" ") def __get_song(self): try: return str(util.cli.execute(self.__cmd + "metadata -f '" + self.__format + "'")).strip() except Exception as e: logging.exception(e) return " " bumblebee-status-2.2.0/bumblebee_status/modules/contrib/pomodoro.py000066400000000000000000000122061450256260500256310ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Display and run a Pomodoro timer. Left click to start timer, left click again to pause. Right click will cancel the timer. Parameters: * pomodoro.work: The work duration of timer in minutes (defaults to 25) * pomodoro.break: The break duration of timer in minutes (defaults to 5) * pomodoro.format: Timer display format with '%m' and '%s' for minutes and seconds (defaults to '%m:%s') Examples: '%m min %s sec', '%mm', '', 'timer' * pomodoro.notify: Notification command to run when timer ends/starts (defaults to nothing) 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) contributed by `martindoublem `_, inspired by `karthink `_ - many thanks! """ from __future__ import absolute_import import datetime from math import ceil 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(self.text)) # Parameters self.__work_period = int(self.parameter("work", 25)) self.__break_period = int(self.parameter("break", 5)) self.__time_format = self.parameter("format", "%m:%s") self.__notify_cmd = self.parameter("notify", "") # TODO: Handle time formats more gracefully. This is kludge. self.display_seconds_p = False self.display_minutes_p = False if "%s" in self.__time_format: self.display_seconds_p = True if "%m" in self.__time_format: self.display_minutes_p = True self.remaining_time = datetime.timedelta(minutes=self.__work_period) self.time = None self.pomodoro = {"state": "OFF", "type": ""} self.__text = self.remaining_time_str() + self.pomodoro["type"] core.input.register( self, button=core.input.LEFT_MOUSE, cmd=self.timer_play_pause ) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.timer_reset) def remaining_time_str(self): if self.display_seconds_p and self.display_minutes_p: minutes, seconds = divmod(self.remaining_time.seconds, 60) if not self.display_seconds_p: minutes = ceil(self.remaining_time.seconds / 60) seconds = 0 if not self.display_minutes_p: minutes = 0 seconds = self.remaining_time.seconds minutes = "{:2d}".format(minutes) seconds = "{:02d}".format(seconds) return self.__time_format.replace("%m", minutes).replace("%s", seconds) + " " def text(self, widget): return "{}".format(self.__text) def update(self): if self.pomodoro["state"] == "ON": timediff = datetime.datetime.now() - self.time if timediff.seconds >= 0: self.remaining_time -= timediff self.time = datetime.datetime.now() if self.remaining_time.total_seconds() <= 0: self.notify() if self.pomodoro["type"] == "Work": self.pomodoro["type"] = "Break" self.remaining_time = datetime.timedelta( minutes=self.__break_period ) elif self.pomodoro["type"] == "Break": self.pomodoro["type"] = "Work" self.remaining_time = datetime.timedelta(minutes=self.__work_period) self.__text = self.remaining_time_str() + self.pomodoro["type"] def notify(self): if self.__notify_cmd: util.cli.execute(self.__notify_cmd) def timer_play_pause(self, widget): if self.pomodoro["state"] == "OFF": self.pomodoro = {"state": "ON", "type": "Work"} self.remaining_time = datetime.timedelta(minutes=self.__work_period) self.time = datetime.datetime.now() elif self.pomodoro["state"] == "ON": self.pomodoro["state"] = "PAUSED" self.remaining_time -= datetime.datetime.now() - self.time self.time = datetime.datetime.now() elif self.pomodoro["state"] == "PAUSED": self.pomodoro["state"] = "ON" self.time = datetime.datetime.now() def timer_reset(self, widget): if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "PAUSED": self.pomodoro = {"state": "OFF", "type": ""} self.remaining_time = datetime.timedelta(minutes=self.__work_period) def state(self, widget): state = [] state.append(self.pomodoro["state"].lower()) if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "OFF": state.append(self.pomodoro["type"].lower()) return state # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/portage_status.py000066400000000000000000000050151450256260500270370ustar00rootroot00000000000000"""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() bumblebee-status-2.2.0/bumblebee_status/modules/contrib/prime.py000066400000000000000000000040341450256260500251070ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays and changes the current selected prime video card Left click will call 'sudo prime-select nvidia' Right click will call 'sudo prime-select nvidia' Running these commands without a password requires editing your sudoers file (always use visudo, it's very easy to make a mistake and get locked out of your computer!) sudo visudo -f /etc/sudoers.d/prime Then put a line like this in there: user ALL=(ALL) NOPASSWD: /usr/bin/prime-select If you can't figure out the sudoers thing, then don't worry, it's still really useful. 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 * prime-select contributed by `jeffeb3 `_ - 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(self.query)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__chooseNvidia) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__chooseIntel) self.nvidiastring = self.parameter("nvidiastring", "nv") self.intelstring = self.parameter("intelstring", "it") def __chooseNvidia(self, event): util.cli.execute("sudo prime-select nvidia") def __chooseIntel(self, event): util.cli.execute("sudo prime-select intel") def query(self, widget): try: res = util.cli.execute("prime-select query") except RuntimeError: return "n/a" for line in res.split("\n"): if not line: continue if "nvidia" in line: return self.nvidiastring if "intel" in line: return self.intelstring return "n/a" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/progress.py000066400000000000000000000073431450256260500256450ustar00rootroot00000000000000""" Show progress for cp, mv, dd, ... Parameters: * progress.placeholder: Text to display while no process is running (defaults to 'n/a') * progress.barwidth: Width of the progressbar if it is used (defaults to 8) * progress.format: Format string (defaults to '{bar} {cmd} {arg}') Available values are: {bar} {pid} {cmd} {arg} {percentage} {quantity} {speed} {time} * progress.barfilledchar: Character used to draw the filled part of the bar (defaults to '#'), notice that it can be a string * progress.baremptychar: Character used to draw the empty part of the bar (defaults to '-'), notice that it can be a string Requires the following executable: * progress contributed by `remi-dupre `_ - many thanks! """ import core.module import core.widget import util.cli import util.format import re class Module(core.module.Module): def __init__(self, config, theme): 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)) count = round((width * widget.get("per")) / 100) filledchar = self.parameter("barfilledchar", "#") emptychar = self.parameter("baremptychar", "-") bar = "[{}{}]".format(filledchar * count, emptychar * (width - count)) str_format = self.parameter("format", "{bar} {cmd} {arg}") return str_format.format( bar=bar, pid=widget.get("pid"), cmd=widget.get("cmd"), arg=widget.get("arg"), percentage=widget.get("per"), quantity=widget.get("qty"), speed=widget.get("spd"), time=widget.get("tim"), ) else: return self.parameter("placeholder", "n/a") def update_progress_info(self, widget): """Update widget's information about the copy""" if not self.__active: return # These regex extracts following groups: # 1. pid # 2. command # 3. arguments # 4. progress (xx.x formatted) # 5. quantity (.. unit / .. unit formatted) # 6. speed # 7. time remaining extract_nospeed = re.compile( r"\[ *(\d*)\] ([a-zA-Z]*) (.*)\n\t(\d*\.*\d*)% \((.*)\)\n.*" ) extract_wtspeed = re.compile( r"\[ *(\d*)\] ([a-zA-Z]*) (.*)\n\t(\d*\.*\d*)% \((.*)\) (\d*\.\d .*) remaining (\d*:\d*:\d*)\n.*" ) try: raw = util.cli.execute("progress -qW 0.1") result = extract_wtspeed.match(raw) if not result: # Abort speed measures raw = util.cli.execute("progress -q") result = extract_nospeed.match(raw) widget.set("spd", "???.? B/s") widget.set("tim", "??:??:??") else: widget.set("spd", result.group(6)) widget.set("tim", result.group(7)) widget.set("pid", int(result.group(1))) widget.set("cmd", result.group(2)) widget.set("arg", result.group(3)) widget.set("per", float(result.group(4))) widget.set("qty", result.group(5)) return True except Exception: return False def update(self): self.__active = bool(util.cli.execute("progress -q")) def state(self, widget): if self.__active: return ["copying", "no-autohide"] return "pending" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/publicip.py000066400000000000000000000121521450256260500256020ustar00rootroot00000000000000""" 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)) self.__previous_default_route = None self.__current_default_route = None self.background = True # 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 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) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/rofication.py000066400000000000000000000043121450256260500261270ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/rotation.py000066400000000000000000000034651450256260500256410ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Shows a widget for each connected screen and allows the user to loop through different orientations. Requires the following executable: * xrandr """ import core.module import core.input import util.cli possible_orientations = ["normal", "left", "inverted", "right"] class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) def update(self): widgets = self.widgets() for line in util.cli.execute("xrandr -q").split("\n"): if not " connected" in line: continue display = line.split(" ", 2)[0] orientation = "normal" for curr_orient in possible_orientations: if (line.split(" ")).count(curr_orient) > 1: orientation = curr_orient break widget = self.widget(name=display) if not widget: widget = self.add_widget(full_text=display, name=display) core.input.register( widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle ) widget.set("orientation", orientation) def state(self, widget): return widget.get("orientation", "normal") def __toggle(self, event): widget = self.widget_by_id(event["instance"]) # compute new orientation based on current orientation idx = possible_orientations.index(widget.get("orientation")) idx = (idx + 1) % len(possible_orientations) new_orientation = possible_orientations[idx] widget.set("orientation", new_orientation) util.cli.execute( "xrandr --output {} --rotation {}".format(widget.name, new_orientation) ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/rss.py000066400000000000000000000303371450256260500246070ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """RSS news ticker Fetches rss news items and shows these as a news ticker. Left-clicking will open the full story in a browser. New stories are highlighted. Parameters: * rss.feeds : Space-separated list of RSS URLs * rss.length : Maximum length of the module, default is 60 contributed by `lonesomebyte537 `_ - many thanks! """ import feedparser import webbrowser import time import os import tempfile import logging import random import re import json import core.module import core.widget import core.input # pylint: disable=too-many-instance-attributes class Module(core.module.Module): REFRESH_DELAY = 600 SCROLL_SPEED = 3 LAYOUT_STYLES_ITEMS = [[1, 1, 1], [3, 3, 2], [2, 3, 3], [3, 2, 3]] HISTORY_FILENAME = ".config/i3/rss.hist" def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.ticker_update)) self._feeds = self.parameter( "feeds", "https://www.espn.com/espn/rss/news" ).split(" ") self._feeds_to_update = [] self._response = "" self._max_title_length = int(self.parameter("length", 60)) self._items = [] self._current_item = None self._ticker_offset = 0 self._pre_delay = 0 self._post_delay = 0 self._state = [] self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html") self._last_refresh = 0 self._last_update = 0 core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self._open) core.input.register( self, button=core.input.RIGHT_MOUSE, cmd=self._create_newspaper ) self._history = {"ticker": {}, "newspaper": {}} self._load_history() def _load_history(self): if os.path.isfile(self.HISTORY_FILENAME): self._history = json.loads(open(self.HISTORY_FILENAME, "r").read()) def _update_history(self, group): sources = set([i["source"] for i in self._items]) self._history[group] = dict( [ [s, [i["title"] for i in self._items if i["source"] == s]] for s in sources ] ) def _save_history(self): if not os.path.exists(os.path.dirname(self.HISTORY_FILENAME)): os.makedirs(os.path.dirname(self.HISTORY_FILENAME)) open(self.HISTORY_FILENAME, "w").write(json.dumps(self._history)) def _check_history(self, items, group): for i in items: i["new"] = not ( i["source"] in self._history[group] and i["title"] in self._history[group][i["source"]] ) def _open(self, _): if self._current_item: webbrowser.open(self._current_item["link"]) def _check_for_image(self, entry): image = next( iter([l["href"] for l in entry["links"] if l["rel"] == "enclosure"]), None ) if not image and "media_content" in entry: try: media = sorted( entry["media_content"], key=lambda i: i["height"] if "height" in i else 0, reverse=True, ) image = next( iter([i["url"] for i in media if i["medium"] == "image"]), None ) except Exception: pass if not image: match = re.search( r"]*src\s*=['\']*([^\s^>^'^\']*)['\']*", entry["summary"] ) if match: image = match.group(1) return image if image else "" def _remove_tags(self, txt): return re.sub(r"<[^>]*>", "", txt) def _create_item(self, entry, url, feed): return { "title": self._remove_tags(entry["title"].replace("\n", " ")), "link": entry["link"], "new": True, "source": url, "summary": self._remove_tags(entry["summary"]), "feed": feed, "image": self._check_for_image(entry), "published": time.mktime(entry.published_parsed) if hasattr(entry, "published_parsed") else 0, } def _update_items_from_feed(self, url): parser = feedparser.parse(url) new_items = [ self._create_item(entry, url, parser["feed"]["title"]) for entry in parser["entries"] ] # Check history self._check_history(new_items, "ticker") # Remove the previous items self._items = [i for i in self._items if i["source"] != url] # Add the new items self._items.extend(new_items) # Sort the items on publish date self._items.sort(key=lambda i: i["published"], reverse=True) def _check_for_refresh(self): if self._feeds_to_update: # Update one feed at a time to not overload this update cycle url = self._feeds_to_update.pop() self._update_items_from_feed(url) if not self._feeds_to_update: self._update_history("ticker") self._save_history() if not self._current_item: self._next_item() elif time.time() - self._last_refresh >= self.REFRESH_DELAY: # Populate the list with feeds to update self._feeds_to_update = self._feeds[:] # Update the refresh time self._last_refresh = time.time() def _next_item(self): self._ticker_offset = 0 self._pre_delay = 2 self._post_delay = 4 if not self._items: return # Index of the current element idx = ( self._items.index(self._current_item) if self._current_item in self._items else -1 ) # First show new items, else show next new_items = [i for i in self._items if i["new"]] self._current_item = next( iter(new_items), self._items[(idx + 1) % len(self._items)] ) def _check_scroll_done(self): # Check if the complete title has been shown if self._ticker_offset + self._max_title_length > len( self._current_item["title"] ): # Do not immediately show next item after scroll self._post_delay -= 1 if self._post_delay == 0: self._current_item["new"] = False # Mark the previous item as 'old' self._next_item() else: # Increase scroll position self._ticker_offset += self.SCROLL_SPEED def ticker_update(self, _): # Only update the ticker once a second now = time.time() if now - self._last_update < 1: return self._response self._last_update = now self._check_for_refresh() # If no items were retrieved, return an empty string if not self._current_item: return " " * self._max_title_length # Prepare a substring of the item title self._response = self._current_item["title"][ self._ticker_offset : self._ticker_offset + self._max_title_length ] # Add spaces if too short self._response = self._response.ljust(self._max_title_length) # Do not immediately scroll if self._pre_delay > 0: # Change state during pre_delay for new items if self._current_item["new"]: self._state = ["warning"] self._pre_delay -= 1 return self._response self._state = [] self._check_scroll_done() return self._response def state(self, _): return self._state def _create_news_element(self, item, overlay_title): try: timestr = ( "" if item["published"] == 0 else str(time.ctime(item["published"])) ) except Exception as exc: logging.error(str(exc)) raise e element = "

" element += "
" element += ( " " ) element += ( "
" + ("" if item["new"] else "") + item["title"] + "
" ) element += "
" element += "
" + item["summary"] + "
" element += ( "
" + item["feed"] + "" + timestr + "
" ) element += "
" return element def _create_news_section(self, newspaper_items): style = random.randint(0, 3) section = "" for i in range(0, 3): section += "" section += "
" for _ in range(0, self.LAYOUT_STYLES_ITEMS[style][i]): if newspaper_items: section += self._create_news_element( newspaper_items[0], self.LAYOUT_STYLES_ITEMS[style][i] != 3 ) del newspaper_items[0] section += "
" return section def _create_newspaper(self, _): content = "" newspaper_items = self._items[:] self._check_history(newspaper_items, "newspaper") # Make sure new items are always listed first, independent of publish date newspaper_items.sort( key=lambda i: i["published"] + (10000000 if i["new"] else 0), reverse=True ) while newspaper_items: content += self._create_news_section(newspaper_items) self._newspaper_file.write( HTML_TEMPLATE.replace("[[CONTENT]]", content) ) self._newspaper_file.flush() webbrowser.open("file://" + self._newspaper_file.name) self._update_history("newspaper") self._save_history() HTML_TEMPLATE = """
Bumblebee Daily
[[CONTENT]]
""" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/sensors.py000066400000000000000000000137451450256260500255000ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # pylint: disable=C0111,R0903 """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 be: 'coretemp-isa-00000/Core 0/temp1_input' (defaults to 'false') * sensors.match: (fallback) Line to match against output of 'sensors -u' (default: temp1_input) * sensors.match_pattern: (fallback) Line to match against before temperature is read (no default) * sensors.match_number: (fallback) which of the matches you want (default -1: last match). * sensors.show_freq: whether to show CPU frequency. (default: true) contributed by `mijoharas `_ - many thanks! """ import re import os import json import logging log = logging.getLogger(__name__) 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.temperature)) self._temperature = "unknown" self._mhz = "n/a" self._match_number = int(self.parameter("match_number", "-1")) self._match_pattern = self.parameter("match_pattern", None) self._pattern = re.compile( r"^\s*{}:\s*([\d.]+)$".format(self.parameter("match", "temp1_input")), re.MULTILINE, ) 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() 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 def _get_temp_from_sensors(self): if self._json == True: try: output = json.loads(util.cli.execute("sensors -j")) for key in self.parameter("path").split("/"): output = output[key] return int(float(output)) except Exception as e: logging.error("unable to read sensors: {}".format(str(e))) return "unknown" else: output = util.cli.execute("sensors -u") if self._match_pattern: temp_pattern = self.parameter("match", "temp1_input") match = re.search( r"{}.+{}:\s*([\d.]+)$".format(self._match_pattern, temp_pattern), output.replace("\n", ""), ) if match: return int(float(match.group(1))) else: return "unknown" match = self._pattern.findall(output) if match: return int(float(match[self._match_number])) return "unknown" def get_temp(self): if self.use_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" def get_mhz(self): mhz = None try: output = open( "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq" ).read() mhz = int(float(output) / 1000.0) except: output = open("/proc/cpuinfo").read() m = re.search(r"cpu MHz\s+:\s+(\d+)", output) if m: mhz = int(m.group(1)) else: m = re.search(r"BogoMIPS\s+:\s+(\d+)", output) if m: return "{} BogoMIPS".format(int(m.group(1))) if not mhz: return "n/a" if mhz < 1000: return "{} MHz".format(mhz) else: return "{:0.01f} GHz".format(float(mhz) / 1000.0) def temperature(self, _): if self._freq: return "{}°c @ {}".format(self._temperature, self._mhz) else: return "{}°c".format(self._temperature) def update(self): self._temperature = self.get_temp() if self._freq: self._mhz = self.get_mhz() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/shell.py000066400000000000000000000054531450256260500251100ustar00rootroot00000000000000# pylint: disable=C0111,R0903,W1401 r""" Execute command in shell and print result Few command examples: 'ping -c 1 1.1.1.1 | grep -Po '(?<=time=)\d+(\.\d+)? ms'' 'echo 'BTC=$(curl -s rate.sx/1BTC | grep -Po \'^\d+\')USD'' 'curl -s https://wttr.in/London?format=%l+%t+%h+%w' 'pip3 freeze | wc -l' 'any_custom_script.sh | grep arguments' Parameters: * shell.command: Command to execute Use single parentheses if evaluating anything inside (sh-style) For example shell.command='echo $(date +'%H:%M:%S')' But NOT shell.command='echo $(date +'%H:%M:%S')' Second one will be evaluated only once at startup * shell.interval: Update interval in seconds (defaults to 1s == every bumblebee-status update) * shell.async: Run update in async mode. Won't run next thread if previous one didn't finished yet. Useful for long running scripts to avoid bumblebee-status freezes (defaults to False) contributed by `rrhuffy `_ - many thanks! """ import os import subprocess import threading import core.module import core.widget import core.input import util.format import util.cli class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.get_output)) self.__command = self.parameter("command", 'echo "no command configured"') 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) def set_output(self, value): self.__output = value @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() core.event.trigger("update", [self.id], redraw_only=True) return # if previous thread didn't end yet then don't do anything if self.__current_thread.is_alive(): return # spawn new thread to execute command and pass callback method to get output from it self.__current_thread = threading.Thread( target=lambda obj, cmd: obj.set_output( util.cli.execute(cmd, ignore_errors=True) ), args=(self, self.__command), ) self.__current_thread.start() def state(self, _): if self.__output == "no command configured": return "warning" # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/shortcut.py000066400000000000000000000043621450256260500256520ustar00rootroot00000000000000# pylint: disable=C0112,R0903 """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). 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)' Parameters: * shortcut.cmds : List of commands to execute * shortcut.labels: List of widgets' labels (text) * shortcut.delim : Commands and labels delimiter (; semicolon by default) contributed by `cacyss0807 `_ - many thanks! """ import logging LINK = "https://github.com/tobi-wan-kenobi/bumblebee-status/wiki" LABEL = "Click me" import core.module import core.input import core.decorators class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): super().__init__(config, theme, []) self.__labels = self.parameter("labels", "{}".format(LABEL)) self.__cmds = self.parameter("cmds", "firefox {}".format(LINK)) self.__delim = self.parameter("delim", ";") self.update_widgets() def update_widgets(self): """ Creates a set of widget per user define shortcut.""" cmds = self.__cmds.split(self.__delim) labels = self.__labels.split(self.__delim) # to be on the safe side create as many widgets as there are data (cmds or labels) num_shortcuts = min(len(cmds), len(labels)) # report possible problem as a warning if len(cmds) is not len(labels): logging.warning( "shortcut: the number of commands does not match " "the number of provided labels." ) logging.warning("cmds : %s, labels : %s", cmds, labels) for idx in range(0, num_shortcuts): cmd = cmds[idx] label = labels[idx] widget = self.add_widget(full_text=label) core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=cmd) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/smartstatus.py000066400000000000000000000106471450256260500263740ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # smart function inspired by py-SMART https://github.com/freenas/py-SMART # under Copyright (C) 2015 Marc Herndon and GPL2 """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.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. """ import os import shutil import core.module import core.decorators import util.cli import util.format class Module(core.module.Module): @core.decorators.every(minutes=5) def __init__(self, config, theme): super().__init__(config, theme, []) self.devices = self.list_devices() self.display = self.parameter("display", "combined") self.drives = self.parameter("drives", "sda") self.show_names = util.format.asbool(self.parameter("show_names", True)) self.create_widgets() def create_widgets(self): if self.display == "combined" or self.display == "combined_singles": widget = self.add_widget() widget.set("device", "combined") widget.set("assessment", self.combined()) self.output(widget) else: for device in self.devices: if self.display == "singles" and device not in self.drives: continue widget = self.add_widget() widget.set("device", device) widget.set("assessment", self.smart(device)) self.output(widget) def update(self): for widget in self.widgets(): device = widget.get("device") if device == "combined": widget.set("assessment", self.combined()) self.output(widget) else: widget.set("assessment", self.smart(device)) self.output(widget) def output(self, widget): device = widget.get("device") assessment = widget.get("assessment") if self.show_names: widget.full_text("{}: {}".format(device, assessment)) else: widget.full_text("{}".format(assessment)) def state(self, widget): states = [] assessment = widget.get("assessment") if assessment == "Pre-fail": states.append("warning") if assessment == "Fail": states.append("critical") return states 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" if result == "Pre-fail": return "Pre-fail" return "OK" def list_devices(self): for (root, folders, files) in os.walk("/dev"): if root == "/dev": devices = { "".join(filter(lambda i: i.isdigit() == False, file)) for file in files if "sd" in file } nvme = { file for file in files if ("nvme0n" in file and "p" not in file) } devices.update(nvme) return devices def smart(self, disk_name): smartctl = shutil.which("smartctl") assessment = None output = util.cli.execute( "sudo {} --health {}".format(smartctl, os.path.join("/dev/", disk_name)) ) output = output.split("\n") line = output[4] if "SMART" in line: if any([i in line for i in ["PASSED", "OK"]]): assessment = "OK" else: assessment = "Fail" if assessment == "OK": output = util.cli.execute( "sudo {} -A {}".format(smartctl, os.path.join("/dev/", disk_name)) ) output = output.split("\n") for line in output: if "Pre-fail" in line: assessment = "Pre-fail" return assessment # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/solaar.py000066400000000000000000000030701450256260500252530ustar00rootroot00000000000000"""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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/spaceapi.py000066400000000000000000000110641450256260500255610ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # pylint: disable=C0111,R0903 """Displays the state of a Space API endpoint Space API is an API for hackspaces based on JSON. See spaceapi.io for an example. Requires the following libraries: * requests Parameters: * spaceapi.url: String representation of the api endpoint * spaceapi.format: Format string for the output Format Strings: * Format strings are indicated by double %% * They represent a leaf in the JSON tree, layers separated by '.' * Boolean values can be overwritten by appending '%true%false' in the format string * Example: to reference 'open' in '{'state':{'open': true}}' you would write '%%state.open%%', if you also want to say 'Open/Closed' depending on the boolean you would write '%%state.open%Open%Closed%%' contributed by `rad4day `_ - many thanks! """ import requests import threading import re import json import core.module import core.widget import core.input import core.decorators def formatStringBuilder(s, json): """ Parses Format Strings Parameter: s -> format string json -> the spaceapi response object """ identifiers = re.findall(r"%%.*?%%", s) for i in identifiers: ic = i[2:-2] # Discard %% j = ic.split("%") # Only neither of, or both true AND false may be overwritten if len(j) != 3 and len(j) != 1: return "INVALID FORMAT STRING" if len(j) == 1: # no overwrite s = s.replace(i, json[j[0]]) elif json[j[0]]: # overwrite for True s = s.replace(i, j[1]) else: # overwrite for False s = s.replace(i, j[2]) return s class Module(core.module.Module): @core.decorators.every(minutes=15) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.getState)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__forceReload) self.__data = {} self.__error = None self.__thread = None # The URL representing the api endpoint self.__url = self.parameter("url", default="http://club.entropia.de/spaceapi") self._format = self.parameter( "format", default=" %%space%%: %%state.open%Open%Closed%%" ) def state(self, widget): try: if self.__error is not None: return ["critical"] elif self.__data["state.open"]: return ["warning"] else: return [] except KeyError: return ["critical"] def update(self): if not self.__thread or self.__thread.is_alive() == False: self.__thread = threading.Thread(target=self.get_api_async, args=()) self.__thread.start() def getState(self, widget): text = self._format if self.__error is not None: text = self.__error else: try: text = formatStringBuilder(self._format, self.__data) except KeyError: text = "KeyError" return text def get_api_async(self): try: with requests.get(self.__url, timeout=10) as request: # Can't implement error handling for python2.7 if I use # request.json() as it uses simplejson in newer versions self.__data = self.__flatten(json.loads(request.text)) self.__error = None except requests.exceptions.Timeout: self.__error = "Timeout" except requests.exceptions.HTTPError: self.__error = "HTTP Error" except ValueError: self.__error = "Not a JSON response" core.event.trigger("update", [self.id], redraw_only=True) # left_mouse_button handler def __forceReload(self, event): if self.__thread: self.__thread.raise_exception() self.__error = "RELOADING" core.event.trigger("update", [self.id], redraw_only=True) # Flattens the JSON structure recursively, e.g. ['space']['open'] # becomes ['space.open'] def __flatten(self, json): out = {} for key in json: value = json[key] if type(value) is dict: flattened_key = self.__flatten(value) for fk in flattened_key: out[key + "." + fk] = flattened_key[fk] else: out[key] = value return out # Author: Tobias Manske # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/spotify.py000066400000000000000000000146501450256260500254750ustar00rootroot00000000000000"""Displays the current song being played and allows pausing, skipping ahead, and skipping back. 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 contributed by `yvesh `_ - many thanks! added controls by `LtPeriwinkle `_ - many thanks! fixed icons and layout parameter by `gkeep `_ - many thanks! """ import sys import dbus 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, []) self.background = True self.__bus_name = self.parameter("bus_name", "spotify") self.__layout = util.format.aslist( self.parameter( "layout", "spotify.song,spotify.prev,spotify.pause,spotify.next", ) ) self.__bus = dbus.SessionBus() self.__song = "" self.__pause = "" self.__format = self.parameter("format", "{artist} - {title}") if self.__bus_name == "spotifyd": self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotifyd \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." else: self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." widget_map = {} for widget_name in self.__layout: widget = self.add_widget(name=widget_name) if widget_name == "spotify.prev": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Previous", } widget.set("state", "prev") elif widget_name == "spotify.pause": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "PlayPause", } elif widget_name == "spotify.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Next", } widget.set("state", "next") elif widget_name == "spotify.song": if util.format.asbool(self.parameter("concise_controls", "false")): widget_map[widget] = [ { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "PlayPause", }, { "button": core.input.WHEEL_UP, "cmd": self.__cmd + "Next", }, { "button": core.input.WHEEL_DOWN, "cmd": self.__cmd + "Previous", } ] else: raise KeyError( "The spotify module does not have a {widget_name!r} widget".format( widget_name=widget_name ) ) # is there any reason the inputs can't be directly registered above? for widget, callback_options in widget_map.items(): if isinstance(callback_options, dict): core.input.register(widget, **callback_options) elif isinstance(callback_options, list): # used by concise_controls for opts in callback_options: core.input.register(widget, **opts) def hidden(self): return self.string_song == "" @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: 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 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: self.__song = "" @property def string_song(self): if sys.version_info.major < 3: return unicode(self.__song) return str(self.__song) bumblebee-status-2.2.0/bumblebee_status/modules/contrib/stock.py000066400000000000000000000046441450256260500251250ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # pylint: disable=C0111,R0903 """Display a stock quote from finance.yahoo.com 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" contributed by `msoulier `_ - many thanks! """ import json import urllib.request import logging import core.module import core.widget 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) def __init__(self, config, theme): 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 = [] def value(self, widget): result = "" 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 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 [] else: logging.error("unable to retrieve stock exchange rate") return [] return results def update(self): self.__values = self.fetch() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/sun.py000066400000000000000000000060021450256260500245750ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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 (if none of those are set, location is determined automatically via location APIs) contributed by `lonesomebyte537 `_ - many thanks! """ from suntime import Sun, SunTimeException import requests from dateutil.tz import tzlocal import datetime import core.module import core.widget import core.decorators import util.location class Module(core.module.Module): @core.decorators.every(hours=1) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.suntimes)) lat = self.parameter("lat", None) lon = self.parameter("lon", None) self.__sun = None if not lat or not lon: try: lat, lon = util.location.coordinates() except Exception: pass if lat and lon: self.__sun = Sun(float(lat), float(lon)) def suntimes(self, _): if self.__sunset and self.__sunrise: if self.__isup: return "\u21A7{} \u21A5{}".format( self.__sunset.strftime("%H:%M"), self.__sunrise.strftime("%H:%M") ) return "\u21A5{} \u21A7{}".format( self.__sunrise.strftime("%H:%M"), self.__sunset.strftime("%H:%M") ) return "n/a" def __calculate_times(self): if not self.__sun: self.__sunset = self.__sunrise = None return self.__isup = False order_matters = True try: self.__sunrise = self.__sun.get_local_sunrise_time() except SunTimeException: self.__sunrise = "no sunrise" order_matters = False try: self.__sunset = self.__sun.get_local_sunset_time() except SunTimeException: self.__sunset = "no sunset" order_matters = False if not order_matters: return now = datetime.datetime.now(tz=tzlocal()) if now > self.__sunset: tomorrow = (now + datetime.timedelta(days=1)).date() try: self.__sunrise = self.__sun.get_local_sunrise_time(tomorrow) self.__sunset = self.__sun.get_local_sunset_time(tomorrow) except SunTimeException: self.__sunrise = "no sunrise" self.__sunset = "no sunset" elif now > self.__sunrise: tomorrow = (now + datetime.timedelta(days=1)).date() try: self.__sunrise = self.__sun.get_local_sunrise_time(tomorrow) except SunTimeException: self.__sunrise = "no sunrise" return self.__isup = True def update(self): self.__calculate_times() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/system.py000066400000000000000000000076701450256260500253300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # pylint: disable=C0111,R0903 """ system module adds the possibility to * shutdown * 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.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') * system.switch_user: specify a command for switching the user (defaults to 'i3exit switch_user') * system.lock: specify a command for locking the screen (defaults to 'i3exit lock') * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') Requirements: tkinter (python3-tk package on debian based systems either you can install it as python package) contributed by `bbernhard `_ - many thanks! """ import logging import functools try: import tkinter as tk from tkinter import messagebox as tkmessagebox except ImportError: logging.warning("failed to import tkinter - bumblebee popups won't work!") import core.module import core.widget import core.input import core.decorators import util.cli import util.popup import util.format class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.text)) self.__confirm = util.format.asbool(self.parameter("confirm", True)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) def text(self, widget): return "" def __on_command(self, header, text, command): do_it = True if self.__confirm: root = tk.Tk() root.withdraw() root.focus_set() do_it = tkmessagebox.askyesno(header, text) root.destroy() if do_it: 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) reboot_cmd = self.parameter("reboot", "reboot") shutdown_cmd = self.parameter("shutdown", "shutdown -h now") logout_cmd = self.parameter("logout", "i3exit logout") switch_user_cmd = self.parameter("switch_user", "i3exit switch_user") lock_cmd = self.parameter("lock", "i3exit lock") suspend_cmd = self.parameter("suspend", "i3exit suspend") hibernate_cmd = self.parameter("hibernate", "i3exit hibernate") menu.add_menuitem( "shutdown", callback=functools.partial( self.__on_command, "Shutdown", "Shutdown?", shutdown_cmd ), ) menu.add_menuitem( "reboot", callback=functools.partial( self.__on_command, "Reboot", "Reboot?", reboot_cmd ), ) menu.add_menuitem( "log out", callback=functools.partial( self.__on_command, "Log out", "Log out?", logout_cmd ), ) # don't ask for these menu.add_menuitem( "switch user", callback=functools.partial(util.cli.execute, switch_user_cmd) ) menu.add_menuitem( "lock", callback=functools.partial(util.cli.execute, lock_cmd) ) menu.add_menuitem( "suspend", callback=functools.partial(util.cli.execute, suspend_cmd) ) menu.add_menuitem( "hibernate", callback=functools.partial(util.cli.execute, hibernate_cmd) ) menu.show(widget, 0, 0) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/taskwarrior.py000066400000000000000000000044511450256260500263460ustar00rootroot00000000000000"""Displays the number of pending tasks in TaskWarrior. Requires the following library: * taskw 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! """ from taskw import TaskWarrior import core.module import core.widget import core.decorators class Module(core.module.Module): def __init__(self, config, theme): 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. """ 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 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/thunderbird.py000066400000000000000000000047631450256260500263160ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/timetz.py000066400000000000000000000010461450256260500253070ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current date and time. Parameters: * time.format: strftime()-compatible formatting string * time.locale: locale to use rather than the system default """ import core.decorators from .datetimetz import Module class Module(Module): @core.decorators.every(seconds=59) # ensures one update per minute def __init__(self, config, theme): super().__init__(config, theme) def default_format(self): return "%X %Z" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/title.py000066400000000000000000000054061450256260500251200ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays focused i3 window title. Requirements: * i3ipc 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! """ import threading try: import i3ipc except ImportError: pass no_title = "n/a" import core.module import core.decorators import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) # 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 = "" # set output of the module self.add_widget( full_text=self.__scrolling_focused_title if self.__scroll else self.__focused_title ) # create a connection with i3ipc self.__i3 = i3ipc.Connection() # event is called both on focus change and title change, and on workspace 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() # initialize the first title self.__pollTitle() def __focused_title(self, widget): return self.__title @core.decorators.scrollable def __scrolling_focused_title(self, widget): return self.__full_title 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 except: self.__full_title = no_title if self.__full_title is None: self.__full_title = no_title if not self.__scroll: # cut the text if it is too long if len(self.__full_title) > self.__max: self.__title = self.__full_title[ 0 : self.__max - len(self.__placeholder) ] self.__title = "{}{}".format(self.__title, self.__placeholder) else: self.__title = self.__full_title # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/todo.py000066400000000000000000000025321450256260500247410ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the number of todo items from a text file Parameters: * todo.file: File to read TODOs from (defaults to ~/Documents/todo.txt) contributed by `codingo `_ - many thanks! """ import os.path import core.module import core.widget import core.input class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.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) ) def output(self, widget): return str(self.__todos) def update(self): self.__todos = self.count_items() def state(self, widgets): if self.__todos == 0: return "empty" return "items" def count_items(self): try: i = 0 with open(self.__doc) as f: for l in f.readlines(): if l.strip() != '': i += 1 return i except Exception: return 0 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/todo_org.py000066400000000000000000000037561450256260500256210ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/todoist.py000066400000000000000000000040041450256260500254550ustar00rootroot00000000000000# 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)) bumblebee-status-2.2.0/bumblebee_status/modules/contrib/traffic.py000066400000000000000000000146131450256260500254150ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays network IO for interfaces. Parameters: * traffic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth') * traffic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) * 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 char shows 2 seconds. If set, enables up/down traffic graphs contributed by `meain `_ - many thanks! """ import re import time import psutil import logging import netifaces import core.module import util.format import util.graph class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self._exclude = tuple( filter( len, util.format.aslist( self.parameter("exclude", "lo,virbr,docker,vboxnet,veth") ), ) ) self._status = "" self._showname = util.format.asbool(self.parameter("showname", True)) self._format = self.parameter("format", "{:.2f}") self._prev = {} self._states = {} self._lastcheck = 0 self._states["include"] = [] self._states["exclude"] = [] for state in tuple( filter(len, util.format.aslist(self.parameter("states", ""))) ): if state[0] == "^": self._states["exclude"].append(state[1:]) else: self._states["include"].append(state) self._graphlen = int(self.parameter("graphlen", 0)) if self._graphlen > 0: self._graphdata = {} self._first_run = True self._update_widgets() def state(self, widget): if "traffic.rx" in widget.name: return "rx" if "traffic.tx" in widget.name: return "tx" return self._status def update(self): try: self._update_widgets() except Exception as e: logging.exception(e) def create_widget(self, name, txt=None, attributes={}): widget = self.add_widget(name=name, full_text=txt) for key in attributes: widget.set(key, attributes[key]) return widget def get_addresses(self, intf): retval = [] try: for ip in netifaces.ifaddresses(intf).get(netifaces.AF_INET, []): if ip.get("addr", "") != "": retval.append(ip.get("addr")) except Exception: return [] return retval def get_minwidth_str(self): """ computes theme.minwidth string based on traffic.format and traffic.graphlen parameters """ minwidth_str = "" if self._graphlen > 0: graph_len = int(self._graphlen / 2) graph_prefix = "0" * graph_len minwidth_str += graph_prefix minwidth_str += "1000" try: length = int(re.match(r"{:\.(\d+)f}", self._format).group(1)) if length > 0: minwidth_str += "." + "0" * length except AttributeError: # return default value return "1000.00KiB/s" finally: minwidth_str += "KiB/s" return minwidth_str def _update_widgets(self): interfaces = [ i for i in netifaces.interfaces() if not i.startswith(self._exclude) ] self.clear_widgets() counters = psutil.net_io_counters(pernic=True) now = time.time() timediff = now - (self._lastcheck if self._lastcheck else now) if timediff <= 0: timediff = 1 self._lastcheck = now for interface in interfaces: if self._graphlen > 0: if interface not in self._graphdata: self._graphdata[interface] = { "rx": [0] * self._graphlen, "tx": [0] * self._graphlen, } if not interface: interface = "lo" state = "down" if len(self.get_addresses(interface)) > 0: state = "up" elif util.format.asbool(self.parameter("hide_down", True)): continue if len(self._states["exclude"]) > 0 and state in self._states["exclude"]: continue if ( len(self._states["include"]) > 0 and state not in self._states["include"] ): continue data = { "rx": counters[interface].bytes_recv, "tx": counters[interface].bytes_sent, } name = "traffic-{}".format(interface) if self._showname: self.create_widget(name, interface) for direction in ["rx", "tx"]: name = "traffic.{}-{}".format(direction, interface) widget = self.create_widget( name, attributes={"theme.minwidth": self.get_minwidth_str()}, ) prev = self._prev.get(name, 0) bspeed = (int(data[direction]) - int(prev)) / timediff speed = util.format.byte(bspeed, self._format) txtspeed = "{0}/s".format(speed) if self._graphlen > 0: # skip first value returned by psutil, because it is # giant and ruins the grapth ratio until it gets pushed # out of saved list if self._first_run is True: self._first_run = False else: self._graphdata[interface][direction] = self._graphdata[ interface ][direction][1:] self._graphdata[interface][direction].append(bspeed) txtspeed = "{}{}".format( util.graph.braille(self._graphdata[interface][direction]), txtspeed, ) widget.full_text(txtspeed) self._prev[name] = data[direction] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/twmn.py000066400000000000000000000023241450256260500247600ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Toggle twmn notifications. Requires the following executable: * systemctl contributed by `Pseudonick47 `_ - many thanks! """ import core.module import core.widget import core.input 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.__paused = False # Make sure that twmn is currently not paused util.cli.execute("killall -SIGUSR2 twmnd", ignore_errors=True) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status) def toggle_status(self, event): self.__paused = not self.__paused try: if self.__paused: util.cli.execute("systemctl --user start twmnd") else: util.cli.execute("systemctl --user stop twmnd") except: self.__paused = not self.__paused # toggling failed def state(self, widget): if self.__paused: return ["muted"] return ["unmuted"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/uptime.py000066400000000000000000000012741450256260500253010ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the system uptime. contributed by `ccoors `_ - many thanks! """ from datetime import timedelta 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.__uptime = "" def output(self, _): return "{}".format(self.__uptime) def update(self): with open("/proc/uptime", "r") as f: uptime_seconds = int(float(f.readline().split()[0])) self.__uptime = timedelta(seconds=uptime_seconds) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/usage.py000066400000000000000000000043521450256260500251020ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/vpn.py000066400000000000000000000074211450256260500246010ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """ Displays the VPN profile that is currently in use. Left click opens a popup menu that lists all available VPN profiles and allows to establish a VPN connection using that profile. Prerequisites: * tk python library (usually python-tk or python3-tk, depending on your distribution) * nmcli needs to be installed and configured properly. To quickly test, whether nmcli is working correctly, type 'nmcli -g NAME,TYPE,DEVICE con' which lists all the connection profiles that are configured. Make sure that your VPN profile is in that list! e.g: to import a openvpn profile via nmcli: `sudo nmcli connection import type openvpn file
` contributed by `bbernhard `_ - many thanks! """ import logging import functools import core.module import core.widget import core.input import util.cli import util.popup class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.vpn_status)) self.__connected_vpn_profile = None self.__selected_vpn_profile = None res = util.cli.execute("nmcli -g NAME,TYPE c") lines = res.splitlines() self.__vpn_profiles = [] for line in lines: info = line.split(":") try: if self.__isvpn(info[1]): self.__vpn_profiles.append(info[0]) except: pass core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) def __isvpn(self, connection_type): return connection_type in ["vpn", "wireguard"] def update(self): try: res = util.cli.execute("nmcli -g NAME,TYPE,DEVICE con") lines = res.splitlines() self.__connected_vpn_profile = None for line in lines: info = line.split(":") if self.__isvpn(info[1]) and info[2] != "": self.__connected_vpn_profile = info[0] except Exception as e: logging.exception("Could not get VPN status") self.__connected_vpn_profile = None def vpn_status(self, widget): if self.__connected_vpn_profile is None: return "off" return self.__connected_vpn_profile def __on_vpndisconnect(self): try: util.cli.execute( "nmcli c down '{vpn}'".format(vpn=self.__connected_vpn_profile) ) self.__connected_vpn_profile = None except Exception as e: logging.exception("Could not disconnect VPN connection") def __on_vpnconnect(self, name): self.__selected_vpn_profile = name try: util.cli.execute( "nmcli c up '{vpn}'".format(vpn=self.__selected_vpn_profile) ) self.__connected_vpn_profile = name except Exception as e: logging.exception("Could not establish VPN connection") self.__connected_vpn_profile = None def popup(self, widget): menu = util.popup.menu(self.__config) if self.__connected_vpn_profile is not None: menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect) for vpn_profile in self.__vpn_profiles: if ( self.__connected_vpn_profile is not None and self.__connected_vpn_profile == vpn_profile ): continue menu.add_menuitem( vpn_profile, callback=functools.partial(self.__on_vpnconnect, vpn_profile), ) menu.show(widget) def state(self, widget): return [] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/wakatime.py000066400000000000000000000055441450256260500256040ustar00rootroot00000000000000# 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) bumblebee-status-2.2.0/bumblebee_status/modules/contrib/watson.py000066400000000000000000000035721450256260500253140ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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! """ import logging import re import functools import core.module import core.widget import core.input 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.text)) self.__tracking = False self.__info = {} self.__format = self.parameter("format", "{project} [{tags}]") core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle) def toggle(self, widget): if self.__tracking: util.cli.execute("watson stop") else: util.cli.execute("watson restart") self.__tracking = not self.__tracking def text(self, widget): if self.__tracking: return self.__format.format(**self.__info) 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: self.__tracking = False return def state(self, widget): return "on" if self.__tracking else "off" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/weather.py000066400000000000000000000120101450256260500254230ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # pylint: disable=C0111,R0903 """Displays the temperature on the current location based on the ip Requires the following python packages: * requests Parameters: * weather.location: Set location, defaults to 'auto' for getting location automatically from a web service If set to a comma-separated list, left-click and right-click can be used to rotate the locations. Locations should be city names or city ids. * 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 contributed by `TheEdgeOfRage `_ - many thanks! """ import core.module import core.widget import core.input import util.format import util.location import re import requests from requests.exceptions import RequestException class Module(core.module.Module): @core.decorators.every(minutes=15) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.output)) self.__temperature = 0 self.__apikey = self.parameter("apikey", "af7bfe22287c652d032a3064ffa44088") self.__location = util.format.aslist(self.parameter("location", "auto")) self.__index = 0 self.__showcity = util.format.asbool(self.parameter("showcity", True)) self.__showminmax = util.format.asbool(self.parameter("showminmax", False)) self.__unit = self.parameter("unit", "metric") self.__valid = False core.input.register( self, button=core.input.LEFT_MOUSE, cmd=self.__next_location ) core.input.register( self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_location ) def __next_location(self, event): self.__index = (self.__index + 1) % len(self.__location) self.update() def __prev_location(self, event): self.__index = ( len(self.__location) - 1 if self.__index <= 0 else self.__index - 1 ) self.update() def temperature(self): return util.format.astemperature(self.__temperature, self.__unit) def tempmin(self): return util.format.astemperature(self.__tempmin, self.__unit) def tempmax(self): return util.format.astemperature(self.__tempmax, self.__unit) def city(self): city = re.sub(r"[_-]", " ", self.__city) return "{} ".format(city) def output(self, widget): if not self.__valid: return "?" if self.__showminmax: self.__showcity = False return ( self.city() + self.temperature() + " Hi:" + self.tempmax() + " Lo:" + self.tempmin() ) elif self.__showcity: return self.city() + self.temperature() else: return self.temperature() def state(self, widget): if self.__valid: if "thunderstorm" in self.__weather: return ["thunder"] elif "drizzle" in self.__weather: return ["rain"] elif "rain" in self.__weather: return ["rain"] elif "snow" in self.__weather: return ["snow"] elif "sleet" in self.__weather: return ["sleet"] elif "clear" in self.__weather: return ["clear"] elif "cloud" in self.__weather: return ["clouds"] return [] def update(self): try: weather_url = "https://api.openweathermap.org/data/2.5/weather?appid={}".format( self.__apikey ) weather_url = "{}&units={}".format(weather_url, self.__unit) if self.__location[self.__index] == "auto": coord = util.location.coordinates() weather_url = "{url}&lat={lat}&lon={lon}".format( url=weather_url, lat=coord[0], lon=coord[1] ) elif self.__location[self.__index].isdigit(): weather_url = "{url}&id={id}".format( url=weather_url, id=self.__location[self.__index] ) else: weather_url = "{url}&q={city}".format( url=weather_url, city=self.__location[self.__index] ) weather = requests.get(weather_url).json() self.__city = weather["name"] self.__temperature = int(weather["main"]["temp"]) self.__tempmin = int(weather["main"]["temp_min"]) self.__tempmax = int(weather["main"]["temp_max"]) self.__weather = weather["weather"][0]["main"].lower() self.__valid = True except Exception: self.__valid = False # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/xkcd.py000066400000000000000000000011451450256260500247240ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Opens a random xkcd comic in the browser. contributed by `whzup `_ - many thanks! """ import core.module import core.widget import core.input import core.decorators class Module(core.module.Module): @core.decorators.never def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget("xkcd")) core.input.register( self, button=core.input.LEFT_MOUSE, cmd="xdg-open https://c.xkcd.com/random/comic/", ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/yubikey.py000066400000000000000000000020111450256260500254450ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Shows yubikey information Requires: https://github.com/Yubico/python-yubico The output indicates that a YubiKey is not connected or it displays the corresponding serial number. contributed by `EmmaTinten `_ - many thanks! """ import yubico import core.module import core.widget import core.decorators class Module(core.module.Module): @core.decorators.every(seconds=5) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.keystate)) self.__keystate = "No YubiKey" def keystate(self, widget): return self.__keystate def update(self): try: self.__keystate = "YubiKey: " + str( yubico.find_yubikey(debug=False).serial() ) except yubico.yubico_exception.YubicoError: self.__keystate = "No YubiKey" except Exception: self.__keystate = "n/a" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/contrib/zpool.py000066400000000000000000000231061450256260500251370ustar00rootroot00000000000000"""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: '') * zpool.format: Format string, tags {name}, {used}, {left}, {size}, {percentfree}, {percentuse}, {status}, {shortstatus}, {fragpercent}, {deduppercent} are supported. (Default: '{name} {used}/{size} ({percentfree}%)') * zpool.showio: Show also widgets detailing current read and write I/O (Default: true) * zpool.ioformat: Format string for I/O widget, tags {ops} (operations per seconds) and {band} (bandwidth) are supported. (Default: '{band}') * zpool.warnfree: Warn if free space is below this percentage (Default: 10) * zpool.sudo: Use sudo when calling the `zpool` binary. (Default: false) Option `zpool.sudo` is intended for Linux users using zfsonlinux older than 0.7.0: In pre-0.7.0 releases of zfsonlinux regular users couldn't invoke even informative commands such as `zpool list`. If this option is true, command `zpool list` is invoked with sudo. If this option is used, the following (or ekvivalent) must be added to the `sudoers(5)`: ``` ALL = (root) NOPASSWD: /usr/bin/zpool list ``` Be aware of security implications of doing this! contributed by `adam-dej `_ - many thanks! """ import time import logging from pkg_resources import parse_version log = logging.getLogger(__name__) import core.module import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self._includelist = set( filter( lambda x: len(x) > 0, util.format.aslist(self.parameter("list", default="")), ) ) self._format = self.parameter( "format", default="{name} {shortstatus} {used}/{size} " + "({percentfree}%)" ) self._usesudo = util.format.asbool(self.parameter("sudo", default=False)) self._showio = util.format.asbool(self.parameter("showio", default=True)) self._ioformat = self.parameter("ioformat", default="{band}") self._warnfree = int(self.parameter("warnfree", default=10)) def update(self): self.clear_widgets() zfs_version_path = "/sys/module/zfs/version" # zpool list -H: List all zpools, use script mode (no headers and tabs as separators). try: with open(zfs_version_path, "r") as zfs_mod_version: zfs_version = zfs_mod_version.readline().rstrip().split("-")[0] except IOError: # ZFS isn't installed or the module isn't loaded, stub the version zfs_version = "0.0.0" logging.error( "ZFS version information not found at {}, check the module is loaded.".format( zfs_version_path ) ) raw_zpools = util.cli.execute( ("sudo " if self._usesudo else "") + "zpool list -H" ).split("\n") for raw_zpool in raw_zpools: try: # Ignored fields (assigned to _) are 'expandsz' and 'altroot', also 'ckpoint' in ZFS 0.8.0+ if parse_version(zfs_version) < parse_version("0.8.0"): ( name, size, alloc, free, _, frag, cap, dedup, health, _, ) = raw_zpool.split("\t") else: ( name, size, alloc, free, _, _, frag, cap, dedup, health, _, ) = raw_zpool.split("\t") cap = cap.rstrip("%") percentuse = int(cap) percentfree = 100 - percentuse # There is a command, zpool iostat, which is however blocking and was therefore # causing issues. # Instead, we read file `/proc/spl/kstat/zfs//io` which contains # cumulative I/O statistics since boot (or pool creation). We store these values # (and timestamp) during each widget update, and during the next widget update we # use them to compute delta of transferred bytes, and using the last and current # timestamp the rate at which they have been transferred. with open("/proc/spl/kstat/zfs/{}/io".format(name), "r") as f: # Third row provides data we need, we are interested in the first 4 values. # More info about this file can be found here: # https://github.com/zfsonlinux/zfs/blob/master/lib/libspl/include/sys/kstat.h#L580 # The 4 values are: # nread, nwritten, reads, writes iostat = list(map(int, f.readlines()[2].split()[:4])) except (ValueError, IOError): # Unable to parse info about this pool, skip it continue if self._includelist and name not in self._includelist: continue widget = self.widget(name) if not widget: widget = self.add_widget(name=name) widget.set("last_iostat", [0, 0, 0, 0]) widget.set("last_timestamp", 0) delta_iostat = [b - a for a, b in zip(iostat, widget.get("last_iostat"))] widget.set("last_iostat", iostat) # From docs: # > Note that even though the time is always returned as a floating point number, not # > all systems provide time with a better precision than 1 second. # Be aware that that may affect the precision of reported I/O # Also, during one update cycle the reported I/O may be garbage if the system time # was changed. timestamp = time.time() delta_timestamp = widget.get("last_timestamp") - timestamp widget.set("last_timestamp", time.time()) # abs is there because sometimes the result is -0 rate_iostat = [abs(x / delta_timestamp) for x in delta_iostat] nread, nwritten, reads, writes = rate_iostat # theme.minwidth is not set since these values are not expected to change # rapidly widget.full_text( self._format.format( name=name, used=alloc, left=free, size=size, percentfree=percentfree, percentuse=percentuse, status=health, shortstatus=self._shortstatus(health), fragpercent=frag, deduppercent=dedup, ) ) widget.set("state", health) widget.set("percentfree", percentfree) widget.set("visited", True) if self._showio: wname, rname = [name + x for x in ["__write", "__read"]] widget_w = self.widget(wname) widget_r = self.widget(rname) if not widget_w or not widget_r: widget_r = self.add_widget(name=rname) widget_w = self.add_widget(name=wname) for w in [widget_r, widget_w]: w.set( "theme.minwidth", self._ioformat.format( ops=9999, band=util.format.bytefmt(999.99 * (1024 ** 2)) ), ) widget_w.full_text( self._ioformat.format( ops=round(writes), band=util.format.bytefmt(nwritten) ) ) widget_r.full_text( self._ioformat.format( ops=round(reads), band=util.format.bytefmt(nread) ) ) def state(self, widget): if widget.name.endswith("__read"): return "poolread" elif widget.name.endswith("__write"): return "poolwrite" state = widget.get("state") if state == "FAULTED": return [state, "critical"] elif state == "DEGRADED" or widget.get("percentfree") < self._warnfree: return [state, "warning"] return state @staticmethod def _shortstatus(status): # From `zpool(8)`, section Device Failure and Recovery: # A pool's health status is described by one of three states: online, degraded, or faulted. # An online pool has all devices operating normally. A degraded pool is one in which one # or more devices have failed, but the data is still available due to a redundant # configuration. A faulted pool has corrupted metadata, or one or more faulted devices, and # insufficient replicas to continue functioning. shortstate = { "DEGRADED": "DEG", "FAULTED": "FLT", "ONLINE": "ONL", } try: return shortstate[status] except KeyError: return "" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/000077500000000000000000000000001450256260500227105ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/core/__init__.py000066400000000000000000000000001450256260500250070ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/core/cpu.py000066400000000000000000000035721450256260500240600ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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 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)) core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) @property 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 update(self): for idx, cpu_perc in enumerate(self.cpu_utilization()): self.widgets()[idx].set("utilization", cpu_perc) def state(self, widget): return self.threshold_state(widget.get("utilization", 0.0), 70, 80) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/date.py000066400000000000000000000007751450256260500242100ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current date and time. Parameters: * date.format: strftime()-compatible formatting string * date.locale: locale to use rather than the system default """ import core.decorators from .datetime import Module class Module(Module): @core.decorators.every(hours=1) def __init__(self, config, theme): super().__init__(config, theme) def default_format(self): return "%x" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/datetime.py000066400000000000000000000027361450256260500250660ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current date and time. Parameters: * datetime.format: strftime()-compatible formatting string * datetime.locale: locale to use rather than the system default """ from __future__ import absolute_import import datetime import locale import core.module import core.widget import core.input class Module(core.module.Module): def __init__(self, config, theme, dtlibrary=None): 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() 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(".")) except Exception as e: locale.setlocale(locale.LC_ALL, ("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) if hasattr(retval, "decode"): return retval.decode(enc) return retval # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/debug.py000066400000000000000000000007671450256260500243620ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Shows that debug is enabled""" import platform import core.module import core.widget import core.decorators class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.full_text)) def full_text(self, widgets): return "debug" def state(self, widget): return "warning" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/disk.py000066400000000000000000000043041450256260500242150ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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.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 import core.module import core.widget import core.input import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.diskspace)) 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 self._size = 0 self._percent = 0 core.input.register( self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.parameter("open", "xdg-open"), self._path), ) 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) percent_str = self._percent return self._format.format( path=self._path, used=used_str, left=left_str, size=size_str, percent=percent_str, ) def update(self): st = os.statvfs(self._path) self._size = st.f_blocks * st.f_frsize self._used = (st.f_blocks - st.f_bfree) * st.f_frsize self._left = self._size - self._used self._percent = 100.0 * self._used / self._size def state(self, widget): return self.threshold_state(self._percent, 80, 90) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/error.py000066400000000000000000000013571450256260500244210ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Shows bumblebee-status errors""" import platform import core.module import core.widget import core.event class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.full_text)) self.__error = "" self.__state = "critical" core.event.register("error", self.__set_error) def full_text(self, widgets): return self.__error def __set_error(self, error="n/a", state="critical"): self.__error = error self.__state = state def state(self, widget): if self.__error: return [self.__state] return [] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/git.py000066400000000000000000000041321450256260500240450ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Print the branch and git status for the currently focused window. Requires: * xcwd * Python module 'pygit2' """ import os import pygit2 import core.module import util.cli class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self.__error = False def hidden(self): return self.__error def update(self): state = {} self.clear_widgets() try: directory = util.cli.execute("xcwd").strip() directory = self.__get_git_root(directory) repo = pygit2.Repository(directory) self.add_widget(name="git.main", full_text=repo.head.shorthand) for filepath, flags in repo.status().items(): if ( flags == pygit2.GIT_STATUS_WT_NEW or flags == pygit2.GIT_STATUS_INDEX_NEW ): state["new"] = True if ( flags == pygit2.GIT_STATUS_WT_DELETED or flags == pygit2.GIT_STATUS_INDEX_DELETED ): state["deleted"] = True if ( flags == pygit2.GIT_STATUS_WT_MODIFIED or flags == pygit2.GIT_STATUS_INDEX_MODIFIED ): state["modified"] = True self.__error = False if "new" in state: self.add_widget(name="git.new") if "modified" in state: self.add_widget(name="git.modified") if "deleted" in state: self.add_widget(name="git.deleted") except Exception as e: self.__error = True def state(self, widget): return widget.name.split(".")[1] def __get_git_root(self, directory): while len(directory) > 1: if os.path.exists(os.path.join(directory, ".git")): return directory directory = "/".join(directory.split("/")[0:-1]) return "/" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/keys.py000066400000000000000000000027011450256260500242350ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/core/layout-xkb.py000066400000000000000000000041631450256260500253650ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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. """ from xkbgroup import * import logging log = logging.getLogger(__name__) import core.module import core.widget import core.input import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.current_layout)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_keymap) self.__show_variant = util.format.asbool(self.parameter("show_variant", True)) def __next_keymap(self, event): self.__set_keymap(1) def __prev_keymap(self, event): self.__set_keymap(-1) def __set_keymap(self, rotation): xkb = XKeyboard() if xkb.groups_count < 2: return # nothing to do layouts = xkb.groups_symbols idx = layouts.index(xkb.group_symbol) xkb.group_symbol = str(layouts[(idx + rotation) % len(layouts)]) def current_layout(self, widget): try: xkb = XKeyboard() log.debug("group num: {}".format(xkb.group_num)) name = ( xkb.group_name if util.format.asbool(self.parameter("showname", False)) else xkb.group_symbol ) if self.__show_variant: return ( "{} ({})".format(name, xkb.group_variant) if xkb.group_variant else name ) return name except Exception as e: print("got exception: {}".format(e)) return "n/a" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/layout.py000077700000000000000000000000001450256260500272432layout-xkb.pyustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/core/layout_xkb.py000077700000000000000000000000001450256260500301072layout-xkb.pyustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/modules/core/load.py000066400000000000000000000024331450256260500242030ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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) """ import os import multiprocessing import core.module import core.input class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.load)) self._load = [0, 0, 0] try: 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" ) def load(self, widget): return "{:.02f}/{:.02f}/{:.02f}".format( self._load[0], self._load[1], self._load[2] ) def update(self): self._load = os.getloadavg() def state(self, widget): return self.threshold_state(self._load[0], self._cpus * 0.7, self._cpus * 0.8) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/memory.py000066400000000000000000000055421450256260500246000ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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%) * memory.format: Format string (defaults to '{used}/{total} ({percent:05.02f}%)') * memory.usedonly: Only show the amount of RAM in use (defaults to False). Same as memory.format='{used}' """ import re import core.module import core.widget import core.input import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.memory_usage)) core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) @property def _format(self): if util.format.asbool(self.parameter("usedonly", False)): return "{used}" else: return self.parameter("format", "{used}/{total} ({percent:05.02f}%)") def memory_usage(self, widget): return self._format.format(**self._mem) def update(self): data = self.__parse_meminfo() if "MemAvailable" in data: used = data["MemTotal"] - data["MemAvailable"] else: used = ( data["MemTotal"] - data["MemFree"] - data["Buffers"] - data["Cached"] - data["Slab"] ) self._mem = { "total": util.format.byte(data["MemTotal"]), "available": util.format.byte(data["MemAvailable"]), "free": util.format.byte(data["MemFree"]), "used": util.format.byte(used), "percent": float(used) / float(data["MemTotal"]) * 100.0, } def state(self, widget): if self._mem["percent"] > float(self.parameter("critical", 90)): return "critical" if self._mem["percent"] > float(self.parameter("warning", 80)): 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 bumblebee-status-2.2.0/bumblebee_status/modules/core/nic.py000066400000000000000000000150401450256260500240330ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """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.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) """ import re import shutil import netifaces import subprocess import core.module import core.decorators import util.cli import util.format class Module(core.module.Module): @core.decorators.every(seconds=5) 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._include = util.format.aslist(self.parameter("include", "")) self._states = {"include": [], "exclude": []} for state in tuple( filter(len, util.format.aslist(self.parameter("states", ""))) ): if state[0] == "^": 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._update_widgets(widgets) def update(self): self._update_widgets(self.widgets()) def state(self, widget): states = [] if widget.get("state") == "down": states.append("critical") elif widget.get("state") != "up": states.append("warning") intf = widget.get("intf") 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 if intf.startswith("w"): return True return False def _istunnel(self, intf): return intf.startswith("tun") or intf.startswith("wg") def get_addresses(self, intf): retval = [] try: for ip in netifaces.ifaddresses(intf).get(netifaces.AF_INET, []): if ip.get("addr", "") != "": retval.append(ip.get("addr")) except Exception: 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.extend([i for i in netifaces.interfaces() if i in self._include]) for intf in interfaces: addr = [] state = "down" for ip in self.get_addresses(intf): addr.append(ip) state = "up" if len(self._states["exclude"]) > 0 and state in self._states["exclude"]: continue if ( len(self._states["include"]) > 0 and state not in self._states["include"] ): 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) # join/split is used to get rid of multiple whitespaces (in case SSID is not available, for instance widget.full_text( " ".join( self._format.format( 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) 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 bumblebee-status-2.2.0/bumblebee_status/modules/core/pasink.py000066400000000000000000000003061450256260500245460ustar00rootroot00000000000000from .pulseaudio import Module class Module(Module): def __init__(self, config, theme): super().__init__(config, theme, "sink") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/pasource.py000066400000000000000000000003101450256260500250750ustar00rootroot00000000000000from .pulseaudio import Module class Module(Module): def __init__(self, config, theme): super().__init__(config, theme, "source") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/ping.py000066400000000000000000000055021450256260500242210ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Periodically checks the RTT of a configurable host using ICMP echos Requires the following executable: * ping Parameters: * ping.address : IP address to check * ping.timeout : Timeout for waiting for a reply (defaults to 5.0) * ping.probes : Number of probes to send (defaults to 5) * ping.warning : Threshold for warning state, in seconds (defaults to 1.0) * ping.critical: Threshold for critical state, in seconds (defaults to 2.0) """ import re import time import core.module import core.widget import core.event import core.decorators import util.cli class Module(core.module.Module): @core.decorators.every(seconds=60) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.rtt)) self.background = True widget = self.widget() widget.set("address", self.parameter("address", "8.8.8.8")) widget.set("rtt-probes", self.parameter("probes", 5)) widget.set("rtt-timeout", self.parameter("timeout", 5.0)) widget.set("rtt-avg", 0.0) widget.set("rtt-unit", "") widget.set("packet-loss", 0) def rtt(self, widget): if widget.get("rtt-unreachable"): return "{}: unreachable".format(widget.get("address")) return "{}: {:.1f}{} ({}%)".format( widget.get("address"), widget.get("rtt-avg"), widget.get("rtt-unit"), widget.get("packet-loss"), ) def state(self, widget): if widget.get("rtt-unreachable"): return ["critical"] return self.threshold_state(widget.get("rtt-avg"), 1000.0, 2000.0) def update(self): widget = self.widget() try: widget.set("rtt-unreachable", False) res = util.cli.execute( "ping -n -q -c {} -W {} {}".format( widget.get("rtt-probes"), widget.get("rtt-timeout"), widget.get("address"), ) ) for line in res.split("\n"): if line.startswith( "{} packets transmitted".format(widget.get("rtt-probes")) ): m = re.search(r"(\d+)% packet loss", line) widget.set("packet-loss", m.group(1)) if not line.startswith("rtt"): continue m = re.search(r"([0-9\.]+)/([0-9\.]+)/([0-9\.]+)/([0-9\.]+)\s+(\S+)", line) widget.set("rtt-min", float(m.group(1))) widget.set("rtt-avg", float(m.group(2))) widget.set("rtt-max", float(m.group(3))) widget.set("rtt-unit", m.group(5)) except Exception as e: widget.set("rtt-unreachable", True) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/pulseaudio.py000066400000000000000000000234171450256260500254430ustar00rootroot00000000000000# 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. !!! 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: * pulseaudio.autostart: If set to 'true' (default is 'false'), automatically starts the pulseaudio daemon if it is not running * pulseaudio.percent_change: How much to change volume by when scrolling on the module (default is 2%) * pulseaudio.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') 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 * pactl * pavucontrol """ import re import logging import functools import core.module import core.widget import core.input 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)) if util.format.asbool(self.parameter("autostart", False)): util.cli.execute("pulseaudio --start", ignore_errors=True) self._change = util.format.asint( self.parameter("percent_change", "2%").strip("%"), 0, 100 ) self._limit = util.format.asint(self.parameter("limit", "0%").strip("%"), 0) self._left = 0 self._right = 0 self._mono = 0 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)}, { "expr": "Mute:", "callback": ( lambda line: self.mute(False if " no" in line.lower() else True) ), }, {"expr": "Volume:", "callback": self.getvolume}, ] core.input.register(self, button=core.input.RIGHT_MOUSE, cmd="pavucontrol") 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 set_volume(self, amount): util.cli.execute( "pactl set-{}-{} @DEFAULT_{}@ {}".format( self._channel, "volume", self._channel.upper(), amount ) ) def increase_volume(self, event): if self._limit > 0: # we need to check the limit left = int(self._left) right = int(self._right) if ( left + self._change >= self._limit or right + self._change >= self._limit ): if left == right: # easy case, just set to limit self.set_volume("{}%".format(self._limit)) return else: # don't adjust anymore, since i don't know how to update only one channel return self.set_volume("+{}%".format(self._change)) def decrease_volume(self, event): self.set_volume("-{}%".format(self._change)) def toggle(self, event): util.cli.execute( "pactl set-{}-mute @DEFAULT_{}@ toggle".format( self._channel, self._channel.upper() ) ) def mute(self, value): self._mute = value def getvolume(self, line): if "mono" in line: 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) def _default_device(self): output = util.cli.execute("pactl info") pattern = "Default {}: ".format("Sink" if self._channel == "sink" else "Source") for line in output.split("\n"): if line.startswith(pattern): return line.replace(pattern, "") logging.error("no pulseaudio device found") return "n/a" def display(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))) elif self._left == self._right: vol = "{}%".format(self._left) if self._showbars: vol = "{} {}".format(vol, util.graph.hbar(float(self._left))) else: vol = "{}%/{}%".format(self._left, self._right) if self._showbars: vol = "{} {}{}".format( vol, 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 def update(self): try: self._failed = False channel = "sinks" if self._channel == "sink" else "sources" self.__selected_default_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: found = True continue if found is False: continue for pattern in self._patterns: if not pattern["expr"] in line: continue if pattern["callback"](line) is False and found == True: return except Exception as e: self._failed = True logging.exception(e) if util.format.asbool(self.parameter("autostart", False)): util.cli.execute("pulseaudio --start", ignore_errors=True) 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"] return ["unmuted"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/pulsectl.py000066400000000000000000000173171450256260500251260ustar00rootroot00000000000000# 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"] return ["unmuted"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/pulsein.py000066400000000000000000000003061450256260500247400ustar00rootroot00000000000000from .pulsectl import Module class Module(Module): def __init__(self, config, theme): super().__init__(config, theme, "source") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/pulseout.py000066400000000000000000000003041450256260500251370ustar00rootroot00000000000000from .pulsectl import Module class Module(Module): def __init__(self, config, theme): super().__init__(config, theme, "sink") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/redshift.py000066400000000000000000000074321450256260500251000ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current color temperature of redshift Requires the following executable: * redshift Parameters: * redshift.location : location provider, either of 'auto' (default), 'geoclue2', 'ipinfo' or 'manual' 'auto' uses whatever redshift is configured to do * 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 import threading import core.module import core.widget import core.input import core.decorators import util.cli import util.format import util.location def get_redshift_value(module): widget = module.widget() location = module.parameter("location", "auto") lat = module.parameter("lat", None) lon = module.parameter("lon", None) # Even if location method is set to manual, if we have no lat or lon, # fall back to the geoclue2 method. 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") if location == "manual": command.extend(["-l", "{}:{}".format(lat, lon)]) if location == "geoclue2": command.extend(["-l", "geoclue2"]) try: res = util.cli.execute(" ".join(command)) except Exception: res = "" widget.set("temp", "n/a") widget.set("transition", "") widget.set("state", "day") for line in res.split("\n"): line = line.lower() if "temperature" in line: widget.set("temp", line.split(" ")[2].upper()) if "period" in line: state = line.split(" ")[1] if "day" in state: widget.set("state", "day") elif "night" in state: widget.set("state", "night") else: widget.set("state", "transition") match = re.search(r"(\d+)\.\d+% ([a-z]+)", line) widget.set( "transition", "({}% {})".format(match.group(1), match.group(2)) ) core.event.trigger("update", [widget.module.id], redraw_only=True) class Module(core.module.Module): @core.decorators.every(seconds=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.text)) self.__thread = None self.show_transition = util.format.asbool( self.parameter("show_transition", True) ) if self.parameter("location", "") == "ipinfo": # override lon/lat with ipinfo try: location = util.location.coordinates() self.set("lat", location[0]) self.set("lon", location[1]) self.set("location", "manual") except Exception: # Fall back to geoclue2. self.set("location", "geoclue2") self._text = "" def text(self, widget): val = widget.get("temp", "n/a") transition = widget.get("transition", "") if transition and self.show_transition: val = "{} {}".format(val, transition) return val def update(self): if self.__thread is not None and self.__thread.is_alive(): return self.__thread = threading.Thread(target=get_redshift_value, args=(self,)) self.__thread.start() def state(self, widget): return widget.get("state", None) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/scroll.py000066400000000000000000000031321450256260500245570ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/core/sensors2.py000066400000000000000000000232121450256260500250400ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """Displays sensor temperature and CPU frequency Parameters: * sensors2.chip: 'sensors -u' compatible filter for chip to display (default to empty - show all chips) * sensors2.showcpu: Enable or disable CPU frequency display (default: true) * sensors2.showtemp: Enable or disable temperature display (default: true) * sensors2.showfan: Enable or disable fan display (default: true) * 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.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') * sensors2.chip_field_include: Comma-separated list of adaper field to include (defaults to '' will include all by default) """ import re import core.module import core.widget import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self.__chip = self.parameter("chip", "") self.__data = {} self.__update() self.__create_widgets() def update(self): self.__update() for widget in self.widgets(): self.__update_widget(widget) def state(self, widget): widget_type = widget.get("type", "") try: data = self.__data[widget.get("adapter")][widget.get("package")][ widget.get("field") ] if "crit" in data and float(data["input"]) > float(data["crit"]): return ["critical", widget_type] if "max" in data and float(data["input"]) > float(data["max"]): return ["warning", widget_type] except: pass return [widget_type] def __create_widgets(self): show_temp = util.format.asbool(self.parameter("showtemp", True)) show_fan = util.format.asbool(self.parameter("showfan", True)) show_other = util.format.asbool(self.parameter("showother", False)) include_chip = tuple( filter(len, util.format.aslist(self.parameter("chip_include", ""))) ) exclude_chip = tuple( filter(len, util.format.aslist(self.parameter("chip_exclude", ""))) ) include_field = tuple( filter(len, util.format.aslist(self.parameter("field_include", ""))) ) exclude_field = tuple( filter(len, util.format.aslist(self.parameter("field_exclude", ""))) ) include_chip_field = tuple( filter(len, util.format.aslist(self.parameter("chip_field_include", ""))) ) exclude_chip_field = tuple( filter(len, util.format.aslist(self.parameter("chip_field_exclude", ""))) ) if util.format.asbool(self.parameter("showcpu", True)): widget = self.add_widget(full_text=self.__cpu) widget.set("type", "cpu") for adapter in self.__data: if include_chip or exclude_chip: if include_chip: if all([chip not in adapter for chip in include_chip]): continue else: if any([chip in adapter for chip in exclude_chip]): continue if include_chip_field: try: if all( [i.split(".")[0] not in adapter for i in include_chip_field] ): continue except: pass for package in self.__data[adapter]: if util.format.asbool(self.parameter("showname", False)): widget = self.add_widget(full_text=package) widget.set("data", self.__data[adapter][package]) widget.set("package", package) widget.set("field", "") widget.set("adapter", adapter) for field in self.__data[adapter][package]: if include_field or exclude_field: if include_field: if all( [included not in field for included in include_field] ): continue else: if any([excluded in field for excluded in exclude_field]): continue try: if include_chip_field or exclude_chip_field: if include_chip_field: if all( [ i.split(".")[1] not in field for i in include_chip_field if i.split(".")[0] in adapter ] ): continue else: if any( [ i.split(".")[1] in field for i in exclude_chip_field if i.split(".")[0] in adapter ] ): continue except: pass widget = None if "temp" in field and show_temp: # seems to be a temperature widget = self.add_widget() widget.set("type", "temp") if "fan" in field and show_fan: # seems to be a fan widget = self.add_widget() widget.set("type", "fan") elif show_other: # everything else widget = self.add_widget() widget.set("type", "other") if widget: widget.set("package", package) widget.set("field", field) widget.set("adapter", adapter) def __update_widget(self, widget): if widget.get("field", "") == "": return # nothing to do data = self.__data[widget.get("adapter")][widget.get("package")][ widget.get("field") ] if "temp" in widget.get("field"): widget.full_text("{:0.01f}°C".format(data["input"])) elif "fan" in widget.get("field"): widget.full_text("{:0.0f}RPM".format(data["input"])) else: widget.full_text("{:0.0f}".format(data["input"])) def __update(self): output = util.cli.execute( "sensors -u {}".format(self.__chip), ignore_errors=True ) self.__data = self.__parse(output) def __parse(self, data): output = {} package = "" adapter = None chip = None for line in data.split("\n"): if "Adapter" in line: # new adapter line = line.replace("Adapter: ", "") output[chip + " " + line] = {} adapter = chip + " " + line chip = line # default - line before adapter is always the chip if not adapter: continue key, value = (line.split(":") + ["", ""])[:2] if not line.startswith(" "): # assume this starts a new package if package in output[adapter] and output[adapter][package] == {}: del output[adapter][package] output[adapter][key] = {} package = key else: # feature for this chip try: name, variant = (key.strip().split("_", 1) + ["", ""])[:2] if not name in output[adapter][package]: output[adapter][package][name] = {} if variant: output[adapter][package][name][variant] = {} output[adapter][package][name][variant] = float(value) except Exception as e: pass return output def __cpu(self, _): mhz = None try: output = open( "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq" ).read() mhz = int(float(output) / 1000.0) except: output = open("/proc/cpuinfo").read() m = re.search(r"cpu MHz\s+:\s+(\d+)", output) if m: mhz = int(m.group(1)) else: m = re.search(r"BogoMIPS\s+:\s+(\d+)", output) if m: return "{} BogoMIPS".format(int(m.group(1))) if not mhz: return "n/a" if mhz < 1000: return "{} MHz".format(mhz) else: return "{:0.01f} GHz".format(float(mhz) / 1000.0) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/spacer.py000066400000000000000000000012451450256260500245410ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Draws a widget with configurable text content. Parameters: * spacer.text: Widget contents (defaults to empty string) """ import core.module import core.widget import core.decorators import core.input class Module(core.module.Module): @core.decorators.every(minutes=60) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.text)) self.__text = self.parameter("text", "") 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 bumblebee-status-2.2.0/bumblebee_status/modules/core/speedtest.py000066400000000000000000000032001450256260500252550ustar00rootroot00000000000000# 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 bumblebee-status-2.2.0/bumblebee_status/modules/core/test.py000066400000000000000000000004651450256260500242460ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Test module """ import core.widget import core.module class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config=config, theme=theme, widgets=core.widget.Widget("test")) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/time.py000066400000000000000000000007311450256260500242210ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Displays the current date and time. Parameters: * time.format: strftime()-compatible formatting string * time.locale: locale to use rather than the system default """ import core.decorators from .datetime import Module class Module(Module): def __init__(self, config, theme): super().__init__(config, theme) def default_format(self): return "%X" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/vault.py000066400000000000000000000070471450256260500244250ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Copy passwords from a password store into the clipboard (currently supports only 'pass') 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! """ # TODO: # - support multiple backends by abstracting the menu structure into a tree # - build the menu and the actions based on that abstracted tree # import os import time import threading import core.module import core.widget import core.input import core.event import util.cli import util.popup def generate_callback(callback, path, name): return lambda: callback(os.path.join(path, name)) def build_menu(parent, current_directory, callback): with os.scandir(current_directory) as it: for entry in it: if entry.name.startswith("."): continue if entry.is_file(): name = entry.name[: entry.name.rfind(".")] parent.add_menuitem( name, callback=generate_callback(callback, current_directory, name), ) else: submenu = util.popup.menu(self.__config, parent, leave=False) build_menu( submenu, os.path.join(current_directory, entry.name), callback ) parent.add_cascade(entry.name, submenu) class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.text)) self.__duration = int(self.parameter("duration", 30)) self.__offx = int(self.parameter("offx", 0)) self.__offy = int(self.parameter("offy", 0)) self.__path = os.path.expanduser( self.parameter("location", "~/.password-store/") ) self.__reset() core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) def popup(self, widget): menu = util.popup.menu(self.__config, leave=False) build_menu(menu, self.__path, self.__callback) menu.show(widget, offset_x=self.__offx, offset_y=self.__offy) def __reset(self): self.__timer = None self.__text = str(self.parameter("text", "")) def __callback(self, secret_name): secret_name = secret_name.replace(self.__path, "") # remove common path if self.__timer: self.__timer.cancel() env = os.environ env["PASSWORD_STORE_CLIP_TIME"] = str(self.__duration) res = util.cli.execute( "pass show -c {}".format(secret_name), wait=False, env=env, ignore_errors=True, ) self.__timer = threading.Timer(self.__duration, self.__reset) self.__timer.start() self.__start = int(time.time()) self.__text = secret_name def text(self, widget): if self.__timer: return "{} ({}s)".format( self.__text, self.__duration - (int(time.time()) - self.__start) ) return self.__text # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/modules/core/xrandr.py000066400000000000000000000141041450256260500245600ustar00rootroot00000000000000# pylint: disable=C0111,R0903 """Shows a widget for each connected screen and allows the user to enable/disable screens. Parameters: * xrandr.overwrite_i3config: If set to 'true', this module assembles a new i3 config every time a screen is enabled or disabled by taking the file '~/.config/i3/config.template' 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 Requires the following executable: * xrandr """ import re import sys import core.module import core.input import core.decorators from bumblebee_status.discover import utility import util.cli import util.format try: import i3 except Exception: 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 try: i3.Subscription(self._output_update, "output") except Exception: 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 self.clear_widgets() for display in self._query_displays(): if display.name.startswith(self._exclude): continue 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) 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: widget = self.add_widget(full_text="") widget.set("state", "refresh") core.input.register(widget, button=1, cmd=self.update) self._initialized = True 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 _toggle(self, event): widget = self.widget(widget_id=event["instance"]) if widget.get("state") == "on": self._disable_display(widget.name) else: side = "left" if event["button"] == core.input.LEFT_MOUSE else "right" self._enable_display(widget.name, side) self.update(force=True) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/000077500000000000000000000000001450256260500212655ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/util/__init__.py000066400000000000000000000000001450256260500233640ustar00rootroot00000000000000bumblebee-status-2.2.0/bumblebee_status/util/algorithm.py000066400000000000000000000015521450256260500236300ustar00rootroot00000000000000import copy def merge(target, *args): """Merges arbitrary data - copied from http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts :param target: the data structure to fill :param args: a list of data structures to merge into target :return: target, with all data in args merged into it :rtype: whatever type was originally passed in """ if len(args) > 1: for item in args: merge(target, item) return target item = args[0] if not isinstance(item, dict): return item for key, value in item.items(): if key in target and isinstance(target[key], dict): merge(target[key], value) else: if not key in target: target[key] = copy.deepcopy(value) return target # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/cli.py000066400000000000000000000056061450256260500224150ustar00rootroot00000000000000import os import shlex import subprocess import logging def execute( cmd, wait=True, ignore_errors=False, include_stderr=False, env=None, return_exitcode=False, shell=False, ): """Executes a commandline utility and returns its output :param cmd: the command (as string) to execute, returns the program's output :param wait: set to True to wait for command completion, False to return immediately, defaults to True :param ignore_errors: set to True to return a string when an exception is thrown, otherwise might throw, defaults to False :param include_stderr: set to True to include stderr output in the return value, defaults to False :param env: provide a dict here to specify a custom execution environment, defaults to None :param return_exitcode: set to True to return a pair, where the first member is the exit code and the message the second, defaults to False :param shell: set to True to run command in a separate shell, defaults to False :raises RuntimeError: the command either didn't exist or didn't exit cleanly, and ignore_errors was set to False :return: output of cmd, or stderr, if ignore_errors is True and the command failed; or a tuple of exitcode and the previous, if return_exitcode is set to True :rtype: string or tuple (if return_exitcode is set to True) """ 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, 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() if proc.returncode != 0: err = "{} exited with code {}".format(cmd, proc.returncode) logging.warning(err) if ignore_errors: return (proc.returncode, err) if return_exitcode else err raise RuntimeError(err) res = out.decode("utf-8") logging.debug(res) return (proc.returncode, res) if return_exitcode else res return (0, "") if return_exitcode else "" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/format.py000066400000000000000000000107151450256260500231330ustar00rootroot00000000000000# -*- coding: utf-8 -*- import re def asbool(val): """Converts a value into a boolean :param val: value to convert; accepts a wide range of possible representations, such as yes, no, true, false, on, off :return: True of val maps to true, False otherwise :rtype: boolean """ if val is None: return False if isinstance(val, bool): return val val = str(val).strip().lower() return val in ("t", "true", "y", "yes", "on", "1") def asint(val, minimum=None, maximum=None): """Converts a value into an integer :param val: value to convert :param minimum: if specified, this determines the lower boundary for the returned value, defaults to None :param maximum: if specified, this determines the upper boundary for the returned value, defaults to None :return: integer representation of value :rtype: integer """ if val is None: val = 0 val = int(val) val = min(val, maximum if maximum else val) val = max(val, minimum if minimum else val) return val def aslist(val): """Converts a comma-separated value string into a list :param val: value to convert, either a single value or a comma-separated string :return: list representation of the value passed in :rtype: list of strings """ if val is None: return [] if isinstance(val, list): return val return str(val).replace(" ", "").split(",") __UNITS = {"metric": "C", "kelvin": "K", "imperial": "F", "default": "C"} def astemperature(val, unit="metric"): """Returns a temperature representation of the input value :param val: value to format, must be convertible into an integer :param unit: unit of the input value, supported units are: metric, kelvin, imperial, defaults to metric :return: temperature representation of the input value :rtype: string """ return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"])) def byte(val, fmt="{:.2f}", sys="IEC"): """Returns a byte representation of the input value :param val: value to format, must be convertible into a float :param fmt: optional output format string, defaults to {:.2f} :param sys: optional unit system specifier - SI (kilo, Mega, Giga, ...) or IEC (kibi, Mebi, Gibi, ...) - defaults to IEC :return: byte representation (e.g. 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: return "{}{}B".format(fmt, unit).format(val) val /= div return "{}{}".format(fmt).format(val * div, final) __seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?") def seconds(duration): """Returns a time duration (in seconds) representation of the input value :param duration: value to format (e.g. 5h30m2s) :return: duration in seconds of the input value :rtype: float """ if isinstance(duration, int) or isinstance(duration, float): return float(duration) matches = __seconds_pattern.match(duration) result = 0.0 if matches.group(2): result += float(matches.group(2)) * 3600 # hours if matches.group(4): result += float(matches.group(4)) * 60 # minutes if matches.group(5): result += float(matches.group(5)) # seconds return result def duration(duration, compact=False, unit=False): """Returns a time duration string representing the input value :param duration: value to format, must be convertible into an into :param compact: whether to show also seconds, defaults to False :param unit: whether to display he unit, defaults to False :return: duration representation (e.g. 5:02s) of the input value :rtype: string """ duration = int(duration) if duration < 0: return "n/a" minutes, seconds = divmod(duration, 60) hours, minutes = divmod(minutes, 60) suf = "m" res = "{:02d}:{:02d}".format(minutes, seconds) if hours > 0: if compact: res = "{:02d}:{:02d}".format(hours, minutes) else: res = "{:02d}:{}".format(hours, res) suf = "h" return "{}{}".format(res, suf if unit else "") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/graph.py000066400000000000000000000124201450256260500227370ustar00rootroot00000000000000MAX_PERCENTS = 100.0 class Bar(object): bars = None def __init__(self, value): self.value = value class HBar(Bar): bars = [ "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588", ] """This class is a helper class used to draw horizontal bars - please use hbar directly :param value: percentage value to draw (float, between 0 and 100) """ def __init__(self, value): super(HBar, self).__init__(value) self.step = MAX_PERCENTS / len(HBar.bars) def get_char(self): """Returns the character representing the current object's value :return: character representing the value passed during initialization :rtype: string with one character """ for i in range(len(HBar.bars)): left = i * self.step right = (i + 1) * self.step if left <= self.value < right: return self.bars[i] return self.bars[-1] def hbar(value): """"Retrieves the horizontal bar character representing the input value :param value: percentage value to draw (float, between 0 and 100) :return: character representing the value passed during initialization :rtype: string with one character """ return HBar(value).get_char() class VBar(Bar): bars = [ "\u258f", "\u258e", "\u258d", "\u258c", "\u258b", "\u258a", "\u2589", "\u2588", ] """This class is a helper class used to draw vertical bars - please use vbar directly :param value: percentage value to draw (float, between 0 and 100) :param width: maximum width of the bar in characters """ def __init__(self, value, width=1): super(VBar, self).__init__(value) self.step = MAX_PERCENTS / (len(VBar.bars) * width) self.width = width """Returns the characters representing the current object's value :return: characters representing the value passed during initialization :rtype: string """ def get_chars(self): if self.value == 100: return self.bars[-1] * self.width if self.width == 1: for i in range(len(VBar.bars)): left = i * self.step right = (i + 1) * self.step if left <= self.value < right: return self.bars[i] else: full_parts = int(self.value // (self.step * len(Vbar.bars))) remainder = self.value - full_parts * self.step * CHARS empty_parts = self.width - full_parts if remainder >= 0: empty_parts -= 1 part_vbar = VBar(remainder * self.width) # scale to width chars = self.bars[-1] * full_parts chars += part_vbar.get_chars() chars += " " * empty_parts return chars def vbar(value, width): """Returns the characters representing the current object's value :param value: percentage value to draw (float, between 0 and 100) :param width: maximum width of the bar in characters :return: characters representing the value passed during initialization :rtype: string """ return VBar(value, width).get_chars() class BrailleGraph(object): chars = { (0, 0): " ", (1, 0): "\u2840", (2, 0): "\u2844", (3, 0): "\u2846", (4, 0): "\u2847", (0, 1): "\u2880", (0, 2): "\u28a0", (0, 3): "\u28b0", (0, 4): "\u28b8", (1, 1): "\u28c0", (2, 1): "\u28c4", (3, 1): "\u28c6", (4, 1): "\u28c7", (1, 2): "\u28e0", (2, 2): "\u28e4", (3, 2): "\u28e6", (4, 2): "\u28e7", (1, 3): "\u28f0", (2, 3): "\u28f4", (3, 3): "\u28f6", (4, 3): "\u28f7", (1, 4): "\u28f8", (2, 4): "\u28fc", (3, 4): "\u28fe", (4, 4): "\u28ff", } """This class is a helper class used to draw braille graphs - please use braille directly :param values: values to draw """ def __init__(self, values): self.values = values # length of values list must be even # because one Braille char displays two values if len(self.values) % 2 == 1: self.values.append(0) self.steps = self.get_steps() self.parts = [tuple(self.steps[i : i + 2]) for i in range(len(self.steps))[::2]] @staticmethod def get_height(value, unit): if value < unit / 10.0: return 0 elif value <= unit: return 1 elif value <= unit * 2: return 2 elif value <= unit * 3: return 3 else: return 4 def get_steps(self): maxval = max(self.values) unit = maxval / 4.0 if unit == 0: return [0] * len(self.values) stepslist = [] for value in self.values: stepslist.append(self.get_height(value, unit)) return stepslist def get_chars(self): chars = [] for part in self.parts: chars.append(BrailleGraph.chars[part]) return "".join(chars) def braille(values): return BrailleGraph(values).get_chars() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/location.py000066400000000000000000000066701450256260500234600ustar00rootroot00000000000000"""Retrieves location information from an external 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 """ import json import time import urllib.request __document = None __data = {} __next = 0 __sources = [ { "url": "http://free.ipwhois.io/json/", "mapping": { "latitude": "latitude", "longitude": "longitude", "country": "country_name", "country_code": "country_code", "city": "city_name", "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", "ip": "public_ip", }, } ] def __expired(): global __next return __next <= time.time() def __load(): global __data global __next __data = {} for src in __sources: try: tmp = json.loads(urllib.request.urlopen(src["url"]).read()) for k, v in src["mapping"].items(): __data[v] = tmp.get(k, None) __next = time.time() + 60 * 60 * 12 # update once every 12h return except Exception as e: pass __next = time.time() + 60 * 30 # error - try again every 30m def __get(name): global __data if not __data or __expired(): __load() if name in __data: return __data[name] else: return None def reset(): """Resets the location library, ensuring that a new query will be started""" global __next global __data __data = None __next = 0 def coordinates(): """Returns a latitude, longitude pair :return: current latitude and longitude :rtype: pair of strings """ return __get("latitude"), __get("longitude") def country(): """Returns the current country name :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") def public_ip(): """Returns the current public IP :return: public IP :rtype: string """ 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 bumblebee-status-2.2.0/bumblebee_status/util/popup.py000066400000000000000000000061371450256260500230110ustar00rootroot00000000000000"""Pop-up menus.""" import logging import tkinter as tk import tkinter.font as tkFont 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): self.running = True 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() self._menu.bind("", self.release) """Returns the root node of this menu :return: root node """ def root(self): return self._root """Returns the menu :return: menu """ def menu(self): return self._menu def __on_focus_out(self, event=None): self.running = False self._root.destroy() def __on_click(self, callback): self._root.destroy() callback() def release(self, event=None): self.running=False if self.parent: self.parent.release(event) """Adds a cascading submenu to the current menu :param menuitem: label to display for the submenu :param submenu: submenu to show """ def add_cascade(self, menuitem, submenu): self._menu.add_cascade(label=menuitem, menu=submenu.menu(), font=self._font_size) """Adds an item to the current menu :param menuitem: label to display for the entry :param callback: method to invoke on click """ def add_menuitem(self, menuitem, callback): self._menu.add_command( label=menuitem, command=functools.partial(self.__on_click, callback), font=self._font_size, ) """Adds a separator to the menu in the current location""" def add_separator(self): self._menu.add_separator() """Shows this menu :param event: i3wm event that triggered the menu (dict that contains "x" and "y" fields) :param offset_x: x-axis offset from mouse position for the menu (defaults to 0) :param offset_y: y-axis offset from mouse position for the menu (defaults to 0) """ def show(self, event, offset_x=0, offset_y=0): try: self._menu.tk_popup(event["x"] + offset_x, event["y"] + offset_y) finally: self._menu.grab_release() while self.running == True: try: self._root.update_idletasks() self._root.update() except: self.running = False try: self._root.destroy() except: pass # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/store.py000066400000000000000000000024531450256260500227770ustar00rootroot00000000000000"""Store interface Allows arbitrary classes to offer a simple get/set store interface by deriving from the Store class in this module """ class Store(object): """Interface for storing and retrieving simple values""" def __init__(self): super(Store, self).__init__() self._data = {} def set(self, key, value): """Sets key to value, overwriting any existing data for that key :param key: the name of the parameter to set :param value: the value to be set """ self._data[key] = {"value": value, "used": False} def unused_keys(self): """Returns a list of unused keys :return: a list of keys that are set, but never used :rtype: list of strings """ return [key for key, value in self._data.items() if value["used"] == False] def get(self, key, default=None): """Returns the current value for the specified key, or a default value, if the key is not set :param key: the name of the parameter to retrieve :param default: the default value to return, defaults to None """ if key in self._data: self._data[key]["used"] = True return self._data.get(key, {"value": default})["value"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 bumblebee-status-2.2.0/bumblebee_status/util/xresources.py000066400000000000000000000004241450256260500240410ustar00rootroot00000000000000import 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") bumblebee-status-2.2.0/coverage.sh000077500000000000000000000001141450256260500171110ustar00rootroot00000000000000#!/bin/bash coverage run --source=. -m pytest tests -v coverage report -m bumblebee-status-2.2.0/create-pkgbuild.py000066400000000000000000000012051450256260500203750ustar00rootroot00000000000000#!/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) bumblebee-status-2.2.0/docs/000077500000000000000000000000001450256260500157135ustar00rootroot00000000000000bumblebee-status-2.2.0/docs/FAQ.rst000066400000000000000000000043051450256260500170560ustar00rootroot00000000000000.. contents:: FAQs ==== The new version has broken my setup! ----------------------------------------- First, sorry about that! Then, please open a bug report and I will try to fix the issue quickly. If that is not an acceptable solution, here are different ways to step back to the last stable version: git ``git checkout v1.10.4`` pip ``pip install --user --force-reinstall bumblebee-status==1.10.4`` aur ``# checkout rev. 7ac3dde7361c6a530141df2b3c0137f57d6b4f70 from https://aur.archlinux.org/bumblebee-status.git`` My bar doesn’t show any background colors ----------------------------------------- Please check that you are using i3wm 4.12 or later. Before that, i3wm 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). .. 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 Font Awesome removed some icons used by ``bumblebee-status`` from the free set in version 5, so if possible, stick with 4. .. code-block:: bash # Font Awesome installation instructions # Arch Linux $ sudo pacman -S awesome-terminal-fonts # FreeBSD $ sudo pkg install font-awesome $ sudo pkg install py36-tzlocal py36-pytz py36-netifaces py36-psutil py36-requests #for dependencies # 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/ bumblebee-status-2.2.0/docs/Makefile000066400000000000000000000011721450256260500173540ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) bumblebee-status-2.2.0/docs/api.rst000066400000000000000000000001261450256260500172150ustar00rootroot00000000000000API Reference ================ .. toctree:: :maxdepth: 4 src/bumblebee_status bumblebee-status-2.2.0/docs/conf.py000066400000000000000000000037631450256260500172230ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = "bumblebee-status" copyright = "2020, tobi-wan-kenobi" author = "tobi-wan-kenobi" master_doc = "index" import os import sys sys.path.insert(0, os.path.abspath("..")) # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # 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, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] bumblebee-status-2.2.0/docs/development/000077500000000000000000000000001450256260500202355ustar00rootroot00000000000000bumblebee-status-2.2.0/docs/development/general.rst000066400000000000000000000010421450256260500224010ustar00rootroot00000000000000General 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) 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. 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. :) bumblebee-status-2.2.0/docs/development/index.rst000066400000000000000000000002371450256260500221000ustar00rootroot00000000000000Developer's Guide ============================================ .. toctree:: :maxdepth: 2 :caption: Contents: general module theme testing bumblebee-status-2.2.0/docs/development/module.rst000066400000000000000000000224261450256260500222620ustar00rootroot00000000000000How to write a module ===================== Introduction ------------ Adding a new module to ``bumblebee-status`` is straight-forward: - Add a new Python module in ``bumblebee_status/modules/contrib/``. The name of the module will be the name that the user needs to specify when invoking ``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 ``black -t py34`` before submitting 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! Coding guidelines ----------------- I’m pretty open to whatever style you use, but if it’s all the same to you (and yes, I know that the current codebase is only slowly adapting to this): - Please favour double quotes for strings. For private methods/variables, please use a leading ``__`` (e.g. ``__output`` rather than ``_output``) For anything else, please run your code through `black `_. Hello world ----------- This example will show “hello world” in the status bar: .. code:: python """Short description in RST format please have a look at other modules, this will go into the documentation verbatim (list of modules) """ import core.module import core.widget class Module(core.module.Module): def __init__(self, config): super().__init__(config, core.widget.Widget(self.full_text)) def full_text(self, widgets): return 'hello world' # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 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. 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 :) Otherwise, you have a number of ways to handle widgets: - During the ``super().init__(...)`` inside the module’s constructor, you can specify a **list** of widgets, and those will comprise the widgets (in ordered fashion) - During runtime, you can set a new list of widgets by using the ``self.add_widget()`` method of the module to add new widgets and ``self.clear_widgets()`` method to remove all widgets. Adding widgets at runtime ------------------------- If you want to add widgets during runtime, please use the ``add_widget()`` method of the module: :: def do_something(self): self.add_widget(full_text="my sample text", name="") TODO: expand on this Periodic updates (update() vs. full_text) ----------------------------------------- ``bumblebee-status`` modules have two different ways to update their data: 1. Each interval, the callback registered when the widget was created is called. You can do arbitrarily complex things there 2. Each interval, **before** the widget’s callback is invoked, a generic ``update(self, widgets)`` method is called on the **module** Largely, where you want to put your update code is up to you. My observations: - If you want to change the widgets a module has, you **have** to stick with ``update()`` - For simple modules, doing the data update in the widget callback is simplest (see ``kernel``, for example) Widget states ------------- Each widget inside a module can have a list of states (for example, two predefined states are ``warning`` and ``critical``). States define how a widget is rendered (i.e. which fields in the theme file are selected to draw it. Somewhat paradoxically, to give a **widget** a state, a method called ``def state(self, widget)`` has to be defined on the **module**. The reason for this is that the module typically contains all of the statefulness, so assumedly, it's easier to determine the state of a widget from the module, rather than from the widget itself. The ``state()`` method simply returns a list of strings, which make up the state this particular widget has. The themeing code then iterates these states and selects the matching theme information from the theme file. This, it does by performing a "best match" search through the theme, like this: - Is there a theme definition for the **module** that in turn contains a JSON object for the **state**? If so, use that (for example: ``"cpu": { "critical": { "fg": "#ff0000" } }``) - If not, is there a theme definition inside the ``defaults`` or ``cycle`` theme entries? For more details on that, please refer to `How to write a theme `_ If multiple states match on the "same level", the last state in the state list is used. For example, if a module returns ``[ "critical", "warning" ]`` as state, typically, the widget will be drawn as ``warning``. One important helper method is ``def threshold_state(value, warning, critical)``, which each module possesses. Using that, it is very easy to define warning and critical states when the widget represents a simple numeric value. Sounds confusing? An example will clarify: Let's say your widget returns a percentage (disk usage, or CPU usage). The widget should be marked as "warning" when the percentage is above 50, and as "critical", if it is above 90. This, you would do like this: .. code-block:: python def state(self, widget): return self.threshold_state(self.__value, 50, 90) Advanced topics --------------- Event handlers ~~~~~~~~~~~~~~ The ``core.input`` module can be used to execute callbacks during mouse events: .. code:: python import core.module import core.widget import core.input class Module(core.module.Module): @core.decorators.every(minutes=60, seconds=20) def __init__(self, config): super().__init__(config=config, widgets=) core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=) The command can be either a CLI tool that will be directly executed (e.g. ``cmd='shutdown -h now'``) or a method that will be executed. The method’s signature needs to be: ``def (self, event)``, where “event” is the event data provided by i3wm. The full list of possible bindings: - LEFT_MOUSE - RIGHT_MOUSE - MIDDLE_MOUSE - WHEEL_UP - WHEEL_UP Setting a default update interval ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To change the default update interval, you can use a simple decorator: .. code:: python import core.module import core.widget import core.decorators class Module(core.module.Module): @core.decorators.every(minutes=60, seconds=20) def __init__(self, config): super().__init__(config=config, widgets=) **NOTE**: This makes the update interval of the module independent of what the user configures via ``-i ``! It is still possible to override the module’s interval using ``-p .interval=``, however. Redraw outside the update interval ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sometimes, it is desirable to redraw a widget dynamically, even outside its update interva. This can be useful if the value to be displayed is calculated in a separate thread. In such a scenario, the ``update()`` method would simply trigger of a thread and the actual value would be available later (but presumably before the next update call). If that is the case, it is possible to fire off an event in the thread to cause the affected widget to be redrawn, like this: .. code:: python import core.event # later core.event.trigger("update", [], redraw_only=True) A concrete example of this can be found in the module ``redshift``, and a couple of others. Scrolling content ~~~~~~~~~~~~~~~~~ If a widgets produces a large amount of content, it might be desirable to limit the amount of space the widget can occupy and scroll the content, if necessary. This behaviour can be achieved using the ``scrollable`` decorator like this: .. code:: python import core.module import core.widget import core.decorators class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.description)) @core.decorators.scrollable def description(self, widget): pass # TODO: implement There are a couple of parameters that can be set on the affected module, either in the 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 bumblebee-status-2.2.0/docs/development/testing.rst000066400000000000000000000024351450256260500224500ustar00rootroot00000000000000Testing 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``. bumblebee-status-2.2.0/docs/development/theme.rst000066400000000000000000000140411450256260500220710ustar00rootroot00000000000000How to write a theme ======================== Introduction ------------ ``bumblebee-status`` themes are simply JSON files that describe various attributes (foreground color, background color, etc.) of the blocks that make up a status bar. It is possible to specify each attribute at various levels: - For a specific state of a specific module - For a specific module - A cycle of attributes (those are cycled through widget-by-widget) - Default values Looking up a value follows the “more specific rather than more generic” approach. In other words, if a foreground color exists for the “warning” state of module “a”, any less specific foreground color value that **would** match will be ignored. Themes are loaded from the following locations: - ``$(BUMBLEBEE_STATUS_BASE_DIR)/themes/`` - ``~/.config/bumblebee-status/themes/`` Basic structure --------------- A very simple theme file looks like this: .. code:: json { "icons": [ "awesome-fonts" ], "defaults": { "fg": "#000000", "bg": "#ffffff", "warning": { "fg": "#ff0000", "bg": "#ffffff" } } } Icons ----- Using the ``icons`` directive, it’s possible to reuse icon definitions for multiple themes. The value of the field is the **basename** of a 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 ----------------------------------- ``bumblebee-status`` supports `github:dylanaraps/pywal `__ definitions. To make use of them, simply generate a colorset using pyWAL and reference it in the theme like this: .. code:: json { "icons": [ ], "colors": [ "wal" ], "defaults": { "critical": { "fg": "cursor", "bg": "color5" }, "warning": { "fg": "cursor", "bg": "color6" }, } } Additionally, you can use the ``colors`` directive to set up named colors for your scheme: .. code:: json { "icons": [ ], "colors": [ { "red": "#ff0000", "green": "#00ff00", "black": "#000000" } ], "defaults": { "critical": { "fg": "red", "bg": "black" } } Pango support ------------- All values that accept a full text (i.e. the base level, ``prefix`` and ``suffix``) accept a special attribute ``pango`` **instead** of all other attributes. In other words, if you specify ``pango``, any other attribute on that level (foreground color, etc.) will be ignored! Inside ``pango``, you can just specify arbitrary Pango attributes, and those will be applied to a ```` that’s automatically enclosing the actual text. Full list of attributes ----------------------- This list specifies the names of all attributes, their JSON type and a short description. defaults, object Container to specify fallback values, in case nothing more specific matches. Can itself contain any of the other attributes (to e.g. specify a default background color). cycle, array of objects Similar to defaults, but specifies a list of containers that is iterated for each widget being drawn. Effectively, this allows alternating attribute values for widgets (for a powerline effect, for example) icons, array of strings Allows loading of external JSON files and merging them into the current one (adding fields that do not exist in the current JSON, but not overwriting existing values). In practice, this is used to load common icon sets (hence the name). warning, object Specifies attributes such as foreground or background colors for a widget that is in state "warning" critical, object Specifies attributes such as foreground or background colors for a widget that is in state "critical" fg, string Specifies foreground (text) color bg, string Specifies background (block) color separator, string Specifies a string that will be used as separator between two widgets padding, string Specifies a string that will be used as padding at the beginning and end of each widget pango, object Specifies `pango `_ markup attributes. Once this attribute is encountered, all other text formatting, such as `fg` or `bg` are ignored for this widget! prefix, string Specifies a string that will be used as prefix for matching widgets suffix, string Specifies a string that will be used as suffix for matching widgets default-separators, boolean If set to true, the default i3bar separators are drawn, otherwise not separator-block-width, integer Specifies the width of the i3bar default separators, if they are drawn , object Container to specify values matching a specific module , object Container to specify values matching a specific state of a widget Note that it is also possible to nest containers, for example, it is possible to embed a "state" object inside a specific "module" object to have formatting specific to one module, depending on the state of a widget. In concrete terms, this is used, for example, by multiple mediaplayer modules (cmus, deadbeef, etc.) to have specific formatting for play/pause, etc, for that single widget only, like this: .. code-block:: json { "cmus": { "playing": { "prefix": "play" } } } Examples -------- see `github:tobi-wan-kenobi/bumblebee-status/themes `__ bumblebee-status-2.2.0/docs/docstring.tmpl000066400000000000000000000001641450256260500206060ustar00rootroot00000000000000 """ :param : :raises : :return: :rtype: """ bumblebee-status-2.2.0/docs/favicon.ico000066400000000000000000000360561450256260500200460ustar00rootroot00000000000000 h6  (00 h&(  ooo'KKK3///))(303x!UUU'"$ ,1$$%˲8QJZ@ ! 󙙙ssִQx9J7m|3bqQ]]]w@@@!#Y4MTཽ }GIOx<Yc[""BVXdWb깸A`ʶ{г??? PQQJJJCCCͲ+yV»988;;;䳳i,,-q;L徾m}|}ZZ[DS߳"t)))""#W9H( @ .iiiRmmm)KKKs888889kkf9AA@333 Ƹ)()QQQQ2SL JJJ'?G4fu999Rݵ[)13.WbCT[Z'*111kvZ\\\\\.T` ! -1/b][K7pI]>WWWFȣʷ m'q۶@.Wb%&2cs\N%$%Ԫ岲ҳ֙S"5:[Z(*IIIa޲lmmT]HyᲳ4 #'DL:zN2fwC]XţRχ~]]]D3bos< 粲\]\T$58Fũqu\]]%:@HHHԳm 𲲲쾾É9ȳȷ;}󳲲Ɂoٴþ,oղʒAAA#$%$ $&&x`CCC1 ! lml 989Ƒp323 !l...]~~./.IJLķAA@eddeMMMGYYX%%%fZt͚ȟLQL5lȌanô8MMM+<<<M볲҉$""NON===>򳳳x$$&ONOBCC  111R Vã} HLL<'''kkk(0` mmmfftptOhss+[[[pUUU'gggYRRR;:;LLOP ___PDDF:::101 !!BBDp6III%$%455 EDD220XbXmmmr***MMM.F~זՎg&)+**!144kzI/Ydcccam⵴_MMM#'( ,1&AI6qJX[[<|IHH(˭dS@MY[]]\]F')766!ɵǥ]Z\]\]\\]R"389͐򹸸踺鹸󶶶"+o\\[XTM@W][)IQ^^`y&河˞]CEaАҸ0 [YM?2ds'@H! #$M\\:wVYYJ}U󲳳QR4j{"7<'(=]\L===Ȳ̗ Mߵ̗HKJ*S][\Z)+''&UUU6볲c:Դ//.')Y]]2\h:;:uuu 񳳲UM鲳###H]]MpttDճj}QQQ%("49-S_>J"585kz\]ԝț ӰAvER[\Z(MW$CBBCCCrW貳͂s,,,l 898LMMIII&&'$%%䳳Ўf첲688:XXX,,-+oǓ AAAJvwv232~~~Ȃ͵%fLf QQP*++.5𲲳X===ihiuuu˴û՚KKKUZ[Z999tǓ.//򠠠ϳNƋ&&&323 dd`Bೲ< ĥҬ,..%%% ' ':貳[6鲳vFFDejkk111iUYԲ3C<<&~mmmZ󺻻ӥӱܸ̝ DUDtut"##)))hAز40..cSRR[[[1ﲲITTT )))www554]]]4)ɢѨ"+++cbbz{{A@@BBBI!uƼָηUuub +++t>>>=UUU$UUU555Z_Z;===---424MMMcFMM$s\\ bumblebee-status-2.2.0/docs/features.rst000066400000000000000000000141601450256260500202650ustar00rootroot00000000000000Advanced 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 ------ Many modules provide default handling for various events, for example: - Mouse-Wheel on any module moves to the next/previous i3 workspace - Left-click on the “disk” module opens the specified path in a file browser - Left-click on either “memory” or “cpu” opens a system monitor - Left-click on a “pulseaudio” (or pasource/pasink) module toggles the mute state - Mouse-Wheel up/down on a “pulseaudio” module raises/lowers the volume You can provide your own handlers to any module by using the following “special” configuration parameters: - left-click - right-click - middle-click - wheel-up - wheel-down For example, to execute “pavucontrol” whenever you left-click on the nic module, you could write: ``$ bumblebee-status -p nic.left-click="pavucontrol"`` In the string, you can use the following format identifiers: - name - instance - button For example: ``$ bumblebee-status -p disk.left-click="nautilus {instance}"`` Update intervals ---------------- The general “update interval” is set using the ``-i|--interval`` parameter of ``bumblebee-status``, and defaults to 1 second. Some modules override this interval to update less frequently (e.g. the kernel version is updated only very rarely, as it usually doesn’t change during runtime). Also, modules like ``weather`` or ``stock`` update less frequently, to avoid hitting API limits. For each module, it is possible to specify a parameter ``interval`` to override that behaviour. For example, to update the battery status once per minute, you’d use something like this: ``$ bumblebee-status -m battery -p battery.interval=1m`` The format supports: - numbers (assumed to be seconds - ``battery.interval=20`` means every 20s) - ``h``, ``m``, ``s`` and combinations thereof - ``battery.interval=2m30s`` means every 2 minutes, 30 seconds) Errors ------ If errors occur, you should see them in the i3bar itself. If that does not work, or you need more information for troubleshooting, you can activate a debug log using the ``-d`` or ``--debug`` switch: :: $ ./bumblebee-status -d -m This will log to stderr, so unless you are running ``bumblebee-status`` interactively in the CLI, you’ll need to specify a logfile using ``-f`` or ``--logfile``. Note that putting ``bumblebee-status`` into debug mode will show an indicator in the bar to make sure you don’t forget to clean up the log file occasionally. Automatically hiding modules ---------------------------- If you want to have a minimal bar that stays out of the way, you can use the ``-a`` or ``--autohide`` switch to specify a list of module names. All those modules will only be displayed when (and as long as) their state is either warning or critical (high CPU usage, low disk space, etc.). As long as the module is in a “normal” state and does not require 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 -------------------------------- There are a few parameters you can tweak directly from the commandline via ``-p`` or ``--parameters``: - ``.theme.minwidth`` sets the minimum width of a module/widget (can be a comma-separated list for multi-widget modules). The parameter can be either an integer (in which case it is taken as “number of characters”, or a string, in which case the minwidth is the width of the string (e.g. ``-p cpu.minwidth="100.00%"``) - ``.theme.align`` sets the alignment (again, can be comma-separated for multi-widget modules) - defaults to ``left``, valid values are ``left``, ``right`` and ``center`` An example: :: $ bumblebee-status -m sensors2 -p sensors2.theme.minwidth=10,10,10,10 sensors2.theme.align=center,center,left,right 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 - ~/.config/bumblebee-status.conf - ~/.config/bumblebee-status/config These parameters act as **fallback**, so values specified on the commandline take precedence. Configuration files have the following format: :: [core] modules = autohide = theme = [module-parameters] = For example: :: [module-parameters] github.token=abcdefabcdef12345 bumblebee-status-2.2.0/docs/index.rst000066400000000000000000000035361450256260500175630ustar00rootroot00000000000000.. bumblebee-status documentation master file, created by sphinx-quickstart on Mon May 4 08:03:26 2020. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. 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) - custom modules: :doc:`development/module` - custom themes: :doc:`development/theme` I hope you like it and I appreciate any kind of feedback: bug reports, 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` | +------------------------------------+------------------------------+ see :doc:`FAQ` for details on this Example usage: .. code-block:: bash bar { status_command /bumblebee-status \ -m cpu memory battery time pasink pasource \ -p time.format="%H:%M" \ -t solarized } .. toctree:: :maxdepth: 2 :caption: Contents introduction features FAQ modules themes development/index api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` bumblebee-status-2.2.0/docs/introduction.rst000066400000000000000000000056701450256260500211760ustar00rootroot00000000000000Introduction ================ Installation ---------------- .. code-block:: bash # from git (development snapshot) $ git clone git://github.com/tobi-wan-kenobi/bumblebee-status # from AUR: git clone https://aur.archlinux.org/bumblebee-status.git cd bumblebee-status makepkg -sicr # from PyPI (thanks @tony): # 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 ------------ :doc:`modules` lists the dependencies (Python modules and external executables) for each module. If you are not using a module, you don’t need the dependencies. Usage ------------ In your i3wm configuration, modify the *status_command* for your i3bar like this: .. code-block:: bash bar { status_command \ -m \ -p \ -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: .. code-block:: bash $ cd bumblebee-status $ ./bumblebee-status -l themes $ ./bumblebee-status -l modules To change the update interval, use: .. code-block:: bash $ ./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. 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. For more details on that, please refer to :doc:`features`. All modules can be given “aliases” using ``:``, by which they can be parametrized, for example: .. code-block:: bash $ ./bumblebee-status -m disk:root disk:home -p root.path=/ home.path=/home As a simple example, this is what my i3 configuration looks like: .. code-block:: bash bar { font pango:Inconsolata 10 position top tray_output none status_command ~/.i3/bumblebee-status/bumblebee-status -m nic disk:root \ cpu memory battery date time pasink pasource dnf \ -p root.path=/ time.format="%H:%M CW %V" date.format="%a, %b %d %Y" \ -t solarized-powerline } Restart i3wm and - that’s it! bumblebee-status-2.2.0/docs/logo.png000066400000000000000000001433171450256260500173720ustar00rootroot00000000000000PNG  IHDR"gAMA a cHRMz&u0`:pQ<bKGDC pHYs``kBtIME :1@'IDATx]u`~f! $hw+ݽ;!8I'w|^hВpٙyy_$(R-k˭[-$s%s%s%JI'])o+7zt%? 6PoVoVo~wJ 3  X9Sv<{ɳ'@ 7Tߠ/ߤf/{>C<86b'O:33q@]]BBB?qlI3 KX{I?I?I?M5(((_2fG 0=a+++A"Elޣ]vjV䚓kNyp]w5}:ޖޖޖJ5K 2@X#ֈ52D JŌ3JKKS*++KIU&U)ϲϲϢ'|~mnj>4,Μsiz>o-*U.\3Ou[KRE7_ׁq`PMc4'$$ԊvsG55פ^D)O_NNNEpQPĨQc:uЕCW]_"s7n%!5~"΂D}'Onlܲq:mOr8q2$dg"g21][N ;-Lyq*VZje,8H=^x5#lmAB1C(nN O O ]5|lMm_6~_7~gMO2= bD+~ ۢnQ+2Vdؽ$n|: y-Z5[?ᖁH 0!7zYpe =;ݸwލ+m߷}}3;=4{hO0 D""""Nl;&s`ρ=~7o;o/~?` K (IRG̅gן]vs9s\psvsvsZd K)Pvvvx&mWڮ]{'O^7qs͙7&nLܘ8),),),YZ*iU2f[e[e[%yYYYys3rw2ӕ(Gqb\ 4f,XKb Ä Vs\٥)Ǎ7N8ppp3\[qh) ̲e3 ppp222۶mmf7nd'e'e'DO /mHuLuLu5D :quj cqŀfffy PC 5 *\cp-~7ϐ>S}I[(t7n>E1.ߣ9TWr&H EWגnnn?, !!!#@@]G]G]jm;?ş>B"(xO 0FcH2%L@SS0e\˸`^ۼymkR`R`Re,m m mpO ;\ӕY."enIc$JEx!Dׂ] WN:딷Z`vY6;lvh9"qhġE5H?DE`fnfnfn ~-kZjOƓdsV1yL'8 ].rWnnn@dTdTd㏎ː!/C$$O/*Qɣ׏^?zO<>:=dp ^w{CDCDCD"\FY5tM: HmDWtw;+++7T+$^Nzssss oooxxx8?6is'(vi` KX0 L}/ԲlG<383D7M%")J &Mmێl۶m_fЬY'S]Fa+z >R8{ 7X|cu*U<}dɞݖ-I[x"+h^XA3<$ sn-ZZ{n fHG{a4FhD8~¶-lM7٘iAZ3 |W+ s~pf3g$[4ПD@(bC?ِC>dH‹/ڦO_ 7`<"8,$BEgy2_/6w߽ƬY.=uY_ `.*d)YJb b HFIK(@++I{ҞiB& `=z@7J6+Cy1/淆:;+So;\( hJ /W\r%9c9JߦM}N 8-p[UӸ,,,01(PB鏪"8%8%8%@燝v~x1DaG~EOğ b!kLE"& l賣ώ>-:" nݔGGt ߴihԣQNU}Z4@Q(T^~/+ e0>=T'Iudf]w9`~#oߒ5tl^]w9E ArB,Gi:Mb6// ?IӤi:Θ1;cvTTT " " "V Er@x^p ڕv]6d4hds( wS۱"P4Qkb&SL0u-t 6 mw* QB$$d:o  i^yg/xw>WI***/JJJz^;{6R>BRlFl& 0U*L6g5D#nݵɣGWY(}Q"&J֨5j$=IOh1s0GҴMӠ_ƟVibKcʏ)oŢEFf{*sC(Qأ0 yRUYlf1V>XYDzO&ֲֲ2`y@ӠAO^Ky)@;N}ġrNݩ;@' P;v|x`EVlߵGq{qtP]ym䵑&fom̚fy[[dO'=#-G`w;jR7$$r",SHNU Vq1Q`I4&`㣡7a6 D3gq銚 o&^^'yk\~,.hB ^dhQ0Kmy Κ5; G$w;wF*----[ L5uYח_~>X`s2>?~ ,NjI"7~_}k/^#494fff=[[[IMR6Ʃgggd'IvE @ 1ikp***yjyɼda?ggg5mI^=G yAG4|DsZkar&NHQ?Ʈ]3g͜5s`߾};@z7͍fpɚ5' RݥV4GJ&h6dWȮ]k 8p6/= B-#f"ɉGxnjnjn/ =d{vsSSSqFQ@MI"I@%a|ۑ`=Xޯ_{/D/D/D5445j[\"DZ#MWnyK-5g:tï ?3̩#ml]|.\nT9Jt_t_txX>b@؁q ey2u,' Nw:8ӓO{\riʥ), XA~Ы b"ENL8NX OOOk"Mh>aqaqaq#jƱqlk2V\\Eo XA5Uơ>,:oEE|Z|Z|^z#>giCW `>ק_vNaȆxWxEKIDo߸q[ݩwޝZkzzz bGk4٬go6!h}Zڪ۪۪E"@zzI}s;tөs^*yU|TRv+9UVq/ммP@H SFv ?~n()s21bf`$/ J:qs6l6@d"2KKKSVO s*P1bfL4[o +WίNW8_|w.].]֞֞֞bbb>82#@m-^ԋzG8Qv$=-",Bi_ ~Ll)Re$>M~,? ±AF3rL3m̴٦ov'҈4" qGklخ%qI0H% DEEӁ-2EGM{rn\py Vh݊vo͠4 `++Wį\VrYU7>fcƘBFoZrrr%@n[8 '\>"D,  [033rs9kn*5J3F'N|R!ːe}> I}>s?d9?:9a˩LWѕ6r]doY#ֈ5vP;+((Mܞ=s(?3rss."`L!*2riE?);T _A! i݅Ivw)e OWtJ)t:k%#lm6c Ƿ55|kwY9{ΞM&~G^s`ҙt&33tM7 ߴ$Iզ1b+^^^XS|A5Di=Za0K%+`u us}}}kaaqѺN#te;o֡[n\wfݙugQK@]0GxtѕAYzzzb˙3kgxƷsi֍uc aE5 qI$~qjPC*ojSԬ[v^2=q$Ijd2e.N9cӧ{j`깩禞hSڔ6 d|T%UIUW+&gMF8`{=7Ni,YUW??+b{ouwMM/=6 eXe`ʌJTmFLDL5-jZ{6fPM>4}xn+Y^O9 s@f+ + + ++>rOʌJ!cb\eeeWFYFYFY"*sN9P d.JyPUQ: "&`"ڞUNn]| 6O훷o޾ (B!JZGjm777?_< ## Hw߃ E4n`GmO[W4tiOCc$V{j墳=_7uc*Y%0^%^2*2@(v;qqq*X*liesCf}}}|'2vV'ЏӇ(v~d;a}te\˷!!!aZŪC:$iW^ 0ZP I'O+2WU׀OBZtki~J<;yhThUUV_A1 ;_v}~zwC$Q\xC[wp<y. $IO=^{gggbk5 C0C`ňC6fR}?5Ӛi ~x@o+}[ȉaaa;w>|(D4T&Im%J 6x>y]AE]9|||h*zU}///Iv`}\uYYYm޶bӱt,Mc]% }={0/o@ހޏ `}=/}w_/}+VKh֯Yf ĵk׾ڽC;?A9n׻]m5jU'74{lţ ڦq LqVﭶ{B$b///(VYf囋v8::۶!On?N" /xT\2wQxsV%"$^ͽ{c-[Hl01?Ediii@~f~f~fu j+.,p pu:_ƻ$Ij2W-R,Y1z~ s\:PSS5,װ\Oj Z 9;9;9S9$yyڢowvPӱcMǖ!!!;XWU$ J+Ҋ" &&SOշ@.uzɍrIuu_-Z z+N|iI,(cZޅw]U|  RET0 L W ^>3}fLc[PB]o D!E22$ XzޢXw m<#9 (Y$! Ihhh(N܉_pL2 0aV;Վ#B8W 86$GTjՎW۵?ƹqn(DwtG/@zFBaXLl1wwwz///th#Ϳ6z0)L?z 5(v_/]<QqӸi[r: 2^…&US5UC4}iR4jN(Z>1GB[@VxVxVIܾ}s3wJj%jմwn TTTfO.qo:::8CCCa{5fR3ܼ1cZ!vCl.y^+Js9%NKɕWj\$o\qk9snP"+ƇBd[QHT¼!֊lb61z{{u5k-!mH$Wem"& wW]@3g*U8n Wr@P^(7zPf(3`A~O~O~09lr0@{^קb@`VϬY=@($Oзз׸U +u:n@i4\RRJI$Je!##c_k #|nC*SI%s{7n8oo-[ 3 E0BxUTGQMF5wٻٻ!?1PS5US oFތfW3GBG͏fff'OD?vvv@f/CҀ2}Ny#cd ++hkk#444$h~4@<'D! H&&&Lw'O8תU_88^D73UUh0O\~Zi 󕭒s[('ooQD7ݿUwUEW^jwݹ'ODQ=ap\0 ;wPHp\pùprަm4~HyR;t1+=7ӛ 7 yP%WuA\QQQ`ᓆOr=ݑhhhs=*J,5ִiUӪ`"""_+Wv_=z do,Z _B1ދ((Ũ;7ììn[K` +++]Fei|$$$$M>g3O[ͭV\c1@ @/}wK꒺+a%FqQޏ8A NDDDB`Ş{Vis5j'ON, QK= un 0ol?o01Nc_\\|.mEۊBt4k(6n_+fA˃s9ҋTm |n>7a9|$$$$JJJUrӨ4* PTT Kߝ?ZZZw#%%wwwXi4Nc>}>zb~4FPs_EUUieZ0s2s2sBBBBfuhqqVjY1ZMՀb|qqqZ 666OORߟh`t-#!,, RSjJIƶƶƶ[O!)$Ȗg˳B~wgT3f< G+%chEkEkEk,Hڞ=iT={euAԂ qatVӭL~3ÿev|.˿?],KĒwwY7S%䁒%%%ˡ[HI;'"n`$h} PUUo7ޞ7Ld&2e}j?l-[ H_baG⡁@>&~&~&~_Cm ,:'QmDޮz*l`6|3`n1F1F1F@xHCm+V`E΂1c @Nx#!gHݝݝݝe2wيX%X~}&ߩDe oooY)+e©DYɬdVL2$;t @>B *xNNN'>4u:M <|b đ8` RЫxRAtr{BUWt@'@|KU}m/k/k/Cty*7UfdE "bX\-WM5l 5Q!O'_s^tDtDtE*!@  wDJ"mF^{mҏH;G׶^ >(|P IK9Dy5j>U(&aA y'+ﯼZlJ$=IOHFr)܈H@h Ϙ1?/,_Xq}$$i.ʹBu !HX"Ve-e-e-O=g|JJ.gR 0ynWʞ=+{ok!I;w^ |5@a|AWWW@(ʅr@H!W}C -D)QJ@zuااا4=MOu,,FkPZiiϚ5?;/|&nb7IBՀK$&b]ѵ)Sr#0>o"." ##D~"Ta|N@ 6ЏXm\q]&ܛ7 FL.31gb|BBJ%iIZZ7 85SÁ|tZ#j5ݦvm:#>߆.-%ĘC} nCUUUe!Aw)[ ߪ+ǯԗ՗՗Y0:gtlc1ۢt 䯁'O|QEdddմ5rSY;6 يlQB63CA f.]x;ȋ#/]Oshk֢-j W2UUZbWhWhWxhX[҃ =[ @erryV81@d{)\}ObBLޗBT^jx&L<5E}M/\b]źʻooo }`;ŎHGґtwMfـKO sZObϭ鍦7|YdЫ?Hi4R^y틔8'TUk%2((&yJRԹ}ƥqi4(LUTER8P(LRMRMR  KhQ>E{-[$ Xбc׊*~(q1GVY "h5Z pa]XgLPPncQA` ĖvNKKX`v`G)\~۱y_}肂>UJn@"li4[ їՃۇmeyڵjתG.&D:n׸]v `G65xi}q%$:uv'''^8I<y6MjCcc>@: D>,2z  y`恙-yK;к`ffqUҐA" u3q3q3ἬX~b]r-!Ӡ,c(nz^ɻwW6gmZ` Qm0 (N)N)N,Y Kb\ SoB/K%H_|yI͡o(Qrm˵-&臜%P*KX\\.]6l-d i街8t*F ڠ p OJTqոj\!bGý={3L;PAUʪ@+@kmm TuRRexžke*(R>!dR~U~L~L~bOŞ=E\i4~88 Nwat<Oǃj5j*`xuG_H@>`;v\{+___P]*L\_Sm ҆ K2cˌ-˙3-|^EY_ 4D.t;N'>s;i E}s y0JL%EJ)1cA3hJ^ 7ΒΒΐs>/Q%JF9GOLDC1CX@u5ks*ouH.(y2 -- ='- $$->^xo =# +Sfff>}XMzUUWӍ,,,=+\A"I'0Sz}7kx L ;^[z:fݛ ̌;3000;PB?_<{xvR)4>9s^a}DwڻdȺ6-c("w\Ko!5+5+5YdEqIC>O7l|3`gg.X.w.Cì^z[.\l9c~!fO=iy+W4ц= {ƅO4 {WT[ck ={B?|HO"ZH i!J.tZE"Z /4^&Lz$x[ecYI bBX( }}}z kbW_M3|n^2L*x/^bf˚4f}ȉ g~uQOQXeO=UO>9dC 3u6L+43tNT[yG~iK_c;v/LLL !y0C̡w1Qi+XQ1*mh xhzꍪlq1@@]F~8ѽzZ IT' ^ԋz%9P|u$/:_#XOarrrb_OE"{qF"u)R% / (J͖<Ҷ-xӃM=gE=Uu9 Ģ՟°PҭDr.s;y{zzz[N9;3_ JxFE>D@.A]ztEw9mN4|xcIC/a4 H"wBt>/|2pP|P|P UWGf`6=qpf6l XGZGZG|$G}?h Tkhh혋;AcQP=Wo\5qMñuOx:5HD"ZeJma'h$H8 '`\nr=5zjt_"\/ "A$ H3 `™p&G5j {n칱[GoDGG?=aU}r&e1F6m JcH PGˆ1#YtVįW+v|;@-{@P~򚳚Mgmݷu֝n92rdHM& R<^}UiUZZG-B8q㼈yJ;f ġ _RI'kP]T]T]LBBBX 8p0`+lWμ|-Z5cZ%LFe gqg+g3hhh+ Wme2{m~vFt-]KR #Ȱwa /I'A&SUdY%wvc%KZmwxy߉yՇFpΐ:q%"SP :tRrKrKr`[˨o!@^?PMMMEüoqtÌ 36Xtӱ*uV֝J#HxR㥀iSӦMF?Dh@uA@^t^t^4hfifif?',d,'˟.+"L&dWW_]]~CKԝny:UUTeъ2cA]f7/ -Rzzzhy<^TTT ;^L|1yn&Q+`2&c28LLLΫ:=֏[?OW|5޴~}3@= ư1...b% ,\r6:mt91cǼuϯ{ޏ+r+z!u*fSlxk5E>k}?8,zyW?=noVNl賡&C[[һr O*eK˖-T^ez>5;lh]ɣtQCrWnO)9% ~F? 4(h^pgX!{fFͨ*{ ہo:*##=SW9{~?G^2 y5jU;d2(M7z5cj^tzE?==PbOmmnZݴrqN"Nf 3xzN՝?QmWmWmׇhT%D".emڔ r@IF6@Cݷo޽{{$NdQbp/+Rp{=@GGǾ*O!elؖCͧPa`w^VaV8zKG.Ǯ̯_!8_| 6/b!>NzAoʿ= }AKw~qfi moC}.w4<";wK/= /c rfg8V X]t-N6m7:ĺTAB_W WZ,WH>mu:\h4C34+ B-r\%W!uxꑎ8EA-lM%dQR(tQy-L'N4&-++!淛nl vh& p$ !!x2 '!Zk.9G9G9GmtCGtD-q E{ܰ)a| ^^)gʙY_g}5`s}FgKWwѕt]s\|C.kš&6'nP0Vc0TUKUKUJȻw#:MZk-뫮 H|$ x$M%M%M5Ӛi O'zSo pfnv;hhhpv8;fk@av-h >>>ZXj!3xSrtw)SP9<<o6~TQF1`ii Hّk3fu?_|!a][[\eeUV^hmڢuou$σIlU*000|8Χ˾ԡMMM|(ʇB-d_<8DPq:u7~.9.9.9}k]Bk}n~&w+: FFF"!SZ]閩{1+H###|k89js M;M;M;&555JTc{ډډډ{=˞:lsƜ1[[Vb%VjB5Pu:U &<PuQw*ڰ0x~}ted#G-iuXAA!sG#IY%++[r@r@rTOO/I#!!Zw{rD#\PA09K@&ڒ-i={s[ܭv&nMZY+D777bqBl2QɻQ P q0a4ؿ'MMMUMhe,ϵ@1G h^%ߍZH 6Bъ {wARARARժ?+))x)'*'*')))1'վj_/p B[-TNT`'vb'x*2*Z@/@6ڡ]HX"bIW\ O;Jn5Z5@:Τ3ty?(6 2@09B#YC֐5¤0)hhh: 5''' oI$i40?0?0?иUjH%g4׭XyXXX0AQAQAQZnv캵h ǪzَlG# >!>!>!}kEGM{M{M?fcd6Mf+=%z-]X6@Ɠd<艞 I! =|, ^03e6?-ނb6fc6mu?Əc5=fzu^(KILѿ5}M_{ut~8cueC.hbbb}o??f@vooW1I1I1IN-[d z@pqQa )w;`?J ^^^sUPUMZ5+4Е^*Nf.SЕ^-x9&*"*;pO'cjPjut].QV2tCEmnB&{ɽ444oIF?}HX* ?a//:u^Ng&׃^:姗^wQ̣G1mBBP"33ΓEG$DB$ڢ-ږaл>c<' 35k5_]'}*iO%bg,,,K?7w1b^t6m2%mjԶ6[f7nhiiAoD†0?K߭e:J Dn"7DrArArLLL$H$ dGJR ) 0F¨~W(!!c%##ޓ=Z7/EDDrʉʉeEEEsTL0oeN%ߒo6t]-gtFH$L(t֕Ral Xpf~eyرk5lh|5_x222k{ Z\QRR2ZZZ0M___lM&[h-Z B ]pAkj2 # s t t |swwwF^R#"eQX/d^2/-\0&Y,N׿ O///<>ur\E4Aa=>}}@pwqb\P2L'.3q&c06S)ϔDDD@UU555+++W%===___WQ(s j,A%,a:oP$m/EN|&¾X HI yDo[ ΍ [K-Tt^FMN11NrAۂJӍhZxN@js%%%gP''q\i#D6h>;_(,h'/uБoᄑnsIbNJwKK، qQma_}۪^Go?7&l&h0c1Rs6l嚔kRIMs6ͧ 6c~1ЋP4qI ,PA`h&3Rc9bzKKK0ˍɍɍ>_0 q0E A?O!H+Ҋ`R{=qqqNCrQsFgcoa]zkV qQ =bHc1Y6-mZڴLLZLZLώ}i\e@Mސ7 d؅]( 0=q́FH,jmڪ5ĞVVVog9;;X,b165-NzGW/mY7^8[xUUmQNl [b+/:x[ɷo[-<"Q$ 20H`^wq5&---[Vi<xxSSSx>kl]'8z JۛꯢCE**H z'''̬53jfkrrrI#i$ԘyY A`0^^ROAā@q `nVu l^2$-I˃;tzQqp|a l&ps9ܜ ]~M7ݍ$ĒXPk% =!^t;T!-wz3<3 !bctn6x+-1o~^4̀"}!FL*x` kXאޫGߏPп:3J+ʴ21"_oSOTK%g &L,XXXTԫgeͦ{QUl[0`L[Qn7;nvl:sP((]".⻝؀ ؀wnKRT`Z1VH-RԀ> ..{{{GGGhhh$ ىƃ`<s9dGH"Fvd7 .']H>rVnejG#R~ܦiz9pE(p. {w%ׅu8΃\?@?ӟ~' Щt* G}.>$Ou;c40,΂h=Zxxx;`b&}+urxB+ě!}OQM}p2Jh%ZqݹswxFlP|$E"p7q,::A QB@ 7fԌDK%р||| o)o)o ())qr/K5RLLL9ELL,G$-/-/-9S*vfEEEsWWWe1Ϙg̳ iB133+IҀ4P~S +"= NHhDJp\'+udW.d?R8җ%}ilF{D<(F9c8ԈkŵZdrfgf˶a۰m_ZVj;7ٛM!v;A條#R[)S:tfzjkkhh_D d` F" V[=\\\WlTy $&eF(u NoƛLn1vhQuf"EOcCCCϤTMWN:{;fVi]u%$χW_#AɃrGo?n߹E^˼y-49]T\%D Ur Q  4;N @M8l@3ȑ@O4/ *.qPFFn`ƬYu. i\)۔m~-**`аD٧ڕX%oHސaIkylqq0dPZVU@DQ(Qa 8㯩l:t|ᢩ~!)WS\͞gs#0#;$Սgё?G ˫/jVF]Wr82 SdV{R~kh=`GqE_@s)= i?;#Xt?\'7Dw@8;i P.8 < LhG HCٓ߷>%r"@w]t#8#!H5R dododo F.f5RI$f&&&Nt3fCEE[t}/>r 64qsm7nu3~q UPP< q֥8Ud2 EEEeˢ5vnvnvn)v+v+v,k*C9졢ԫp I*1܂c n'[.8q><8A)*1 ^Ib(ܛSz%|u8ZC* y_qu~zG|O@=4ù)_>PWN)ʜlNK>V`EDpCchu&G>8: 0 H@C&pWh;ӚH )$! I2w>~El :gp+ .@`2 FQay ՐՐd e e (((2yW7o4U"O{'QK\8b‹z}~P:v;2wdز)dɐɛ%#q$ Jp _ 6jո+ꙿ1cG< kQ5Q%?ߍLtCCy!zQx P1撺}M JvDĖhd"@aXK+= #*`+:py@AH!q<h"h0LVz^T $q4-+| %6`h@۞&ؾ֊i 5xGM "_NҞRO @TOTOT j`v0;D2(WUn.7 C١P@UUh8q`9r|~hhh6P2F2F2U*qUKKKgFKɏӤӤJQҨq6BB:^hd8dl^;r Ajc4|y(T=ןmz٦rdzezezAII69*PzzzCLJs_} ,[YZѹhEh%0!k544Vf===¦),KGxtTc"e{Ǜ\STD&PD4Rb(B,~5 :q@ p@X)@*>FQK2`&^D ?M2Sz{P .&3cM9EN0G>'ÿyd/zwݹwBGe6SP&V[EsƋb]>A%\!~_T\]DRRrGKKKmI%+b!/ڇY:w# ݎl*l#&oLޘܽܽܳTN*'+IuIuIj~5::3]cs /hKrrr~6٬gԬRP4@4(7[ȣUn E7"dC^+yJ np+;ӻo^\FhF?`(7ܰr¾T@MҎ#SK]w`;'Nv's@lI?$(A b }83~V}NR*aE`@~=B;&=?(U*@@\#'%nDkEkEkJ`wLf7?Fb?cFBY4GG*&yp F앨LT&*|#HxxGzXEX53L###s+]tՓtm_/=Pט)"!ēx /r)ƙ2en]wi9G QOg#Qɚј $B ]@֐Y(ǫrHz@ Jd&c:}1bvUX_?o)haPxEAtEJr8qljNz5 !!$R!qqq(=(Gg7[3NeצbSm[-N HCC=|||B %%_ Hb?Nazv x8x8x8H.T]B+>q[qiڧoz6}v %$y$"GT_ @|iqEDKzDy畕Nç/i+:0:fL[ך;"=]vE;ɈTZeERA ;ʥaz-PCV{/J>? yjB܀r x r8ir&Q`PUI䞧nnn2*3r˒>&v bb>?G i{%QR`p~W4%ukaÞa~ 04a0˯*M[,Ha{匼sNL(ߌ3 unQ <3q07&cE2od>KA-Om0xE VidJz"Vo-)ʑeY9kaN`zҳktG]/V4|:MA444̍97rXDh"6\h=`3|p!V) e?h&:[ Q^{^ 0 rdܿ, HJ`PRu3B?| QFҊh9nZܣ+ 4!?C`z[<p4 d|aGK{󶯵3YuelɷYt#}FvV$ ,:ƜKe~PHptդC8x Q:=,$77h _U;4`f|bm */@uqzȼy1"@mt>f!!###!v0a.ǴdZ2-^}@xQP8Y/jѪjz7GysD5` >"A`&2 @ʽ1I]cV;ci\GC]{\Xe䈟%dIr W^EI=}OXd8EA%@N~f5,9g^CvK#dRa {d(~fPLVa0F1mljYs>U$A2 0Q}վ{ †@^bԗ,6XljL%ST-t⋄V->fն&{E:u͋/\Ks!7] ?ԙ:Sg#.] YZ:V0#/^hjgZ\8ո`~t% *\!:EGgπϽJ"Bx2;.(>Q>I#WK):f-U掩l4ZQc]ӘOPt-xҘ'#βٷV>&Ҭ$Y߈TTc +d6Mf+'x'[^ ]}([(f`5VcHpf}-^T锘1nzw~C4R;7UWW7`MH[rHA RXXX0&&&|]I2x{ajZ=0baBH F. d qiy>h4 j۫m~%1fc+YHh D kȘDquO"U ɋʮ3ހF|(θ&P]`~ J:Y>7=lDt$оwΰ-q)HGT%~4-C07G5}ůbSH r"hIY Ö<9,﹣v\m3·N)7 [=i x6qY^*P$E- S6cQji['7Q`.^8_ 6&L[8=6퍙<st!-e`TƟkqwbS$Hn ")^(AJRw/N-A F B\~ߴmH^+g̜GM ߖp6?zٜ_Fv޶m|L<';ÝD|Z| e(@>TB#9N[H*ρi|0ƩcF[X݌=|<9 IJ2{Ж{~T}ԕSN}=+]H҅'www>(1JL$L$F`F=pPG JPt4]V!1YQ _ l糝v\c8FG#w&Mu}^\+Y醣lÐC@7@ݱ+*E*PR zlx4AFҏV٣(/RKCd'CN q>^c%%%Esѿlr]`49ƹ8=PJIǤ ݹN\'4RMa+3eNڣk=z b̓x>@ʐ!)CPsЇ-la[Jeδ3 wAsG;P@Gkp2ٰf~MKS+|Z3D=#\yŪhȟ91bE|`t9$䐲xq,A0kZ gwq&jjj{^R?SGي///MMM2y i.ͥ0U:tno&nebX 3VzVzVz'?ҿ ouzej; C`XS7a7辶fDq,@gUxPq# `wpT<䢤j HW e/-"KԒꗐd>_WDX/aH $k%$$Pi#2'Kaaaյ̶a!gŠkGm끩0TP+ߕeOO]ׄ0V|+*] (av@5I5I5ɉRFc]ԩSNC6lH:I'`hM U}>Zŀ8Z/OYoWrB@6!ogq8Ŀ|]I֧[7oTTT0& ca"++ ީM0R)([.k:Y Rn)x/8.n/4KX`fǬֺ[._ Ҫh?NGXK@5.Gg><3/u^$sٰ(_Ɨ{gҝIw&'={7sk-;j;H0o9D(QVJY)4giRV8$MIt2_YoQ-,6x8!nuOuOuubNX(N'Qy|0 0 e:sEP;w3g̺mll*Pn(7ϙϙAmyCқKo.݊݊ݝj6,~.x &uhՂ|ೱ||||vO'oOޞ0@ @Tu\u\la0[mnU/xxUzzۛѦrY{tq*W{sE RL7V3k@/L9w:ґ|@yO}fFe︚  rY,g},YhF1!u{>yy#y#y#ޥw?<7րxg鷡QQhVJG:ґMM!vݩ;8::n݂NNnkt5ֶCi/?lX3eee)7a@ LGOuh_<LnfBzqN4{u7 O@ !*^ AY o!i1מkϵ.]tҥ3~S:n#ͅÿQ?@b#H D*ڌt|k5E^JƖ-;J-^z늴# hĴ)^~/Ȋ6Ma>֣[@"GXɬWQ2dHv>N%!)#'9ZHoȁPy *ڑ!/{P_ᵞp'Ѐ0?)d9oFI:@0;P} R&|JbJbJ"pj姖Xs/^νz',,,RT(Oǻ5 YTQQ w,X_0R]] yggy<ۋZUq?˿Nԉ: ߲׳giҞkDZaͳQF=ETT*HI#iiPuCjZZjvKhh虞rj]Wr핓`:de =+#J~Pюa_g0gVzHV5X2[ `"U3xsN @ H>׎4Q$0 3&dŝb8 DG_. 3 +4RoPcHfwi8tҐ95sj戃dz2=p^H4EVKgKgKgU$A I4P80i&i&i&@b6mG#^c[^-^yE^sO`E>" @m#"Fh_;Ǭ; >($W(#UZh}fra_MOW 57jGbYYY๯!&cMkPC5kT\5X5X5ӎ@-~[gMmWkQa-NC3X'^1\ 50ޤ5ȸ1 9E`cz5Bz6n lߵf#22vIMNv,KDK`mxn5h!Ec,~n6v4ii@t#=v0q!萄@\* ݉QǟC9+.+.+.ז^[zmigzw1} Ruj4[˭?wsv @C<݈<rZ8G`v1ηTh{WU"hN%QXYGS6"hȎ&\AMZV$Ztkѭ߯~+LK2=ezy%6Kl#e=bFEn"%r_p09+<@;PjVATTTT P- _ioo39@SY)ӯkJ{NF0Ӷ63B=mBfhfhO.r9r匓3f nݜPTT%,2 _+|?`h#accxy &&JD%0?Cΐ3͌3V\G[֊NR|81zR @0x^2| !(jXH ` rD@'?5v%)Y ޡ=O]`x6YDl' Nj$!zFtXNGtح}6H,@Tv+02nnz@xQh F&~ 9hs ?Q_bԴ;O})׍_Oz,**>Rs[4e6{t6`Tjۇ=6X K~3mה o7MYG=s=(((8(.Hggg9r&LJ3J3J3@ygy 7 ,CEw=42<>m;v&Pd(2nggg_vf8SaZeXZekPn書#&;jp۳-5 dp h^ K 0'-#z5=襣===0C#F\.?ꖁ^\?2߿Xj,5L 5 Xg0}}}R3Ԭ(MLTL 3 ̂&؂-آ,I!v88 ZP jau:pNloo(1P^)RzaE(Bx`⁀;wdX `;Uݍ?)H`#b֢Zj-a%YɂֳA{ćŇŇ;Je?j4Lk.M}^QI3aҎ+I#KTCLD P+ sbV|&Ѷxx=jB=0HFԈM'Nf&L1cB #S5H&pjۭ1vvv_d[g[g[7?j᫅ `PetKpqe@VVYYcyyyViii=HJJZC|J|J| * @!_/y꧀:WERe p\Whm]ـ$$! .ą8c32M]\S1Sgdd7ro4Le2S:Ir-C6%nl`V]4k&\IC˕W_bؓ\\3x/aHH2.jݍf5WlD(.9RC3XǕ\=<5T 3+EΥ ĸA?bȼ#Eg:5eZ N|ISXۼDTXXrGi-@l/&s` `|`ܦMsfRUXP탥ZY.f%JpÝw}[,$d H̥VyKpWq;㷿˴t2:CWi)AHҟ@=C=ְuU D0c1%ƢƢ0tx͵k6;n\6lpyQf 7҃p e/YIoc֗_G>~:טL C+]Ma\_7 L%"nj|..bFC@q@^u[N8on3wL^!J`"1 c1:uNnfͮ]J<)yrm K_r0Z??/sDΤPZZ@>xC"n\TpoV-Pp5vvv^yWWfQΊfhhCm@jɇb8SNS33>g ٣w^sLrqE B cz29 QHJ~H?g)ܧ"Yɷ@B>zI[#ćrc',PDJ8)@{lm1߀oI6&-̲eqϹMX=BZ\ 0 2W Jq~ @DDĨEnnn#j :CL;'ur5PC=Eފü6U|׍grI6IX 1(>"Ǝ@>aYr `ø6T H%-`{rX@Jb*011D.c b[gˀ⮥5NJhUt]6|mZkmvyKQ-Q-Q_w},CMXnN[#]G_ ~`H>6#GچmًE:6ql؄KZ7nq?{s{s{d""""VՇJy -gԭ^+'36>v=|C |R"8,z ITG7Pr}G4x7:,aVc ~?;nw8W_m+JϿ|P*h'2[̭{53kf̌4iH'q۸ |K5N&Mƛ lzE&_|m(gαnz&M(;7gzʖHw 8CA|I֟VoLhU\2"E'!"D|TN 2t S}D]Y`%))$GQfx c'G=?ͧ; '^.,1+fć%%% o;wwF~~Џ]TG7IEv $]o{_ɥKOKWHWHWj=Aa?IE1a :P2L6o} od AL齿3$SQEosS2x$^+E5⫀ޫD@@gĦMuUJ.{LPnDRΫ}K_?zy?[wnQ(N11!ttc0C ĢE;v>RVs|̓̓̓{Q Pm#۔3R()d" CFUq) Lf5 yYF|{eCCpxx2gSvdH=hڄ6LMMj^ռE|@T*kڟķŷŷB-½pouuuԚZC]Jr|+<̃303C_C_C_SO>%M7>9.CXa$&yDGeƺ_[_Azw>TTQ@׮_\o@z7V?"^DgE#HIz0swy-bcFxTHPi|u:_>ll;Gyz$ۯCn_DejI)SՏ\˻w!TZ_S0SP*>~٘ 62\ ةTv*vB 7l>|!DNY Xsg}p.W_흵peSŦTl#!#@cfSz<_7V;e0[o\_r[&vM8 Q O]4֧hcDz;5d3TSP>{l $H.9dsH"Ֆ*))F o¦:4⋣}Xڳ@0anܬn:uT,M&4<{ytCu"?eN\ ^{鸞9qǝ2ps&wMSNn߽Un9wXwHOQ 860`S~]+r%0\1gt50~v&s0. ALcsD0 ,HG|#Dw@nW(E { >۸\p';QNE.-\NtyrD{xxmoaaI/&$2̪U?>VgCb T NNP:@DDDm{MMMfff{Z$DNfIKK"FĈ2}B} qucC 6y{{w$Iw'E{@jz%&Dx>fM ysYҙ{o L"k~1M<~m(l[65nDgԂ ڇJEMh8}|hFҜԇ9ve bWQun}z,=,=,=!C@_#2"y'Ugz<xm†eh>cC. h<}>{,ҹͻ7޼{Ě ƫW6R郳ryzQ:7{7>o|:LLLGѼpԿԿx' TʋedY3|Ue)D! Ik5븎d2ʨ& gyyyK͗/kdɆ uOiN ~O3J`0*?}T;vRS&N8eI/Df"3tr,(@#4л=lDƦ@+v-uF>M-:G9k(G"Kt B Y{ |}Ԣ$aum'S⥥ 5Gf$Kr\>13n*Ji=2{lE߻lw7̬c1@>\c( Qt}!h# %2&" `%PkƓ3&7=́y9*`gzrj _=.=AWzor>xV^w9|G>&nO?wvGxFxFxrq):={RA-5oc.@ę9RcQFr#!/'L&Iڗ}&}M}=6O^>yO5[Ɇn)'^?~l S4|@IAz6 } U4Db7R h x<'~'ED*p]yW>vY" Ph ebXݧr{=ٞNϦ|S)p. `u X$-I7!bPy0@t8YpˣGs]wu9#nMllA* SuͣBci,Bi!֫WLWD_͖ݑݑ<GkE9}H{n<7F gzзѷѷ\\\bNj}PBBBD@)’pNù;ui_}mO%˒e߆o÷GSi*MC .۾}mR5_eUSA0 U4!MH`r!|nݺu^{]uڴic"q8PK%u:5mM[x  ĵĵD9c~w&L2lka/P@ s -S׃^z=ؐؐؐyq8e5k:uuu5yˁ :\…d"H&9s +J ,~S01څv]/ ( ^\W" PQgLax1Y:fu5ķo5jQᑧEBfBfBfsqㆷX"fE̊z{D/^`?-X±cҞ 7J%WFͨ5@d@!~ˀBh _4~ x=z8' /KE ڑv!/XSfB Se:tM?kײe PݳlْuW$Ilئœ+xYlTQTQT׫]QvYy$&&Yjg B@ 0F5y VCN|kSSSGKھk.ؑcGW"a' |9}}Mٛ7֎֎֎5Ϛ55kj&I`>w#FAQKD-qk:uLk^z1 ߙ3'VL3dӈM#65%uVֻ&5l04t#*ӇMZ7)-Wj_>nwþ|LuuuӎS;R@0a(c3f vvvvvv_dRGGGJj8pAmmm6N8mcyJn&Rΐ0 zzz>p=wMұ17 |X|a|a|I']IZ&)mÇ6lR\7n666RGlQDiu/ߴu|$urg#GJ+Gr{MYPdUdUd59--mۑGzoڋk//i)5 5 5 tH$oPޠA^,{2? =,;.YMI^b)]ۻ7O?C' Sΐ%'''''`X;֮#;g۩Tv*JG}saaa5C_tO=t;FwYTgQEjz顦s_@@@@ )OyoHLxyK%S:X4W+j"- WjBa}> ;o&nr[)?7cO==7n(roLqc= AzIL3iΤ9_UUUFoKgtlmO!C :Ğ{ <ldf͊LϘnȻQյX{g흵w`xڵk׮/__ҺuIk쪮G %tEXtdate:create2022-09-13T17:01:47+00:00z%tEXtdate:modify2022-09-13T17:01:47+00:00mI(tEXtdate:timestamp2022-09-13T17:01:58+00:00nIENDB`bumblebee-status-2.2.0/docs/make.bat000066400000000000000000000014331450256260500173210ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd bumblebee-status-2.2.0/docs/modules.rst000066400000000000000000001732761450256260500201350ustar00rootroot00000000000000.. THIS DOCUMENT IS AUTO-GENERATED, DO NOT MODIFY .. To change this document, please update the docstrings in the individual modules List of modules =============== core ---- 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 date ~~~~ Displays the current date and time. Parameters: * date.format: strftime()-compatible formatting string * date.locale: locale to use rather than the system default .. image:: ../screenshots/date.png datetime ~~~~~~~~ Displays the current date and time. Parameters: * datetime.format: strftime()-compatible formatting string * datetime.locale: locale to use rather than the system default .. image:: ../screenshots/datetime.png debug ~~~~~ Shows that debug is enabled disk ~~~~ 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.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 error ~~~~~ Shows bumblebee-status errors git ~~~ Print the branch and git status for the currently focused window. Requires: * xcwd * Python module 'pygit2' .. 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: * 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. 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) .. image:: ../screenshots/load.png 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%) * memory.format: Format string (defaults to '{used}/{total} ({percent:05.02f}%)') * memory.usedonly: Only show the amount of RAM in use (defaults to False). Same as memory.format='{used}' .. image:: ../screenshots/memory.png nic ~~~ 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.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) .. image:: ../screenshots/nic.png ping ~~~~ Periodically checks the RTT of a configurable host using ICMP echos Requires the following executable: * ping Parameters: * ping.address : IP address to check * ping.timeout : Timeout for waiting for a reply (defaults to 5.0) * ping.probes : Number of probes to send (defaults to 5) * ping.warning : Threshold for warning state, in seconds (defaults to 1.0) * ping.critical: Threshold for critical state, in seconds (defaults to 2.0) .. image:: ../screenshots/ping.png 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: * pulseaudio.autostart: If set to 'true' (default is 'false'), automatically starts the pulseaudio daemon if it is not running * pulseaudio.percent_change: How much to change volume by when scrolling on the module (default is 2%) * pulseaudio.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') 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 * pactl * pavucontrol .. 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.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 ~~~~~~~~ Displays the current color temperature of redshift Requires the following executable: * redshift Parameters: * redshift.location : location provider, either of 'auto' (default), 'geoclue2', 'ipinfo' or 'manual' 'auto' uses whatever redshift is configured to do * 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 ~~~~~~~~ Displays sensor temperature and CPU frequency Parameters: * sensors2.chip: 'sensors -u' compatible filter for chip to display (default to empty - show all chips) * sensors2.showcpu: Enable or disable CPU frequency display (default: true) * sensors2.showtemp: Enable or disable temperature display (default: true) * sensors2.showfan: Enable or disable fan display (default: true) * 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.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') * sensors2.chip_field_include: Comma-separated list of adaper field to include (defaults to '' will include all by default) .. image:: ../screenshots/sensors2.png spacer ~~~~~~ Draws a widget with configurable text content. Parameters: * spacer.text: Widget contents (defaults to empty string) .. image:: ../screenshots/spacer.png speedtest ~~~~~~~~~ Performs a speedtest - only updates when the "play" button is clicked Requires the following python module: * speedtest-cli test ~~~~ Test module time ~~~~ Displays the current date and time. Parameters: * time.format: strftime()-compatible formatting string * time.locale: locale to use rather than the system default .. image:: ../screenshots/time.png vault ~~~~~ Copy passwords from a password store into the clipboard (currently supports only 'pass') 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! .. image:: ../screenshots/vault.png xrandr ~~~~~~ Shows a widget for each connected screen and allows the user to enable/disable screens. Parameters: * xrandr.overwrite_i3config: If set to 'true', this module assembles a new i3 config every time a screen is enabled or disabled by taking the file '~/.config/i3/config.template' 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 Requires the following executable: * xrandr .. image:: ../screenshots/xrandr.png contrib ------- amixer ~~~~~~ get volume level or control it Requires the following executable: * amixer Parameters: * 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! .. image:: ../screenshots/amixer.png apt ~~~ Displays APT package update information (/) Requires the following packages: * aptitude contributed by `qba10 `_ - many thanks! arandr ~~~~~~ Enables handy interaction with arandr for display management. Left-clicking will execute arandr for interactive display management. Right-clicking will bring up a context- and state-sensitive menu that will allow you to switch to a 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 contributed by `zerorust `_ - many thanks! arch-update ~~~~~~~~~~~ Check updates to Arch Linux. Requires the following executable: * checkupdates (from pacman-contrib) 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 ~~~~~~~ Displays battery status, remaining percentage and charging information. Parameters: * battery.device : Comma-separated list of battery devices to read information from (defaults to auto for auto-detection) * battery.warning : Warning threshold in % of remaining charge (defaults to 20) * battery.critical : Critical threshold in % of remaining charge (defaults to 10) * battery.showdevice : If set to 'true', add the device name to the widget (defaults to False) * battery.decorate : If set to 'false', hides additional icons (charging, etc.) (defaults to True) * battery.showpowerconsumption: If set to 'true', show current power consumption (defaults to False) * battery.compact-devices : If set to 'true', compacts multiple batteries into a single entry (default to False) (partially) contributed by `martindoublem `_ - many thanks! .. image:: ../screenshots/battery.png 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! 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`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state. Parameters: * bluetooth.device : the device to read state from (default is hci0) * bluetooth.manager : application to launch on click (blueman-manager) * bluetooth.dbus_destination : dbus destination (defaults to org.blueman.Mechanism) * bluetooth.dbus_destination_path : dbus destination path (defaults to /) * bluetooth.right_click_popup : use popup menu when right-clicked (defaults to True) contributed by `brunosmmm `_ - many thanks! .. image:: ../screenshots/bluetooth.png bluetooth2 ~~~~~~~~~~ Displays bluetooth status. Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and python-dbus to count the number of connections Parameters: * bluetooth.manager : application to launch on click (blueman-manager) 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! .. image:: ../screenshots/brightness.png caffeine ~~~~~~~~ Enable/disable automatic screen locking. Requires the following executables: * xdg-screensaver * xdotool * xprop (as dependency for xdotool) * notify-send contributed by `TheEdgeOfRage `_ - many thanks! .. image:: ../screenshots/caffeine.png cmus ~~~~ Displays information about the current song in cmus. Requires the following executable: * cmus-remote Parameters: * cmus.format: Format string for the song information. Tag values can be put in curly brackets (i.e. {artist}) Additional tags: * {file} - full song file name * {file1} - song file name without path prefix if {file} = '/foo/bar.baz', then {file1} = 'bar.baz' * {file2} - song file name without path prefix and extension suffix if {file} = '/foo/bar.baz', then {file2} = 'bar' * cmus.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles cmus.prev, cmus.next, cmus.shuffle and cmus.repeat, and the main display with play/pause function cmus.main. * cmus.server: The address of the cmus server, either a UNIX socket or host[:port]. Connects to the local instance by default. * cmus.passwd: The password to use for the TCP/IP connection. contributed by `TheEdgeOfRage `_ - many thanks! .. image:: ../screenshots/cmus.png cpu2 ~~~~ 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: * cpu2.layout: Space-separated list of widgets to add. Possible widgets are: * cpu2.maxfreq * cpu2.cpuload * cpu2.coresload * cpu2.temp * 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 * cpu2.fan_pattern: pattern to look for in the output of 'sensors -u'; required if cpu2.fanspeed widget 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! currency ~~~~~~~~ Displays currency exchange rates. Currently, displays currency between GBP and USD/EUR only. Requires the following python packages: * requests Parameters: * currency.interval: Interval in minutes between updates, default is 1. * currency.source: Source currency (ex. 'GBP', 'EUR'). Defaults to 'auto', which infers the local one from IP address. * currency.destination: Comma-separated list of destination currencies (defaults to 'USD,EUR') * currency.sourceformat: String format for source formatting; Defaults to '{}: {}' and has two variables, the base symbol and the rate list * currency.destinationdelimiter: Delimiter used for separating individual rates (defaults to '|') Note: source and destination names right now must correspond to the names used by the API of https://markets.ft.com contributed by `AntouanK `_ - many thanks! .. image:: ../screenshots/currency.png 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 * datetz.format : alias for datetimetz.format * timetz.format : alias for datetimetz.format * timetz.timezone : alias for datetimetz.timezone * datetimetz.locale : locale to use rather than the system default * datetz.locale : alias for datetimetz.locale * timetz.locale : alias for datetimetz.locale * timetz.timezone : alias for datetimetz.timezone contributed by `frankzhao `_ - many thanks! datetz ~~~~~~ Displays the current date and time. Parameters: * date.format: strftime()-compatible formatting string * date.locale: locale to use rather than the system default deadbeef ~~~~~~~~ Displays the current song being played in DeaDBeeF and provides some media control bindings. Left click toggles pause, scroll up skips the current song, scroll down returns to the previous song. Parameters: * deadbeef.format: Format string (defaults to '{artist} - {title}') Available values are: {artist}, {title}, {album}, {length}, {trackno}, {year}, {comment}, {copyright}, {time} This is deprecated, but much simpler. * deadbeef.tf_format: A foobar2000 title formatting-style format string. These can be much more sophisticated than the standard format strings. This is off by default, but specifying any tf_format will enable it. If both deadbeef.format and deadbeef.tf_format are specified, deadbeef.tf_format takes priority. * deadbeef.tf_format_if_stopped: Controls whether or not the tf_format format string should be displayed even if no song is paused or playing. This could be useful if you want to implement your own stop strings with the built in logic. Any non- null value will enable this (by default the module will hide itself when the player is stopped). * deadbeef.previous: Change binding for previous song (default is left click) * deadbeef.next: Change binding for next song (default is right click) * deadbeef.pause: Change binding for toggling pause (default is middle click) Available options for deadbeef.previous, deadbeef.next and deadbeef.pause are: LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN contributed by `joshbarrass `_ - many thanks! deezer ~~~~~~ Displays the current song being played Requires the following library: * python-dbus Parameters: * deezer.format: Format string (defaults to '{artist} - {title}') Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} * deezer.previous: Change binding for previous song (default is left click) * deezer.next: Change binding for next song (default is right click) * deezer.pause: Change binding for toggling pause (default is middle click) Available options for deezer.previous, deezer.next and deezer.pause are: LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN contributed by `wwmoraes `_ - many thanks! dnf ~~~ Displays DNF package update information (///) Requires the following executable: * dnf .. image:: ../screenshots/dnf.png docker_ps ~~~~~~~~~ Displays the number of docker containers running Requires the following python packages: * docker contributed by `jlopezzarza `_ - many thanks! dunst ~~~~~ Toggle dunst notifications. contributed by `eknoes `_ - many thanks! .. image:: ../screenshots/dunst.png dunstctl ~~~~~~~~ Toggle dunst notifications using dunstctl. When notifications are paused using this module dunst doesn't get killed and you'll keep getting notifications on the background that will be displayed when unpausing. This is specially useful if you're using dunst's scripting (https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to be running. Scripts will be executed when dunst gets unpaused. Requires: * dunst v1.5.0+ contributed by `cristianmiranda `_ - 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 ~~~~~~~~~ Displays the price of a cryptocurrency. Requires the following python packages: * requests Parameters: * getcrypto.interval: Interval in seconds for updating the price, default is 120, less than that will probably get your IP banned. * getcrypto.getbtc: 0 for not getting price of BTC, 1 for getting it (default). * getcrypto.geteth: 0 for not getting price of ETH, 1 for getting it (default). * getcrypto.getltc: 0 for not getting price of LTC, 1 for getting it (default). * getcrypto.getcur: Set the currency to display the price in, usd is the default. contributed by `Ryunaq `_ - many thanks! .. image:: ../screenshots/getcrypto.png github ~~~~~~ Displays the unread GitHub notifications count for a GitHub user using the following reasons: * 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 Parameters: * github.token: GitHub user access token, the token needs to have the 'notifications' scope. * github.interval: Interval in minutes between updates, default is 5. * github.reasons: Comma separated reasons to be parsed (e.g.: github.reasons=mention,team_mention,review_requested) contributed by: * v1 - `yvesh `_ - many thanks! * v2 - `cristianmiranda `_ - many thanks! .. image:: ../screenshots/github.png gpmdp ~~~~~ Displays information about the current song in Google Play music player. Requires the following executable: * gpmdp-remote contributed by `TheEdgeOfRage `_ - many thanks! hddtemp ~~~~~~~ Fetch hard drive temperature data from a hddtemp daemon that runs on localhost and default port (7634) contributed by `somospocos `_ - many thanks! hostname ~~~~~~~~ Displays the system hostname. contributed by `varkokonyi `_ - many thanks! http_status ~~~~~~~~~~~ Display HTTP status code Parameters: * http__status.label: Prefix label (optional) * http__status.target: Target to retrieve the HTTP status from * http__status.expect: Expected HTTP status contributed by `valkheim `_ - many thanks! .. image:: ../screenshots/http_status.png 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') contributed by `freed00m `_ - many thanks! .. image:: ../screenshots/indicator.png kernel ~~~~~~ Shows Linux kernel version information contributed by `pierre87 `_ - many thanks! .. image:: ../screenshots/kernel.png layout-xkbswitch ~~~~~~~~~~~~~~~~ Displays and changes the current keyboard layout Requires the following executable: * xkb-switch contributed by `somospocos `_ - many thanks! layout_xkbswitch ~~~~~~~~~~~~~~~~ Displays and changes the current keyboard layout Requires the following executable: * xkb-switch contributed by `somospocos `_ - many thanks! libvirtvms ~~~~~~~~~~ Displays count of running libvirt VMs. Required the following python packages: * libvirt 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 ~~~~ Displays information about the current song in mocp. Left click toggles play/pause. Right click toggles shuffle. Requires the following executable: * mocp Parameters: * mocp.format: Format string for the song information. Replace string sequences with the actual information: * %state State * %file File * %title Title, includes track, artist, song title and album * %artist Artist * %song SongTitle * %album Album * %tt TotalTime * %tl TimeLeft * %ts TotalSec * %ct CurrentTime * %cs CurrentSec * %b Bitrate * %r Sample rate contributed by `chrugi `_ - many thanks! mpd ~~~ Displays information about the current song in mpd. Requires the following executable: * mpc Parameters: * mpd.format: Format string for the song information. Supported tags (see `man mpc` for additional information) * {name} * {artist} * {album} * {albumartist} * {comment} * {composer} * {date} * {originaldate} * {disc} * {genre} * {performer} * {title} * {track} * {time} * {file} * {id} * {prio} * {mtime} * {mdate} Additional tags: * {position} - position of currently playing song not to be confused with %position% mpc tag * {duration} - duration of currently playing song * {file1} - song file name without path prefix if {file} = '/foo/bar.baz', then {file1} = 'bar.baz' * {file2} - song file name without path prefix and extension suffix if {file} = '/foo/bar.baz', then {file2} = 'bar' * mpd.host: MPD host 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 ~~~~~~~~~~~~~~~ Displays network traffic Requires the following library: * netifaces contributed by `izn `_ - many thanks! notmuch_count ~~~~~~~~~~~~~ Displays the result of a notmuch count query default : unread emails which path do not contained 'Trash' (notmuch count 'tag:unread AND NOT path:/.*Trash.*/') Parameters: * notmuch_count.query: notmuch count query to show result Errors: if the notmuch query failed, the shown value is -1 Dependencies: notmuch (https://notmuchmail.org/) contributed by `abdoulayeYATERA `_ - many thanks! nvidiagpu ~~~~~~~~~ Displays GPU name, temperature and memory usage. Parameters: * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{mem_used}/{mem_total} MiB') Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct} Requires nvidia-smi contributed by `RileyRedpath `_ - 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. 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) * octoprint.webcam : Set to True if a webcam is connected (default: False) 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') Requires the following executables: * fakeroot * pacman 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 ~~~~~~ Displays the pi-hole status (up/down) together with the number of ads that were blocked today Parameters: * pihole.address : pi-hole address (e.q: http://192.168.1.3) * 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 ~~~~~~~~~ Displays information about the current song in vlc, audacious, bmp, xmms2, spotify and others 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 pomodoro ~~~~~~~~ Display and run a Pomodoro timer. Left click to start timer, left click again to pause. Right click will cancel the timer. Parameters: * pomodoro.work: The work duration of timer in minutes (defaults to 25) * pomodoro.break: The break duration of timer in minutes (defaults to 5) * pomodoro.format: Timer display format with '%m' and '%s' for minutes and seconds (defaults to '%m:%s') Examples: '%m min %s sec', '%mm', '', 'timer' * pomodoro.notify: Notification command to run when timer ends/starts (defaults to nothing) 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) 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 ~~~~~ Displays and changes the current selected prime video card Left click will call 'sudo prime-select nvidia' Right click will call 'sudo prime-select nvidia' Running these commands without a password requires editing your sudoers file (always use visudo, it's very easy to make a mistake and get locked out of your computer!) sudo visudo -f /etc/sudoers.d/prime Then put a line like this in there: user ALL=(ALL) NOPASSWD: /usr/bin/prime-select If you can't figure out the sudoers thing, then don't worry, it's still really useful. 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 * prime-select contributed by `jeffeb3 `_ - many thanks! progress ~~~~~~~~ Show progress for cp, mv, dd, ... Parameters: * progress.placeholder: Text to display while no process is running (defaults to 'n/a') * progress.barwidth: Width of the progressbar if it is used (defaults to 8) * progress.format: Format string (defaults to '{bar} {cmd} {arg}') Available values are: {bar} {pid} {cmd} {arg} {percentage} {quantity} {speed} {time} * progress.barfilledchar: Character used to draw the filled part of the bar (defaults to '#'), notice that it can be a string * progress.baremptychar: Character used to draw the empty part of the bar (defaults to '-'), notice that it can be a string Requires the following executable: * progress 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 . rotation ~~~~~~~~ Shows a widget for each connected screen and allows the user to loop through different orientations. Requires the following executable: * xrandr rss ~~~ RSS news ticker Fetches rss news items and shows these as a news ticker. Left-clicking will open the full story in a browser. New stories are highlighted. Parameters: * rss.feeds : Space-separated list of RSS URLs * rss.length : Maximum length of the module, default is 60 contributed by `lonesomebyte537 `_ - many thanks! 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 be: 'coretemp-isa-00000/Core 0/temp1_input' (defaults to 'false') * sensors.match: (fallback) Line to match against output of 'sensors -u' (default: temp1_input) * sensors.match_pattern: (fallback) Line to match against before temperature is read (no default) * sensors.match_number: (fallback) which of the matches you want (default -1: last match). * sensors.show_freq: whether to show CPU frequency. (default: true) contributed by `mijoharas `_ - many thanks! .. image:: ../screenshots/sensors.png shell ~~~~~ Execute command in shell and print result Few command examples: 'ping -c 1 1.1.1.1 | grep -Po '(?<=time=)\d+(\.\d+)? ms'' 'echo 'BTC=$(curl -s rate.sx/1BTC | grep -Po \'^\d+\')USD'' 'curl -s https://wttr.in/London?format=%l+%t+%h+%w' 'pip3 freeze | wc -l' 'any_custom_script.sh | grep arguments' Parameters: * shell.command: Command to execute Use single parentheses if evaluating anything inside (sh-style) For example shell.command='echo $(date +'%H:%M:%S')' But NOT shell.command='echo $(date +'%H:%M:%S')' Second one will be evaluated only once at startup * shell.interval: Update interval in seconds (defaults to 1s == every bumblebee-status update) * shell.async: Run update in async mode. Won't run next thread if previous one didn't finished yet. Useful for long running scripts to avoid bumblebee-status freezes (defaults to False) contributed by `rrhuffy `_ - many thanks! shortcut ~~~~~~~~ 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). 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)' Parameters: * shortcut.cmds : List of commands to execute * shortcut.labels: List of widgets' labels (text) * shortcut.delim : Commands and labels delimiter (; semicolon by default) contributed by `cacyss0807 `_ - many thanks! .. image:: ../screenshots/shortcut.png 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.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 ~~~~~~~~ Displays the state of a Space API endpoint Space API is an API for hackspaces based on JSON. See spaceapi.io for an example. Requires the following libraries: * requests Parameters: * spaceapi.url: String representation of the api endpoint * spaceapi.format: Format string for the output Format Strings: * Format strings are indicated by double %% * They represent a leaf in the JSON tree, layers separated by '.' * Boolean values can be overwritten by appending '%true%false' in the format string * Example: to reference 'open' in '{'state':{'open': true}}' you would write '%%state.open%%', if you also want to say 'Open/Closed' depending on the boolean you would write '%%state.open%Open%Closed%%' contributed by `rad4day `_ - many thanks! spotify ~~~~~~~ Displays the current song being played and allows pausing, skipping ahead, and skipping back. 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 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 Parameters: * stock.symbols : Comma-separated list of symbols to fetch * stock.change : Should we fetch change in stock value (defaults to True) contributed by `msoulier `_ - many thanks! .. image:: ../screenshots/stock.png sun ~~~ 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 (if none of those are set, location is determined automatically via location APIs) contributed by `lonesomebyte537 `_ - many thanks! system ~~~~~~ system module adds the possibility to * shutdown * 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.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') * system.switch_user: specify a command for switching the user (defaults to 'i3exit switch_user') * system.lock: specify a command for locking the screen (defaults to 'i3exit lock') * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') Requirements: tkinter (python3-tk package on debian based systems either you can install it as python package) contributed by `bbernhard `_ - many thanks! taskwarrior ~~~~~~~~~~~ Displays the number of pending tasks in TaskWarrior. Requires the following library: * taskw 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 ~~~~~~ Displays the current date and time. Parameters: * time.format: strftime()-compatible formatting string * time.locale: locale to use rather than the system default title ~~~~~ Displays focused i3 window title. Requirements: * i3ipc 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! .. image:: ../screenshots/title.png todo ~~~~ Displays the number of todo items from a text file Parameters: * todo.file: File to read TODOs from (defaults to ~/Documents/todo.txt) 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 ` traffic ~~~~~~~ Displays network IO for interfaces. Parameters: * traffic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth') * traffic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) * 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 char shows 2 seconds. If set, enables up/down traffic graphs contributed by `meain `_ - many thanks! .. image:: ../screenshots/traffic.png twmn ~~~~ Toggle twmn notifications. Requires the following executable: * systemctl contributed by `Pseudonick47 `_ - many thanks! uptime ~~~~~~ Displays the system uptime. contributed by `ccoors `_ - many thanks! .. image:: ../screenshots/uptime.png vpn ~~~ Displays the VPN profile that is currently in use. Left click opens a popup menu that lists all available VPN profiles and allows to establish a VPN connection using that profile. Prerequisites: * tk python library (usually python-tk or python3-tk, depending on your distribution) * nmcli needs to be installed and configured properly. To quickly test, whether nmcli is working correctly, type 'nmcli -g NAME,TYPE,DEVICE con' which lists all the connection profiles that are configured. Make sure that your VPN profile is in that list! e.g: to import a openvpn profile via nmcli: `sudo nmcli connection import type openvpn file ` contributed by `bbernhard `_ - many thanks! watson ~~~~~~ Displays the status of watson (time-tracking tool) Requires the following executable: * watson contributed by `bendardenne `_ - many thanks! weather ~~~~~~~ Displays the temperature on the current location based on the ip Requires the following python packages: * requests Parameters: * weather.location: Set location, defaults to 'auto' for getting location automatically from a web service If set to a comma-separated list, left-click and right-click can be used to rotate the locations. Locations should be city names or city ids. * 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 http://api.openweathermap.org contributed by `TheEdgeOfRage `_ - many thanks! .. image:: ../screenshots/weather.png xkcd ~~~~ Opens a random xkcd comic in the browser. contributed by `whzup `_ - many thanks! yubikey ~~~~~~~ Shows yubikey information Requires: https://github.com/Yubico/python-yubico The output indicates that a YubiKey is not connected or it displays the corresponding serial number. contributed by `EmmaTinten `_ - many thanks! 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: '') * zpool.format: Format string, tags {name}, {used}, {left}, {size}, {percentfree}, {percentuse}, {status}, {shortstatus}, {fragpercent}, {deduppercent} are supported. (Default: '{name} {used}/{size} ({percentfree}%)') * zpool.showio: Show also widgets detailing current read and write I/O (Default: true) * zpool.ioformat: Format string for I/O widget, tags {ops} (operations per seconds) and {band} (bandwidth) are supported. (Default: '{band}') * zpool.warnfree: Warn if free space is below this percentage (Default: 10) * zpool.sudo: Use sudo when calling the `zpool` binary. (Default: false) Option `zpool.sudo` is intended for Linux users using zfsonlinux older than 0.7.0: In pre-0.7.0 releases of zfsonlinux regular users couldn't invoke even informative commands such as `zpool list`. If this option is true, command `zpool list` is invoked with sudo. If this option is used, the following (or ekvivalent) must be added to the `sudoers(5)`: ``` ALL = (root) NOPASSWD: /usr/bin/zpool list ``` Be aware of security implications of doing this! contributed by `adam-dej `_ - many thanks! .. image:: ../screenshots/zpool.png bumblebee-status-2.2.0/docs/other/000077500000000000000000000000001450256260500170345ustar00rootroot00000000000000bumblebee-status-2.2.0/docs/other/NOTES.md000066400000000000000000000011171450256260500202460ustar00rootroot00000000000000# Design - core: only PSL - pass if modules are missing - minimize dependencies, code - test everything in the core framework # small stuff - @parameter? (or was it @attribute?) - remove getter/setters - use __ for private ## Improvements - app launcher (list of apps, themeable) - github pages? ## TODO - themes: use colors to improve theme readability - convert some stuff to simple attributes to reduce LOCs - use widget index for bumblebee-ctl as alternative (??) - use pytest? # documentation Add info about error widget and events for error logging - which location APIs are used? bumblebee-status-2.2.0/docs/requirements.txt000066400000000000000000000000161450256260500211740ustar00rootroot00000000000000docutils<0.18 bumblebee-status-2.2.0/docs/src/000077500000000000000000000000001450256260500165025ustar00rootroot00000000000000bumblebee-status-2.2.0/docs/src/bumblebee_status.core.rst000066400000000000000000000031171450256260500235120ustar00rootroot00000000000000bumblebee\_status.core package ============================== Submodules ---------- bumblebee\_status.core.config module ------------------------------------ .. automodule:: bumblebee_status.core.config :members: :undoc-members: :show-inheritance: bumblebee\_status.core.decorators module ---------------------------------------- .. automodule:: bumblebee_status.core.decorators :members: :undoc-members: :show-inheritance: bumblebee\_status.core.event module ----------------------------------- .. automodule:: bumblebee_status.core.event :members: :undoc-members: :show-inheritance: bumblebee\_status.core.input module ----------------------------------- .. automodule:: bumblebee_status.core.input :members: :undoc-members: :show-inheritance: bumblebee\_status.core.module module ------------------------------------ .. automodule:: bumblebee_status.core.module :members: :undoc-members: :show-inheritance: bumblebee\_status.core.output module ------------------------------------ .. automodule:: bumblebee_status.core.output :members: :undoc-members: :show-inheritance: bumblebee\_status.core.theme module ----------------------------------- .. automodule:: bumblebee_status.core.theme :members: :undoc-members: :show-inheritance: bumblebee\_status.core.widget module ------------------------------------ .. automodule:: bumblebee_status.core.widget :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: bumblebee_status.core :members: :undoc-members: :show-inheritance: bumblebee-status-2.2.0/docs/src/bumblebee_status.rst000066400000000000000000000007251450256260500225650ustar00rootroot00000000000000bumblebee\_status package ========================= Subpackages ----------- .. toctree:: :maxdepth: 4 bumblebee_status.core bumblebee_status.util Submodules ---------- bumblebee\_status.discover module --------------------------------- .. automodule:: bumblebee_status.discover :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: bumblebee_status :members: :undoc-members: :show-inheritance: bumblebee-status-2.2.0/docs/src/bumblebee_status.util.rst000066400000000000000000000026321450256260500235400ustar00rootroot00000000000000bumblebee\_status.util package ============================== Submodules ---------- bumblebee\_status.util.algorithm module --------------------------------------- .. automodule:: bumblebee_status.util.algorithm :members: :undoc-members: :show-inheritance: bumblebee\_status.util.cli module --------------------------------- .. automodule:: bumblebee_status.util.cli :members: :undoc-members: :show-inheritance: bumblebee\_status.util.format module ------------------------------------ .. automodule:: bumblebee_status.util.format :members: :undoc-members: :show-inheritance: bumblebee\_status.util.graph module ----------------------------------- .. automodule:: bumblebee_status.util.graph :members: :undoc-members: :show-inheritance: bumblebee\_status.util.location module -------------------------------------- .. automodule:: bumblebee_status.util.location :members: :undoc-members: :show-inheritance: bumblebee\_status.util.popup module ----------------------------------- .. automodule:: bumblebee_status.util.popup :members: :undoc-members: :show-inheritance: bumblebee\_status.util.store module ----------------------------------- .. automodule:: bumblebee_status.util.store :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: bumblebee_status.util :members: :undoc-members: :show-inheritance: bumblebee-status-2.2.0/docs/themes.rst000066400000000000000000000066371450256260500177460ustar00rootroot00000000000000List of available themes ======================== .. warning:: Some themes (e.g. all ‘powerline’ themes) require Font Awesome __ and a powerline-compatible font (powerline-fonts __. .. note:: If you want to add your own theme, just drop it into ~/.config/bumblebee-status/themes/ .. figure:: ../screenshots/themes/powerline-gruvbox.png :alt: Gruvbox Powerline Gruvbox Powerline (-t gruvbox-powerline) (contributed by `TheEdgeOfRage `__) .. figure:: ../screenshots/themes/gruvbox-powerline-light.png :alt: Gruvbox Powerline Light Gruvbox Powerline Light (-t gruvbox-powerline-light) (contributed by `freed00m `__) .. figure:: ../screenshots/themes/powerline-solarized.png :alt: Solarized Powerline Solarized Powerline (-t solarized-powerline) .. figure:: ../screenshots/themes/gruvbox.png :alt: Gruvbox Gruvbox (-t gruvbox) .. figure:: ../screenshots/themes/gruvbox-light.png :alt: Gruvbox Light Gruvbox Light (-t gruvbox-light) (contributed by `freed00m `__) .. figure:: ../screenshots/themes/solarized.png :alt: Solarized Solarized (-t solarized) .. figure:: ../screenshots/themes/powerline.png :alt: Powerline Powerline (-t powerline) .. figure:: ../screenshots/themes/powerline-greyish.png :alt: Greyish Powerline Greyish Powerline (-t greyish-powerline) (contributed by Joshua Bark) .. figure:: ../screenshots/themes/iceberg.png :alt: Iceberg Iceberg (-t iceberg) (contributed by `whzup `__) .. figure:: ../screenshots/themes/iceberg-powerline.png :alt: Iceberg Powerline Iceberg Powerline (-t iceberg-powerline) (contributed by `whzup `__) .. figure:: ../screenshots/themes/iceberg-dark-powerline.png :alt: Iceberg Dark Powerline Iceberg Dark Powerline (-t iceberg-dark-powerline) (contributed by `gkeep `__) .. figure:: ../screenshots/themes/iceberg-rainbow.png :alt: Iceberg Rainbow Iceberg Rainbow (-t iceberg-rainbow) (contributed by `whzup `__) .. figure:: ../screenshots/themes/iceberg-contrast.png :alt: Iceberg Contrast Iceberg Contrast (-t iceberg-contrast) (contributed by `martindoublem `__) .. figure:: ../screenshots/themes/onedark-powerline.png :alt: One Dark Powerline One Dark Powerline (-t onedark-powerline) (contributed by `dillasyx `__) .. figure:: ../screenshots/themes/dracula-powerline.png :alt: Dracula Powerline Dracula Powerline (-t dracula-powerline) (contributed by `xsteadfastx `__) .. figure:: ../screenshots/themes/nord-powerline.png :alt: Nord Powerline Nord Powerline (-t nord-powerline) (contributed by `uselessthird `__) .. figure:: ../screenshots/themes/night-powerline.png :alt: Night Powerline Night Powerline (-t night-powerline) (contributed by `LtPeriwinkle `__) .. figure:: ../screenshots/themes/default.png :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 `__) bumblebee-status-2.2.0/generate-base-tests.py000077500000000000000000000051731450256260500212100ustar00rootroot00000000000000#!/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 bumblebee-status-2.2.0/man/000077500000000000000000000000001450256260500155365ustar00rootroot00000000000000bumblebee-status-2.2.0/man/bumblebee-ctl.1000066400000000000000000000015161450256260500203250ustar00rootroot00000000000000.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