pax_global_header 0000666 0000000 0000000 00000000064 13567273623 0014530 g ustar 00root root 0000000 0000000 52 comment=ee53f0d41142062981e3547859643214414428a7
i3pystatus-3.35+git20191126.5a8eaf4/ 0000775 0000000 0000000 00000000000 13567273623 0016353 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/.gitignore 0000664 0000000 0000000 00000000221 13567273623 0020336 0 ustar 00root root 0000000 0000000 *__pycache__*
*.pyc
i3pystatus/__main__.py
build/*
dist/*
*.egg-info/*
*~
.i3pystatus-*
ci-build
docs/_build
*.swp
*.ropeproject
\.idea/
venv/
i3pystatus-3.35+git20191126.5a8eaf4/.travis.yml 0000664 0000000 0000000 00000000172 13567273623 0020464 0 ustar 00root root 0000000 0000000 language: python
sudo: false
python:
- "3.4"
install:
- "pip install -r dev-requirements.txt"
script: "./ci-build.sh"
i3pystatus-3.35+git20191126.5a8eaf4/CONTRIBUTORS 0000664 0000000 0000000 00000001624 13567273623 0020236 0 ustar 00root root 0000000 0000000 aaron-lebo
Alastair Houghton
Alexis Lahouze
Alex Timmermann
Andrés Martano
Argish42
Armin Fisslthaler
Armin F. Gnosa
Arvedui
Baptiste Grenier
bparmentier
Cezary Biele
cganas
Christopher Ganas
Chris Wood
David Foucher
David Garcia Quintas
David Wahlstrom
dubwoc
eBrnd
Erik Johnson
enkore
facetoe
Frank Tackitt
gacekjk
Georg Sieber
Goran Mekić
Gordon Schulz
Holden Salomon
Hugo Osvaldo Barrera
Iliyas Jorio
Ismael
Ismael Puerto
Jan Oliver Oelerich
Jason Hite
Joaquin Ignacio Barotto
Jörg Thalheim
Josef Gajdusek
Júlio Rieger Lucchese
Kenneth Lyons
krypt-n
Lukáš Mandák
Łukasz Jędrzejewski
Mathis Felardos
Matthias Pronk
Matthieu Coudron
Matus Telgarsky
Michael Schmidt
microarm15
Mikael Knutsson
Naglis Jonaitis
Pandada8
Paul Bienkowski
Philip Dexter
plumps
Raphael Scholer
Sergei Turukin
Sergey Rublev
siikamiika
Simon Legner
Stéphane Brunner
SyxbEaEQ2
Talwrii
theswitch
tomasm
Tom X. Tobin
Tyjak
Zack Gold
i3pystatus-3.35+git20191126.5a8eaf4/MIT-LICENSE 0000664 0000000 0000000 00000002151 13567273623 0020006 0 ustar 00root root 0000000 0000000 Copyright (c) 2012 Jan Oliver Oelerich, http://www.oelerich.org
Copyright (c) 2013 mabe, http://enkore.de
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. i3pystatus-3.35+git20191126.5a8eaf4/README.rst 0000664 0000000 0000000 00000003520 13567273623 0020042 0 ustar 00root root 0000000 0000000 i3pystatus
==========
.. image:: https://travis-ci.org/enkore/i3pystatus.svg?branch=master
:target: https://travis-ci.org/enkore/i3pystatus
i3pystatus is a large collection of status modules compatible with i3bar from the i3 window manager.
:License: MIT
:Python: 3.4+
:Governance: Patches that don't break the build (Travis or docs) are generally just merged. This is a "do-it-yourself" project, so to speak.
:Releases: No further releases are planned. Install it from Git.
Installation
------------
**Supported Python versions**
i3pystatus requires Python 3.4 or newer and is not compatible with
Python 2.x. Some modules require additional dependencies
documented in the docs.
::
pip3 install git+https://github.com/enkore/i3pystatus.git
Documentation
-------------
`All further user documentation has been moved here. `_
The changelog for old releases can be found `here. `_
Contributors
------------
A list of all contributors can be found in `CONTRIBUTORS `_, but git likely has more up-to-date information. i3pystatus was initially written by Jan Oliver Oelerich and later ported to Python 3 and mostly rewritten by enkore.
Contribute
----------
To contribute a module, make sure it uses one of the ``Module`` classes. Most modules
use ``IntervalModule``, which just calls a function repeatedly in a specified interval.
The ``output`` attribute should be set to a dictionary which represents your modules output,
the protocol is documented `here `_.
Developer documentation is available in the source code and `here
`_.
**Patches and pull requests are very welcome :-)**
i3pystatus-3.35+git20191126.5a8eaf4/ci-build.sh 0000775 0000000 0000000 00000001441 13567273623 0020402 0 ustar 00root root 0000000 0000000 #!/bin/bash -xe
python3 --version
py.test --version
python3 -mpycodestyle --version
# Target directory for all build files
BUILD=${1:-ci-build}
rm -rf ${BUILD}/
mkdir -p $BUILD
python3 -mpycodestyle i3pystatus tests
# Check that the setup.py script works
rm -rf ${BUILD}/test-install ${BUILD}/test-install-bin
mkdir ${BUILD}/test-install ${BUILD}/test-install-bin
PYTHONPATH=${BUILD}/test-install python3 setup.py --quiet install --install-lib ${BUILD}/test-install --install-scripts ${BUILD}/test-install-bin
test -f ${BUILD}/test-install-bin/i3pystatus
test -f ${BUILD}/test-install-bin/i3pystatus-setting-util
PYTHONPATH=${BUILD}/test-install py.test -q --junitxml ${BUILD}/testlog.xml tests
# Check that the docs build w/o warnings (-W flag)
sphinx-build -Nq -b html -W docs ${BUILD}/docs/
i3pystatus-3.35+git20191126.5a8eaf4/dev-requirements.txt 0000664 0000000 0000000 00000000125 13567273623 0022411 0 ustar 00root root 0000000 0000000 pytest>=2.5
sphinx>=1.1,<1.5
colour>=0.0.5
mock>=1.0
pycodestyle>=1.5.7
i3ipc>=1.2.0
i3pystatus-3.35+git20191126.5a8eaf4/docs/ 0000775 0000000 0000000 00000000000 13567273623 0017303 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/docs/Makefile 0000664 0000000 0000000 00000015172 13567273623 0020751 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/i3pystatus.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/i3pystatus.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/i3pystatus"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/i3pystatus"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
i3pystatus-3.35+git20191126.5a8eaf4/docs/_static/ 0000775 0000000 0000000 00000000000 13567273623 0020731 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/docs/_static/i3pystatus.css 0000664 0000000 0000000 00000000555 13567273623 0023600 0 ustar 00root root 0000000 0000000
#module-reference dl.class dt .property,
#module-reference dl.class dt .descclassname,
#module-reference dl.class dt .descname
{
display: none;
}
#module-reference dl.class dd {
margin-top: -1.5em;
}
#module-reference dl.class .modheader {
margin-left: -2em;
margin-bottom: .5em;
}
#module-reference dl.class .modname {
font-weight: bold;
}
i3pystatus-3.35+git20191126.5a8eaf4/docs/changelog.rst 0000664 0000000 0000000 00000032050 13567273623 0021764 0 ustar 00root root 0000000 0000000
Changelog
=========
**No further releases are planned. Install it from Git.**
3.35 (2016-08-31)
+++++++++++++++++
- New modules
- :py:mod:`.google_calendar`: Displays next Google Calendar event
- :py:mod:`.openfiles`: Report open files count
- :py:mod:`.ping`: Display ping time to host
- :py:mod:`.scores`: Display sport scores
- :py:mod:`.scratchpad`: Display number of windows and urgency hints on i3 scratchpad
- :py:mod:`.taskwarrior`: Pending tasks in taskwarrior
- :py:mod:`.wunderground`: Similar to :py:mod:`.weather`, but uses wunderground
- :py:mod:`.zabbix`: Zabbix alerts watcher
- ``i3pystatus`` binary now takes an optional path to a config file
- (purely optional, doesn't change any existing configurations)
- Fixed a bug with required settings (did only occur in develoment branch)
- :py:mod:`.clock`: timezone-related fixes with multiple clocks
- :py:mod:`.dpms`: Added format_disabled option
- :py:mod:`.github`: Added support for acccess tokens
- :py:mod:`.gpu_temp`: Added display_if setting
- :py:mod:`.mail.imap`: Add support for IDLE if imaplib2 is installed
- :py:mod:`.mpd`: Bug fixes
- :py:mod:`.network`: Bug fixes. Upgrading to ``netifaces>=0.10.5`` is recommended for avoiding IPv6-related bugs (disabling IPv6 is of course also a well-working solution)
- :py:mod:`.now_playing`: Also check activatable D-Bus services, bug fixes
- :py:mod:`.openvpn`: Added support for toggling connection on click
- :py:mod:`.pomodoro`: Bug fixes
- :py:mod:`.pulseaudio`: Display/control active sink, bug fixes
- :py:mod:`.reddit`: Fixes for praw
- :py:mod:`.temp`: Added display_if setting
- :py:mod:`.updates`: Added dnf (rpm-based distros) backend
- updates: Added notification support with summary of all available updates
- :py:mod:`.weather`: Added color_icons option, bug fixes
- :py:mod:`.xkblayout`: Bug fixes
3.34 (2016-02-14)
+++++++++++++++++
* New modules
- :py:mod:`.moon`: Display moon phase
- :py:mod:`.online`: Display internet connectivity
- :py:mod:`.xkblayout`: View and change keyboard layout
- :py:mod:`.plexstatus`: View status of Plex Media Server
- :py:mod:`.iinet`: View iiNet internet usage
- :py:mod:`.gpu_mem`, :py:mod:`.gpu_temp`: View memory and temperature stats of nVidia cards
- :py:mod:`.solaar`: Show battery status of Solaar / Logitech Unifying devices
- :py:mod:`.zabbix`: Alerts watcher for the Zabbix enterprise network monitor
- :py:mod:`.sge`: Sun Grid Engine (SGE) monitor
- :py:mod:`.timer`: Timer
- :py:mod:`.syncthing`: Syncthing monitor and control
- :py:mod:`.vk`: Displays number of messages in VKontakte
* Applications started from click events don't block other click events now
* Fixed crash with desktop notifications when python-gobject is installed, but no notification daemon is running
* Log file name is now an option (``logfile`` of :py:class:`.Status`)
* Server used for checking internet connectivity is now an option (``internet_check`` of :py:class:`.Status`)
* Added double click support for click events
* Formatter data is now available with most modules for program callbacks
* Changed default mode to standalone mode
* ``self`` is not passed anymore by default to external Python callbacks (see :py:func:`.get_module`)
* :py:mod:`.dota2wins`: Now accepts usernames in place of a Steam ID
* dota2wins: Changed win percentage to be a float
* :py:mod:`.uptime`: Added days, hours, minutes, secs formatters
* :py:mod:`.battery`: Added alert command feature (runs a shell
command when the battery is discharged below a preset threshold)
* :py:mod:`.spotify`: Added status, format\_not\_running and color\_not\_running settings, rewrite
* :py:mod:`.cmus`: Added status, format\_not\_running and color\_not\_running settings
* :py:mod:`.cmus`: Fixed bug that sometimes lead to empty output
* :py:mod:`.shell`: Added formatting capability
* :py:mod:`.cpu_usage`: Added color setting
* :py:mod:`.mpd`: Added hide\_inactive settings
* mpd: Fixed a bug where an active playlist would be assumed, leading to no output
* mpd: Added support for UNIX sockets
* :py:mod:`.updates`: Added yaourt backend
* updates: Can display a working/busy message now
* updates: Additional formatters for every backend (to distinguish pacman vs. AUR updates, for example)
* :py:mod:`.reddit`: Added link\_karma and comment\_karma formatters
* :py:mod:`.openvpn`: Configurable up/down symbols
* openvpn: Rename colour_up/colour_down to color_up/color_down
* openvpn: NetworkManager compatibility
* :py:mod:`.disk`: Improved handling of unmounted drives. Previously
the free space of the underlying filesystem would be reported if the
path provided was a directory but not a valid mountpoint. This adds
a check to first confirm whether a directory is a mountpoint using
os.path.ismount(), and if not, then runs an os.listdir() to count
the files; empty directories are considered not mounted. This
functionality allows for usage on setups with NFS and will not
report free space of underlying filesystem in cases with local
mountpoints as path.
* :py:mod:`.battery`: Added ``bar_design`` formatter
* :py:mod:`.alsa`: Implemented optional volume display/setting as in AlsaMixer
* :py:mod:`.pulseaudio`: Fixed bug that created zombies on a click event
* :py:mod:`.backlight`: Fixed bug preventing brightness increase
3.33 (2015-06-23)
+++++++++++++++++
* Errors can now be logged to ``~/.i3pystatus-``
- See :ref:`logging`
* Added new callback system
- See :ref:`callbacks`
* Added credentials storage
- See :ref:`credentials`
* Added :ref:`hints` to support special uses cases
* Added support for Pango markup
* Sending SIGUSR1 to i3pystatus refreshes the bar
- See :ref:`refresh`
* Modules are refreshed instantly after a callback was handled
* Fixed issue where i3bar would interpret plain-text with
"HTML-look-alike" characters in them as HTML/Pango
* New modules
- :py:mod:`.github`: Check Github for pending notifications.
- :py:mod:`.whosonlocation`: Change your whosonlocation.com status.
- :py:mod:`.openvpn`: Monitor OpenVPN connections. Currently only supports systems that use Systemd.
- :py:mod:`.net_speed`: Attempts to provide an estimation of internet speeds.
- :py:mod:`.makewatch`: Watches for make jobs and notifies when they are completed.
- :py:mod:`.dota2wins`: Displays the win/loss ratio of a given Dota account.
- :py:mod:`.dpms`: Shows and toggles status of DPMS which prevents screen from blanking.
- :py:mod:`.cpu_freq`: uses by default /proc/cpuinfo to determine the current cpu frequency
- :py:mod:`.updates`: Generic update checker. Currently supports apt-get, pacman and cower
- :py:mod:`.openstack_vms`: Displays the number of VMs in an openstack
cluster in ACTIVE and non-ACTIVE states.
* :py:mod:`.backlight`: add xbacklight support for changing brightness with mouse wheel
* :py:mod:`.battery`: added support for depleted batteries
* battery: added support for multiple batteries
* battery: added option to treat all batteries as one large battery (ALL)
* :py:mod:`.cpu_usage`: removed hard coded interval setting
* :py:mod:`.cpu_usage_bar`: fixed wrong default setting
* :py:mod:`.clock`: removed optional pytz dependency
* :py:mod:`.network`: cycle available interfaces on click
* network: centralized network modules
- Removed ``network_graph``
- Removed ``network_traffic``
- Removed ``wireless``
- All the features of these three modules are now found in network
* network: added total traffic in Mbytes formatters
* network: ``basiciw`` is only required if it is used (wireless)
* network: ``psutil`` is only required if it is used (traffic)
* network: scrolling changes displayed interface
* network: fixed bug that prevented color_up being shown if the user is not using network_traffic
* network: various other enhancements
* :py:mod:`.notmuch`: fixed sync issue with database
* :py:mod:`.now_playing`: added custom format and color when no player is running
* now_playing: differentiates between D-Bus errors and no players running
* now_playing: fixed D-Bus compatibility with players
* :py:mod:`.mail`: added capability to display unread messages per account individually
* :py:mod:`.mpd`: various enhancements and fixes
* :py:mod:`.pulseaudio`: detect default sink changes in pulseaudio
* :py:mod:`.reddit`: can open users mailbox now
* :py:mod:`.shell`: fixed module not stripping newlines
* :py:mod:`.spotify`: check for metadata on start
* :py:mod:`.temp`: alert temperatures
* :py:mod:`.weather`: removed pywapi dependency
* weather: add min_temp and max_temp formatters for daily min/max temperature
3.32 (2014-12-14)
+++++++++++++++++
* Added :py:mod:`.keyboard_locks` module
* Added :py:mod:`.pianobar` module
* Added :py:mod:`.uname` module
* :py:mod:`.cmus`: enhanced artist/title detection from filenames
* cmus: fixed issue when cmus is not running
* :py:mod:`.mpd`: added text_len and truncate_fields options to truncate long artist, album or song names
* :py:mod:`.network_traffic`: added hide_down and format_down options
* :py:mod:`.pomodoro`: added format option
* pomodoro: reset timer on left click
* :py:mod:`.pulseaudio`: fix rounding error of percentage volume
3.31 (2014-10-23)
+++++++++++++++++
* Unexpected exceptions are now displayed in the status bar
* Core: added mouse wheel handling for upcoming i3 version
* Fixed issues with internet-related modules
* New module mixin: ip3ystatus.core.color.ColorRangeModule
* Added :py:mod:`.cmus` module
* Added :py:mod:`.cpu_usage_graph` module
* Added :py:mod:`.network_graph` module
* Added :py:mod:`.network_traffic` module
* Added :py:mod:`.pomodoro` module
* Added :py:mod:`.uptime` module
* :py:mod:`.alsa`: mouse wheel changes volume
* :py:mod:`.battery`: Added no_text_full option
* :py:mod:`.cpu_usage`: Add multicore support
* :py:mod:`.cpu_usage_bar`: Add multicore support
* :py:mod:`.mail`: db_path option made optional
* :py:mod:`.mpd`: Play song on left click even if stopped
* :py:mod:`.network`: Add unknown_up setting
* :py:mod:`.parcel`: Document lxml dependency
* :py:mod:`.pulseaudio`: Added color_muted and color_unmuted options
* pulseaudio: Added step, bar_type, multi_colors, vertical_bar_width options
* pulseaudio: Scroll to change master volume, right click to (un)mute
3.30 (2014-08-04)
+++++++++++++++++
* Added :py:mod:`.bitcoin` module
* Added :py:mod:`.now_playing` module
* Added :py:mod:`.reddit` module
* Added :py:mod:`.shell` module
* Core: fixed custom statusline colors not working properly (see issue #74)
* :py:mod:`.alsa` and :py:mod:`.pulseaudio`: added optional
"formated_muted" audio is muted.
* :py:mod:`.battery`: add bar formatter, add not_present_text,
full_color, charging_color, not_present_color settings
* :py:mod:`.disk`: add color and round_size options
* :py:mod:`.maildir`: use os.listdir instead of ls
* :py:mod:`.mem`: add round_size option
* :py:mod:`.mpd`: add color setting
* mpd: add filename formatter
* mpd: next song on right click
* :py:mod:`.network` and wireless: support interfaces enslaved to a
bonding master
* network: detached_down is now True by default
* network: fixed some issues with interface up/down detection
* :py:mod:`.parcel`: added support for Itella (Finnish national postal
service) setting. If provided, it will be used instead of "format"
when the
* :py:mod:`.temp`: add file setting
* temp: fixed issue with Linux kernels 3.15 and newer
* temp: removed color_critical and high_factor options
* :py:mod:`.text`: add cmd_leftclick and cmd_rightclick options
* :py:mod:`.weather`: add colorize option
* :py:mod:`.wireless`: Add quality_bar formatter
3.29 (2014-04-29)
+++++++++++++++++
* :py:mod:`.network`: prefer non link-local v6 addresses
* :py:mod:`.mail`: Open email client and refresh email with mouse click
* :py:mod:`.disk`: Add display and critical limit
* :py:mod:`.battery`: fix errors if CURRENT_NOW is not present
* battery: add configurable colors
* :py:mod:`.load`: add configurable colors and limit
* :py:mod:`.parcel`: rewrote DHL tracker
* Add :py:mod:`.spotify` module
3.28 (2014-04-12)
+++++++++++++++++
* **If you're currently using the i3pystatus command to run your i3bar**:
Replace ``i3pystatus`` command in your i3 configuration with ``python ~/path/to/your/config.py``
* Do not name your script i3pystatus.py or it will break imports.
* New options for :py:mod:`.mem`
* Added :py:mod:`.cpu_usage`
* Improved error handling
* Removed ``i3pystatus`` binary
* :py:mod:`.pulseaudio:` changed context name to "i3pystatus_pulseaudio"
* Add maildir backend for mails
* Code changes
* Removed DHL tracker of parcel module, because it doesn't work anymore.
3.27 (2013-10-20)
+++++++++++++++++
* Add :py:mod:`.weather` module
* Add :py:mod:`.text` module
* :py:mod:`.pulseaudio`: Add muted/unmuted options
3.26 (2013-10-03)
+++++++++++++++++
* Add :py:mod:`.mem` module
3.24 (2013-08-04)
+++++++++++++++++
**This release introduced changes that may require manual changes to your
configuration file**
* Introduced TimeWrapper
* :py:mod:`.battery`: removed remaining\_* formatters in favor of
TimeWrapper, as it can not only reproduce all the variants removed,
but can do much more.
* :py:mod:`.mpd`: Uses TimeWrapper for song_length, song_elapsed
i3pystatus-3.35+git20191126.5a8eaf4/docs/conf.py 0000664 0000000 0000000 00000024600 13567273623 0020604 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
#
# i3pystatus documentation build configuration file, created by
# sphinx-quickstart on Mon Oct 14 17:41:37 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
# requires PyPI mock
import mock
MOCK_MODULES = [
"alsaaudio",
'circleci.api',
"netifaces", "psutil",
"lxml.html", "lxml.cssselect", "lxml",
"praw",
"gi", "gi.repository", "dbus.mainloop.glib", "dbus",
"GeoIP",
"pywapi", "basiciw",
"i3pystatus.pulseaudio.pulse",
"notmuch",
"requests",
"requests.exceptions",
"bs4",
"dota2py",
'deluge_client',
"novaclient",
"speedtest",
"pyzabbix",
"vk",
"google-api-python-client",
"dateutil",
"dateutil.rrule",
"httplib2",
"oauth2client",
"apiclient",
"googleapiclient",
"googleapiclient.errors",
"vlc",
"dateutil.tz",
"i3ipc",
"dateutil.parser",
"dateutil.relativedelta",
"xkbgroup",
"sensors",
"khal",
"khal.cli",
"khal.settings",
"requests.auth",
"requests.sessions",
"requests.packages",
"requests.packages.urllib3",
"requests.packages.urllib3.response",
'pypd',
'travispy',
"lxml.etree",
"requests.adapters",
"exchangelib",
"soco"
]
for mod_name in MOCK_MODULES:
sys.modules[mod_name] = mock.Mock()
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# 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',
'sphinx.ext.viewcode',
'module_docs',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'i3pystatus'
copyright = '2012-2017 i3pystatus developers. Free and open software under the MIT license'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
#version = ''
# The full version, including alpha/beta/rc tags.
#release = ''
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "tango"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- 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 = "haiku"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# 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']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'i3pystatusdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'i3pystatus.tex', 'i3pystatus Documentation',
'Author', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'i3pystatus', 'i3pystatus Documentation',
['Author'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'i3pystatus', 'i3pystatus Documentation',
'Author', 'i3pystatus', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = 'i3pystatus'
epub_author = 'Author'
epub_publisher = 'Author'
epub_copyright = '2013, Author'
# The language of the text. It defaults to the language option
# or en if the language is not set.
#epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
#epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#epub_identifier = ''
# A unique identification for the text.
#epub_uid = ''
# A tuple containing the cover image and cover page html template filenames.
#epub_cover = ()
# A sequence of (type, uri, title) tuples for the guide element of content.opf.
#epub_guide = ()
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_pre_files = []
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_post_files = []
# A list of files that should not be packed into the epub file.
#epub_exclude_files = []
# The depth of the table of contents in toc.ncx.
#epub_tocdepth = 3
# Allow duplicate toc entries.
#epub_tocdup = True
# Fix unsupported image types using the PIL.
#epub_fix_images = False
# Scale large images.
#epub_max_image_width = 0
# If 'no', URL addresses will not be shown.
#epub_show_urls = 'inline'
# If false, no index is generated.
#epub_use_index = True
def setup(app):
app.add_stylesheet('i3pystatus.css')
i3pystatus-3.35+git20191126.5a8eaf4/docs/configuration.rst 0000664 0000000 0000000 00000052766 13567273623 0022724 0 ustar 00root root 0000000 0000000 Configuration
=============
The configuration file is a normal Python script. The status bar is controlled by a central
:py:class:`.Status` object, which individual *modules* like a :py:mod:`.clock` or a :py:mod:`.battery`
monitor are added to with the ``register`` method.
A typical configuration file could look like this (note the additional
dependencies from :py:mod:`.network` and :py:mod:`.pulseaudio` in this
example):
.. code:: python
from i3pystatus import Status
status = Status()
# Displays clock like this:
# Tue 30 Jul 11:59:46 PM KW31
# ^-- calendar week
status.register("clock",
format="%a %-d %b %X KW%V",)
# Shows the average load of the last minute and the last 5 minutes
# (the default value for format is used)
status.register("load")
# Shows your CPU temperature, if you have a Intel CPU
status.register("temp",
format="{temp:.0f}°C",)
# The battery monitor has many formatting options, see README for details
# This would look like this, when discharging (or charging)
# ↓14.22W 56.15% [77.81%] 2h:41m
# And like this if full:
# =14.22W 100.0% [91.21%]
#
# This would also display a desktop notification (via D-Bus) if the percentage
# goes below 5 percent while discharging. The block will also color RED.
# If you don't have a desktop notification demon yet, take a look at dunst:
# http://www.knopwob.org/dunst/
status.register("battery",
format="{status}/{consumption:.2f}W {percentage:.2f}% [{percentage_design:.2f}%] {remaining:%E%hh:%Mm}",
alert=True,
alert_percentage=5,
status={
"DIS": "↓",
"CHR": "↑",
"FULL": "=",
},)
# This would look like this:
# Discharging 6h:51m
status.register("battery",
format="{status} {remaining:%E%hh:%Mm}",
alert=True,
alert_percentage=5,
status={
"DIS": "Discharging",
"CHR": "Charging",
"FULL": "Bat full",
},)
# Displays whether a DHCP client is running
status.register("runwatch",
name="DHCP",
path="/var/run/dhclient*.pid",)
# Shows the address and up/down state of eth0. If it is up the address is shown in
# green (the default value of color_up) and the CIDR-address is shown
# (i.e. 10.10.10.42/24).
# If it's down just the interface name (eth0) will be displayed in red
# (defaults of format_down and color_down)
#
# Note: the network module requires PyPI package netifaces
status.register("network",
interface="eth0",
format_up="{v4cidr}",)
# Note: requires both netifaces and basiciw (for essid and quality)
status.register("network",
interface="wlan0",
format_up="{essid} {quality:03.0f}%",)
# Shows disk usage of /
# Format:
# 42/128G [86G]
status.register("disk",
path="/",
format="{used}/{total}G [{avail}G]",)
# Shows pulseaudio default sink volume
#
# Note: requires libpulseaudio from PyPI
status.register("pulseaudio",
format="♪{volume}",)
# Shows mpd status
# Format:
# Cloud connected▶Reroute to Remain
status.register("mpd",
format="{title}{status}{album}",
status={
"pause": "▷",
"play": "▶",
"stop": "◾",
},)
status.run()
Also change your i3wm config to the following:
.. code:: ini
# i3bar
bar {
status_command python ~/.path/to/your/config/file.py
position top
workspace_buttons yes
}
.. note:: Don't name your config file ``i3pystatus.py``, as it would
make ``i3pystatus`` un-importable and lead to errors.
Another way to launch your configuration file is to use ``i3pystatus`` script
from installation:
.. code:: bash
i3pystatus -c ~/.path/to/your/config/file.py
If no arguments were provided, ``i3pystatus`` script works as an example of
``Clock`` module.
Formatting
----------
All modules let you specifiy the exact output formatting using a
`format string `_, which
gives you a great deal of flexibility.
If a module gives you a float, it probably has a ton of
uninteresting decimal places. Use ``{somefloat:.0f}`` to get the integer
value, ``{somefloat:0.2f}`` gives you two decimal places after the
decimal dot
.. _formatp:
formatp
~~~~~~~
Some modules use an extended format string syntax (the :py:mod:`.mpd` and
:py:mod:`.weather` modules, for example). Given the format string below the
output adapts itself to the available data.
::
[{artist}/{album}/]{title}{status}
Only if both the artist and album is known they're displayed. If only one or none
of them is known the entire group between the brackets is excluded.
"is known" is here defined as "value evaluating to True in Python", i.e. an empty
string or 0 (or 0.0) counts as "not known".
Inside a group always all format specifiers must evaluate to true (logical and).
You can nest groups. The inner group will only become part of the output if both
the outer group and the inner group are eligible for output.
.. _TimeWrapper:
TimeWrapper
~~~~~~~~~~~
Some modules that output times use :py:class:`.TimeWrapper` to format
these. TimeWrapper is a mere extension of the standard formatting
method.
The time format that should be used is specified using the format specifier, i.e.
with some_time being 3951 seconds a format string like ``{some_time:%h:%m:%s}``
would produce ``1:5:51``.
* ``%h``, ``%m`` and ``%s`` are the hours, minutes and seconds without
leading zeros (i.e. 0 to 59 for minutes and seconds)
* ``%H``, ``%M`` and ``%S`` are padded with a leading zero to two digits,
i.e. 00 to 59
* ``%l`` and ``%L`` produce hours non-padded and padded but only if hours
is not zero. If the hours are zero it produces an empty string.
* ``%%`` produces a literal %
* ``%E`` (only valid on beginning of the string) if the time is null,
don't format anything but rather produce an empty string. If the
time is non-null it is removed from the string.
* When the module in question also uses formatp, 0 seconds counts as
"not known".
* The formatted time is stripped, i.e. spaces on both ends of the
result are removed.
.. _logging:
Logging
-------
Errors do happen and to ease debugging i3pystatus includes a logging
facility. By default i3pystatus will log exceptions raised by modules
to files in your home directory named
``.i3pystatus-``. Some modules might log additional
information.
Setting a specific logfile
~~~~~~~~~~~~~~~~~~~~~~~~~~
When instantiating your ``Status`` object, the path to a log file can be
specified (it accepts environment variables). If this is done, then log messages will be sent to that file and not
to an ``.i3pystatus-`` file in your home directory. This is
useful in that it helps keep your home directory from becoming cluttered with
files containing errors.
.. code-block:: python
from i3pystatus import Status
status = Status(logfile='$HOME/var/i3pystatus.log')
Changing log format
~~~~~~~~~~~~~~~~~~~
.. versionadded:: 3.35
The ``logformat`` option can be useed to change the format of the log files,
using `LogRecord attributes`__.
.. code-block:: python
from i3pystatus import Status
status = Status(
logfile='/home/username/var/i3pystatus.log',
logformat='%(asctime)s %(levelname)s:',
)
.. __: https://docs.python.org/3/library/logging.html#logrecord-attributes
Log level
~~~~~~~~~
Every module has a ``log_level`` option which sets the *minimum*
severity required for an event to be logged.
The numeric values of logging levels are given in the following
table.
+--------------+---------------+
| Level | Numeric value |
+==============+===============+
| ``CRITICAL`` | 50 |
+--------------+---------------+
| ``ERROR`` | 40 |
+--------------+---------------+
| ``WARNING`` | 30 |
+--------------+---------------+
| ``INFO`` | 20 |
+--------------+---------------+
| ``DEBUG`` | 10 |
+--------------+---------------+
| ``NOTSET`` | 0 |
+--------------+---------------+
Exceptions raised by modules are of severity ``ERROR`` by default. The
default ``log_level`` in i3pystatus (some modules might redefine the
default, see the reference of the module in question) is 30
(``WARNING``).
.. _callbacks:
Callbacks
---------
Callbacks are used for click-events (merged into i3bar since i3 4.6,
mouse wheel events are merged since 4.8), that is, you click (or
scroll) on the output of a module in your i3bar and something
happens. What happens is defined by these settings for each module
individually:
- ``on_leftclick``
- ``on_doubleleftclick``
- ``on_rightclick``
- ``on_doublerightclick``
- ``on_upscroll``
- ``on_downscroll``
The global default action for all settings is ``None`` (do nothing),
but many modules define other defaults, which are documented in the
module reference.
.. note::
Each of these callbacks, when triggered, will call the module's ``run()``
function (typically only called each time the module's interval is
reached). If there are things in the ``run()`` function of your module
which you do not want to be executed every time a mouse event is triggered,
then consider using threading to perform the module update, and manually
sleep for the module's interval between updates. You can start the update
thread in the module's ``init()`` function. The ``run()`` function can then
either just update the module's displayed text, or simply do nothing (if
your update thread also handles updating the display text). See the
`weather module`_ for an example of this method.
.. _`weather module`: https://github.com/enkore/i3pystatus/blob/82fc9fb/i3pystatus/weather/__init__.py#L244-L265
The values you can assign to these four settings can be divided to following
three categories:
.. rubric:: Member callbacks
These callbacks are part of the module itself and usually do some simple module
related tasks (like changing volume when scrolling, etc.). All available
callbacks are (most likely not) documented in their respective module
documentation.
For example the module :py:class:`.ALSA` has callbacks named ``switch_mute``,
``increase_volume`` and ``decrease volume``. They are already assigned by
default but you can change them to your liking when registering the module.
.. code:: python
status.register("alsa",
on_leftclick = ["switch_mute"],
# or as a strings without the list
on_upscroll = "decrease_volume",
on_downscroll = "increase_volume",
# this will refresh any module by clicking on it
on_rightclick = "run",
)
Some callbacks also have additional parameters. Both ``increase_volume`` and
``decrease_volume`` have an optional parameter ``delta`` which determines the
amount of percent to add/subtract from the current volume.
.. code:: python
status.register("alsa",
# all additional items in the list are sent to the callback as arguments
on_upscroll = ["decrease_volume", 2],
on_downscroll = ["increase_volume", 2],
)
.. rubric:: Python callbacks
These refer to to any callable Python object (most likely a
function). To external Python callbacks that are not part of the
module the ``self`` parameter is not passed by default. This allows to
use many library functions with no additional wrapper.
If ``self`` is needed to access the calling module, the
:py:func:`.get_module` decorator can be used on the callback:
.. code:: python
from i3pystatus import get_module
# Note that the 'self' parameter is required and gives access to all
# variables of the module.
@get_module
def change_text(self):
self.output["full_text"] = "Clicked"
status.register("text",
text = "Initial text",
on_leftclick = [change_text],
# or
on_rightclick = change_text,
)
If the module your attaching the callback too is not a subclass of
:py:class:`.IntervalModule` you will need to invoke ``init()``.
using :py:class:`.Uname` as an example, the following code would suffice.
.. code:: python
from i3pystatus import get_module
@get_module
def sys_info(self):
if self.format == "{nodename}":
self.format = "{sysname} {release} on {machine}"
else:
self.format = "{nodename}"
self.init()
status.register("uname", format="{nodename}", on_rightclick=sys_info)
You can also create callbacks with parameters.
.. code:: python
from i3pystatus import get_module
@get_module
def change_text(self, text="Hello world!", color="#ffffff"):
self.output["full_text"] = text
self.output["color"] = color
status.register("text",
text = "Initial text",
color = "#00ff00",
on_leftclick = [change_text, "Clicked LMB", "#ff0000"],
on_rightclick = [change_text, "Clicked RMB"],
on_upscroll = change_text,
)
.. rubric:: External program callbacks
You can also use callbacks to execute external programs. Any string that does
not match any `member callback` is treated as an external command. If you want
to do anything more complex than executing a program with a few arguments,
consider creating an `python callback` or execute a script instead.
.. code:: python
status.register("text",
text = "Launcher?",
# open terminal window running htop
on_leftclick = "i3-sensible-terminal -e htop",
# open i3pystatus github page in firefox
on_rightclick = "firefox --new-window https://github.com/enkore/i3pystatus",
)
Most modules provide all the formatter data to program callbacks. The snippet below
demonstrates how this could be used, in this case XMessage will display a dialog box
showing verbose information about the network interface:
.. code:: python
status.register("network",
interface="eth0",
on_leftclick="ip addr show dev {interface} | xmessage -file -"
)
.. _hints:
Hints
-----
Hints are additional parameters used to customize output of a module.
They give you access to all attributes supported by `i3bar protocol
`_.
Hints are available as the ``hints`` setting in all modules and its
value should be a dictionary or ``None``. An attribute defined in
``hints`` will be applied only if the module output does not contain
attribute with the same name already.
Some possible uses for these attributes are:
* `min_width` and `align` can be used to set minimal width of output and
align the text if its width is shorter than `minimal_width`.
* `separator` and `separator_block_width` can be used to remove the
vertical bar that is separating modules.
* `background` can be used to set an alternative background color for the
module. supports RGBA if your i3bar version does.
* `markup` can be set to `"none"` or `"pango"`.
`Pango markup
`_
provides additional formatting options for drawing rainbows and other
fancy stuff.
.. note:: Pango markup requires that i3bar is configured to use `Pango `_, too. It can't work with X core fonts.
Here is an example with the :py:mod:`.network` module.
Pango markup is used to keep the ESSID green at all times while the
recieved/sent part is changing color depending on the amount of traffic.
.. code:: python
status.register("network",
interface = "wlp2s0",
hints = {"markup": "pango"},
format_up = "{essid} {bytes_recv:6.1f}KiB {bytes_sent:5.1f}KiB",
format_down = "",
dynamic_color = True,
start_color = "#00FF00",
end_color = "#FF0000",
color_down = "#FF0000",
upper_limit = 800.0,
)
Or you can use pango to customize the color of ``status`` setting in
:py:mod:`.now_playing` and :py:mod:`.mpd` modules.
.. code:: python
...
hints = {"markup": "pango"},
status = {
"play": "▶",
"pause": "▶",
"stop": "◾",
},
...
Or make two modules look like one.
.. code:: python
status.register("text",
text = "shmentarianism is a pretty long word.")
status.register("text",
hints = {"separator": False, "separator_block_width": 0},
text = "Antidisestabli",
color="#FF0000")
.. _refresh:
Refreshing the bar
------------------
The whole bar can be refreshed by sending SIGUSR1 signal to i3pystatus
process. This feature is not available in chained mode
(:py:class:`.Status` was created with ``standalone=False`` parameter
and gets it's input from ``i3status`` or a similar program).
To find the PID of the i3pystatus process look for the ``status_command`` you
use in your i3 config file.
If your `bar` section of i3 config looks like this
.. code::
bar {
status_command python ~/.config/i3/pystatus.py
}
then you can refresh the bar by using the following command:
.. code:: bash
pkill -SIGUSR1 -f "python /home/user/.config/i3/pystatus.py"
Note that the path must be expanded if using '~'.
.. _internet:
Internet Connectivity
---------------------
Module methods that ``@require(internet)`` won't be run unless a test TCP
connection is successful. By default, this is made to Google's DNS server, but
you can customize the host and port. See :py:class:`.internet`.
If you are behind a gateway that redirects web traffic to an authorization page
and blocks other traffic, the DNS check will return a false positive. This is
often encountered in open WiFi networks. In these cases it is helpful to try a
service that is not traditionally required for web browsing:
.. code:: python
from i3pystatus import Status
status = Status(check_internet=("whois.arin.net", 43))
.. code:: python
from i3pystatus import Status
status = Status(check_internet=("github.com", 22))
.. _credentials:
Credentials
-----------
For modules which require credentials, i3pystatus supports credential
management using the keyring_ module from PyPI.
.. important::
Many distributions have keyring_ pre-packaged, available as
``python-keyring``. Unless you have KWallet_ or SecretService_ available,
you will also most likely need to install keyrings.alt_, which contains
additional keyring backends for use by the keyring_ module.
Both i3pystatus and ``i3pystatus-setting-util`` will abort with a
RuntimeError_ if keyring_ isinstalled but a usable keyring backend is not
present, so it is a good idea to install both if you plan to use a module
which supports credential handling.
To store credentials in a keyring, use the ``i3pystatus-setting-util`` script
installed along i3pystatus.
.. note::
``i3pystatus-setting-util`` will store credentials using the default
keyring backend. The method for determining which backend is the default
can be found :ref:`below `. If, for some reason,
it is necessary to use a keyring other than the default, then you will need
to override the default in your keyringrc.cfg_ for
``i3pystatus-setting-util`` to successfully use it.
Once you have successfully set up credentials, you can add the module to your
config file without specifying the credentials in the registration, e.g.:
.. code:: python
# Use the default keyring to retrieve credentials
status.register('github')
i3pystatus will locate and set the credentials during the module loading
process. Currently supported credentials are ``password``, ``email`` and
``username``.
.. _default-keyring-backend:
.. note::
To determine which backend is the default on your system, run the
following:
.. code-block:: bash
python -c 'import keyring; print(keyring.get_keyring())'
If this command returns a ``keyring.backends.fail.Keyring`` object, none of
the keyrings supported out-of-the box by the keyring_ module are available,
and you will need to install the keyrings.alt_ Python module. keyrings.alt_
provides an encrypted keyring which will be seen as the default if both
keyrings.alt_ and keyring_ are installed, and none of the keyrings
supported by keyring_ are present:
.. code-block:: bash
$ python -c 'import keyring; print(keyring.get_keyring())'
If the keyring backend you used to store credentials using
``i3pystatus-setting-util`` is not the default, then you can change which
keyring backend i3pystatus will use in one of two ways:
#. Override the default in your keyringrc.cfg_
#. Import and instantiate a keyring backend class, and pass it as the
``keyring_backend`` parameter when registering the module:
.. code:: python
# Requires the keyrings.alt package
from keyrings.alt.file import PlaintextKeyring
status.register('github', keyring_backend=PlaintextKeyring())
.. _KWallet: http://www.kde.org/
.. _SecretService: https://specifications.freedesktop.org/secret-service/re01.html
.. _RuntimeError: https://docs.python.org/3/library/exceptions.html#RuntimeError
.. _keyring: https://pypi.python.org/pypi/keyring
.. _keyrings.alt: https://pypi.python.org/pypi/keyrings.alt
.. _keyringrc.cfg: http://pythonhosted.org/keyring/#customize-your-keyring-by-config-file
i3pystatus-3.35+git20191126.5a8eaf4/docs/i3pystatus.core.rst 0000664 0000000 0000000 00000003051 13567273623 0023113 0 ustar 00root root 0000000 0000000 core Package
============
:mod:`core` Package
-------------------
.. automodule:: i3pystatus.core
:members:
:undoc-members:
:show-inheritance:
:mod:`color` Module
-------------------
.. automodule:: i3pystatus.core.color
:members:
:undoc-members:
:show-inheritance:
:mod:`command` Module
---------------------
.. automodule:: i3pystatus.core.command
:members:
:undoc-members:
:show-inheritance:
:mod:`desktop` Module
---------------------
.. automodule:: i3pystatus.core.desktop
:members:
:undoc-members:
:show-inheritance:
:mod:`exceptions` Module
------------------------
.. automodule:: i3pystatus.core.exceptions
:members:
:undoc-members:
:show-inheritance:
:mod:`imputil` Module
---------------------
.. automodule:: i3pystatus.core.imputil
:members:
:undoc-members:
:show-inheritance:
:mod:`io` Module
----------------
.. automodule:: i3pystatus.core.io
:members:
:undoc-members:
:show-inheritance:
:mod:`modules` Module
---------------------
.. automodule:: i3pystatus.core.modules
:members:
:undoc-members:
:show-inheritance:
:mod:`settings` Module
----------------------
.. automodule:: i3pystatus.core.settings
:members:
:undoc-members:
:show-inheritance:
:mod:`threading` Module
-----------------------
.. automodule:: i3pystatus.core.threading
:members:
:undoc-members:
:show-inheritance:
:mod:`util` Module
------------------
.. automodule:: i3pystatus.core.util
:members:
:undoc-members:
:show-inheritance:
i3pystatus-3.35+git20191126.5a8eaf4/docs/i3pystatus.rst 0000664 0000000 0000000 00000005677 13567273623 0022204 0 ustar 00root root 0000000 0000000 Module reference
================
.. Don't list *every* module here, e.g. cpu-usage suffices, because the other
variants are listed below that one.
.. rubric:: Module overview:
:System: `clock`_ - `cpu_freq`_ - `cpu_usage`_ - `disk`_ - `keyboard_locks`_ - `load`_ - `mem`_ -
`swap`_ - `uname`_ - `uptime`_ - `xkblayout`_
:Audio: `alsa`_ - `pulseaudio`_
:Hardware: `backlight`_ - `battery`_ - `temp`_
:Network: `net_speed`_ - `network`_ - `online`_ - `openstack_vms`_ - `openvpn`_
:Music: `cmus`_ - `moc`_ - `mpd`_ - `now_playing`_ - `pianobar`_ - `spotify`_
:Websites: `bitcoin`_ - `dota2wins`_ - `github`_ - `modsde`_ - `parcel`_ - `reddit`_ - `weather`_ -
`whosonlocation`_
:Other: `anybar`_ - `mail`_ - `pomodoro`_ - `pyload`_ - `text`_ - `updates`_
:Advanced: `file`_ - `regex`_ - `makewatch`_ - `runwatch`_ - `shell`_
.. autogen:: i3pystatus Module
.. rubric:: Module list:
.. _mailbackends:
Mail Backends
-------------
The generic mail module can be configured to use multiple mail backends. Here is an
example configuration for the MaildirMail backend:
.. code:: python
from i3pystatus.mail import maildir
status.register("mail",
backends=[maildir.MaildirMail(
directory="/home/name/Mail/inbox")
],
format="P {unread}",
log_level=20,
hide_if_null=False, )
.. autogen:: i3pystatus.mail SettingsBase
.. nothin'
.. _scorebackends:
Score Backends
--------------
.. autogen:: i3pystatus.scores SettingsBase
.. nothin'
.. _updatebackends:
Update Backends
---------------
.. autogen:: i3pystatus.updates SettingsBase
.. nothin'
.. _weatherbackends:
Weather Backends
----------------
.. autogen:: i3pystatus.weather SettingsBase
.. nothin'
.. calendarbackends:
Calendar Backends
-----------------
Generic calendar interface. Requires the PyPI package ``colour``.
.. rubric:: Available formatters
* {title} - the title or summary of the event
* {remaining_time} - how long until this event is due
Additional formatters may be provided by the backend, consult their documentation for details.
.. rubric:: Settings
* {update_interval} - how often (in seconds) the calendar backend should be called to update events
* {dynamic_color} - when set, the color shifts as the event approaches
* {urgent_blink} - when set, urgent is toggled every second when within urgent_seconds of the event
* {urgent_seconds} - how many seconds before the event to begin blinking
* {skip_recurring} - when set, recurring events are skipped
Here is an example of configuring the calendar module to use the ``Lightning`` backend:
.. code:: python
status.register("calendar",
format="{title} {remaining}",
update_interval=10,
urgent_blink=True,
backend=Lightning(database_path=path, days=2))
.. autogen:: i3pystatus.calendar SettingsBase
.. nothin' i3pystatus-3.35+git20191126.5a8eaf4/docs/index.rst 0000664 0000000 0000000 00000000444 13567273623 0021146 0 ustar 00root root 0000000 0000000
Welcome to the i3pystatus documentation!
========================================
Contents:
.. toctree::
:maxdepth: 4
configuration
i3pystatus
changelog
module
i3pystatus.core
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
i3pystatus-3.35+git20191126.5a8eaf4/docs/make.bat 0000664 0000000 0000000 00000015065 13567273623 0020717 0 ustar 00root root 0000000 0000000 @ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^` where ^ is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 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
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\i3pystatus.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\i3pystatus.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end
i3pystatus-3.35+git20191126.5a8eaf4/docs/module.rst 0000664 0000000 0000000 00000007350 13567273623 0021327 0 ustar 00root root 0000000 0000000
Creating modules
================
Creating new modules ("things that display something") to contribute
to i3pystatus is reasonably easy. If the module you want to write
updates it's info periodically, like checking for a network link or
displaying the status of some service, then we have prepared common
tools for this which make this even easier:
- Common base classes: :py:class:`.Module` for everything and
:py:class:`.IntervalModule` specifically for the aforementioned
usecase of updating stuff periodically.
the :py:class:`.Module` class inherits a `logger` attribute and as such
all logging should be implemented via `self.logger.` rather then
initializing a new logger in the module.
- Settings (already built into above classes) allow you to easily
specify user-modifiable attributes of your class for configuration.
See :py:class:`.SettingsBase` for details.
- For modules that require credentials, it is recommended to add a
keyring_backend setting to allow users to specify their own backends
for retrieving sensitive credentials.
Required settings and default values are also handled.
Check out i3pystatus' source code for plenty of (`simple
`_)
examples on how to build modules.
The settings system is built to ease documentation. If you specify
two-tuples like ``("setting", "description")`` then Sphinx will
automatically generate a nice table listing each option, it's default
value and description.
The docstring of your module class is automatically used as the
reStructuredText description for your module in the README file.
.. seealso::
:py:class:`.SettingsBase` for a detailed description of the settings system
Handling Dependencies
---------------------
To make it as easy as possible to use i3pystatus we explicitly
document all dependencies in the docstring of a module.
The wording usually used goes like this:
.. code:: rst
Requires the PyPI package `colour`
To allow automatic generation of the docs without having all
requirements of every module installed mocks are used. To make this
work simply add all modules of dependencies (so no standard library modules
or modules provided by i3pystatus) you import to the ``MOCK_MODULES``
list in ``docs/conf.py``. This needs to be the actual name of the imported
module, so for example if you have ``from somepkg.mod import AClass``,
you need to add ``somepkg.mod`` to the list.
Testing changes
---------------
i3pystatus uses continuous integration (CI) techniques, which means in
our case that every patch and every pull request is tested
automatically. While Travis is used for automatic building of GitHub
pull requests, it is not the authoritative CI system (which is `Der Golem
`_) for the main repository.
The ``ci-build.sh`` script needs to run successfully for a patch to be
accepted. It can be run on your machine, too, so you don't need to
wait for the often slow Travis build to complete. It does not require
any special privileges, except write access to the ``ci-build``
directory (a different build directory can be specified as the first
parameter to ``ci-build.sh``).
The script tests the following things:
1. PEP8 compliance of the entire codebase, *excluding* errors of too
long lines (error code E501). Line lengths of about 120 characters
are acceptable.
2. That ``setup.py`` installs i3pystatus and related binaries (into a
location below the build directory)
3. Unit tests pass, they are tested against the installed version from
2.). A unit test log in JUnit format is generated in the build
directory (``testlog.xml``).
4. Sphinx docs build without errors or warnings. The HTML docs are
generated in the ``docs`` directory in the build directory.
i3pystatus-3.35+git20191126.5a8eaf4/docs/module_docs.py 0000664 0000000 0000000 00000014246 13567273623 0022161 0 ustar 00root root 0000000 0000000
import pkgutil
import importlib
import sphinx.application
from docutils.parsers.rst import Directive
from docutils.nodes import paragraph
from docutils.statemachine import StringList
import i3pystatus.core.settings
import i3pystatus.core.modules
from i3pystatus.core.imputil import ClassFinder
from i3pystatus.core.color import ColorRangeModule
IGNORE_MODULES = ("__main__", "core", "tools")
def is_module(obj):
return (isinstance(obj, type)
and issubclass(obj, i3pystatus.core.settings.SettingsBase)
and not obj.__module__.startswith("i3pystatus.core."))
def fail_on_missing_dependency_hints(obj, lines):
# We can automatically check in some cases if we forgot something
if issubclass(obj, ColorRangeModule):
if all("colour" not in line for line in lines):
raise ValueError(">>> Module <{}> uses ColorRangeModule and should document it <<<\n"
"> Requires the PyPI package ``colour``".format(obj.__name__))
def check_settings_consistency(obj, settings):
errs = []
for setting in settings:
if not setting.required and setting.default is setting.sentinel:
errs.append("<" + setting.name + ">")
if errs:
raise ValueError(">>> Module <{}> has non-required setting(s) {} with no default! <<<\n"
.format(obj.__name__, ", ".join(errs)))
def process_docstring(app, what, name, obj, options, lines):
class Setting:
doc = ""
required = False
default = sentinel = object()
empty = object()
def __init__(self, cls, setting):
if isinstance(setting, tuple):
self.name = setting[0]
self.doc = setting[1]
else:
self.name = setting
if self.name in cls.required:
self.required = True
elif hasattr(cls, self.name):
default = getattr(cls, self.name)
if isinstance(default, str) and not len(default)\
or default is None:
default = self.empty
self.default = default
def __str__(self):
attrs = []
if self.required:
attrs.append("required")
if self.default not in [self.sentinel, self.empty]:
attrs.append("default: ``{default}``".format(default=self.default))
if self.default is self.empty:
attrs.append("default: *empty*")
formatted = "* **{name}** {attrsf} {doc}".format(
name=self.name,
doc="– " + self.doc if self.doc else "",
attrsf=" ({attrs})".format(attrs=", ".join(attrs)) if attrs else "")
return formatted
if is_module(obj) and obj.settings:
fail_on_missing_dependency_hints(obj, lines)
if issubclass(obj, i3pystatus.core.modules.Module):
mod = obj.__module__
if mod.startswith("i3pystatus."):
mod = mod[len("i3pystatus."):]
lines[0:0] = [
".. raw:: html",
"",
"
" +
"Module name: " + mod + " " +
"(class " + name + ")" +
"
",
"",
]
lines.append(".. rubric:: Settings")
lines.append("")
settings = [Setting(obj, setting) for setting in obj.settings]
lines += map(str, settings)
check_settings_consistency(obj, settings)
lines.append("")
def process_signature(app, what, name, obj, options, signature, return_annotation):
if is_module(obj):
return ("", return_annotation)
def get_modules(path, package):
modules = []
for finder, modname, is_package in pkgutil.iter_modules(path):
if modname not in IGNORE_MODULES:
modules.append(get_module(finder, modname, package))
return modules
def get_module(finder, modname, package):
fullname = "{package}.{modname}".format(package=package, modname=modname)
return (modname, finder.find_loader(fullname)[0].load_module(fullname))
def get_all(module_path, modname, basecls):
mods = []
finder = ClassFinder(basecls)
for name, module in get_modules(module_path, modname):
classes = finder.get_matching_classes(module)
found = []
for cls in classes:
if cls.__name__ not in found:
found.append(cls.__name__)
mods.append((module.__name__, cls.__name__))
return sorted(mods, key=lambda module: module[0])
def generate_automodules(path, name, basecls):
modules = get_all(path, name, basecls)
contents = []
for mod in modules:
contents.append("* :py:mod:`~{}`".format(mod[0]))
contents.append("")
for mod in modules:
contents.append(".. _{}:\n".format(mod[0].split(".")[-1]))
contents.append(".. automodule:: {}".format(mod[0]))
contents.append(" :members: {}\n".format(mod[1]))
return contents
class AutogenDirective(Directive):
required_arguments = 2
has_content = True
def run(self):
# Raise an error if the directive does not have contents.
self.assert_has_content()
modname = self.arguments[0]
modpath = importlib.import_module(modname).__path__
basecls = getattr(i3pystatus.core.modules, self.arguments[1])
contents = []
for e in self.content:
contents.append(e)
contents.append("")
contents.extend(generate_automodules(modpath, modname, basecls))
node = paragraph()
self.state.nested_parse(StringList(contents), 0, node)
return [node]
def setup(app: sphinx.application.Sphinx):
app.add_directive("autogen", AutogenDirective)
app.connect("autodoc-process-docstring", process_docstring)
app.connect("autodoc-process-signature", process_signature)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/ 0000775 0000000 0000000 00000000000 13567273623 0020503 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/__init__.py 0000664 0000000 0000000 00000002370 13567273623 0022616 0 ustar 00root root 0000000 0000000 from pkgutil import extend_path
from i3pystatus.core import Status
from i3pystatus.core.modules import Module, IntervalModule
from i3pystatus.core.settings import SettingsBase
from i3pystatus.core.util import formatp, get_module
import argparse
import imp
import logging
import os
__path__ = extend_path(__path__, __name__)
__all__ = [
"Status",
"Module", "IntervalModule",
"SettingsBase",
"formatp",
"get_module",
]
logpath = os.path.join(os.path.expanduser("~"), ".i3pystatus-%s" % os.getpid())
handler = logging.FileHandler(logpath, delay=True)
logger = logging.getLogger("i3pystatus")
logger.addHandler(handler)
logger.setLevel(logging.CRITICAL)
def clock_example():
from i3pystatus.clock import Clock
status = Status()
status.register(Clock())
status.run()
def main():
parser = argparse.ArgumentParser(description='''
run i3pystatus configuration file. Starts i3pystatus clock example if no arguments were provided
''')
parser.add_argument('-c', '--config', help='path to configuration file', default=None, required=False)
args = parser.parse_args()
if args.config:
module_name = "i3pystatus-config"
imp.load_source(module_name, args.config)
else:
clock_example()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/abc_radio.py 0000664 0000000 0000000 00000024447 13567273623 0022773 0 ustar 00root root 0000000 0000000 import logging
import shutil
import threading
import os
import xml.etree.ElementTree as etree
from datetime import datetime
import requests
import vlc
from dateutil import parser
from dateutil.tz import tzutc
from i3pystatus import IntervalModule
from i3pystatus.core.desktop import DesktopNotification
from i3pystatus.core.util import internet, require
class State:
PLAYING = 1
PAUSED = 2
STOPPED = 3
class ABCRadio(IntervalModule):
"""
Streams ABC Australia radio - https://radio.abc.net.au/. Currently uses VLC to do the
actual streaming.
Requires the PyPI packages `python-vlc`, `python-dateutil` and `requests`. Also requires VLC
- https://www.videolan.org/vlc/index.html
.. rubric:: Available formatters
* `{station}` — Current station
* `{title}` — Title of current show
* `{url}` — Show's URL
* `{remaining}` — Time left for current show
* `{player_state}` — Unicode icons representing play, pause and stop
"""
settings = (
("format", "format string for when the player is inactive"),
("format_playing", "format string for when the player is playing"),
("target_stations", "list of station ids to select from. Station ids can be obtained "
"from the following XML - http://www.abc.net.au/radio/data/stations_apps_v3.xml. "
"If the list is empty, all stations will be accessible."),
)
format = "{station} {title} {player_state}"
format_playing = "{station} {title} {remaining} {player_state}"
on_leftclick = 'toggle_play'
on_upscroll = ['cycle_stations', 1]
on_downscroll = ['cycle_stations', -1]
on_doubleleftclick = 'display_notification'
interval = 1
# Destroy the player after this many seconds of inactivity
PLAYER_LIFETIME = 5
# Do not suspend the player when i3bar is hidden.
keep_alive = True
show_info = {}
player = None
station_info = None
station_id = None
stations = None
prev_title = None
prev_station = None
target_stations = []
end = None
start = None
destroy_timer = None
cycle_lock = threading.Lock()
player_icons = {
State.PAUSED: "▷",
State.PLAYING: "▶",
State.STOPPED: "◾",
}
def init(self):
self.station_info = ABCStationInfo()
@require(internet)
def run(self):
if self.station_id is None:
self.stations = self.station_info.get_stations()
# Select the first station in the list
self.cycle_stations(1)
if self.end and self.end <= datetime.now(tz=tzutc()):
self.update_show_info()
format_dict = self.show_info.copy()
format_dict['player_state'] = self.get_player_state()
format_dict['remaining'] = self.get_remaining()
format_template = self.format_playing if self.player else self.format
self.output = {
"full_text": format_template.format(**format_dict)
}
def update_show_info(self):
log.debug("Updating: show_info - %s" % datetime.now())
self.show_info = dict.fromkeys(
('title', 'url', 'start', 'end', 'duration', 'stream', 'remaining', 'station', 'description', 'title',
'short_synopsis', 'url'), '')
self.show_info.update(self.stations[self.station_id])
self.show_info.update(self.station_info.currently_playing(self.station_id))
# Show a notification when the show changes if the user is actively listening.
should_show = self.prev_station == self.show_info['station'] and self.prev_title != self.show_info[
'title'] and self.player
if should_show:
self.display_notification()
self.prev_title = self.show_info['title']
self.prev_station = self.show_info['station']
self.end = self.show_info['end'] if self.show_info['end'] else None
self.start = self.show_info['start'] if self.show_info['start'] else None
def get_player_state(self):
if self.player:
return self.player_icons[self.player.player_state]
else:
return self.player_icons[State.STOPPED]
def get_remaining(self):
if self.end and self.end > datetime.now(tz=tzutc()):
return str(self.end - datetime.now(tz=tzutc())).split(".")[0]
return ''
def cycle_stations(self, increment=1):
with self.cycle_lock:
target_array = self.target_stations if len(self.target_stations) > 0 else list(self.stations.keys())
if self.station_id in target_array:
next_index = (target_array.index(self.station_id) + increment) % len(target_array)
self.station_id = target_array[next_index]
else:
self.station_id = target_array[0]
log.debug("Cycle to: {}".format(self.station_id))
if self.player:
current_state = self.player.player_state
self.player.stop()
else:
current_state = State.STOPPED
self.update_show_info()
if self.player:
self.player.load_stream(self.show_info['stream'])
self.player.set_state(current_state)
def display_notification(self):
if self.show_info:
station, title, synopsis = self.show_info['station'], self.show_info['title'], self.show_info[
'short_synopsis']
title = "{} - {}".format(station, title)
def get_image():
image_link = self.show_info.get('image_link', None)
if image_link:
try:
image_path = "/tmp/{}.icon".format(station)
if not os.path.isfile(image_path):
response = requests.get(image_link, stream=True)
with open(image_path, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
return image_path
except:
pass
DesktopNotification(title=title, body=synopsis, icon=get_image()).display()
log.info("Displayed notification")
def toggle_play(self):
if not self.player:
self.init_player()
if self.player.is_playing():
self.player.pause()
self.destroy_timer = threading.Timer(self.PLAYER_LIFETIME, self.destroy)
self.destroy_timer.start()
else:
if self.destroy_timer:
self.destroy_timer.cancel()
self.destroy_timer = None
self.player.play()
self.run()
def init_player(self):
if self.show_info:
self.player = VLCPlayer()
log.info("Created player: {}".format(id(self.player)))
if not self.player.stream_loaded():
log.info("Loading stream: {}".format(self.show_info['stream']))
self.player.load_stream(self.show_info['stream'])
if not self.player.is_alive():
self.player.start()
def destroy(self):
log.debug("Destroying player: {}".format(id(self.player)))
if self.player:
self.player.destroy()
self.player = None
class ABCStationInfo:
PLAYING_URL = "https://program.abcradio.net.au/api/v1/programitems/{}/live.json?include=now"
def currently_playing(self, station_id):
station_info = self._get(self.PLAYING_URL.format(station_id)).json()
try:
return dict(
title=station_info['now']['program']['title'],
url=station_info['now']['primary_webpage']['url'],
start=parser.parse(station_info['now']['live'][0]['start']),
end=parser.parse(station_info['now']['live'][0]['end']),
duration=station_info['now']['live'][0]['duration_seconds'],
short_synopsis=station_info['now']['short_synopsis'],
stream=sorted(station_info['now']['live'][0]['outlets'][0]['audio_streams'], key=lambda x: x['type'])[0]['url']
)
except (KeyError, IndexError):
return {}
def get_stations(self):
stations = dict()
station_xml = etree.fromstring(self._get('http://www.abc.net.au/radio/data/stations_apps_v3.xml').content)
for element in station_xml:
attrib = element.attrib
if attrib["showInAndroidApp"] == 'true':
stations[attrib['id']] = dict(
id=attrib['id'],
station=attrib['name'],
description=attrib.get('description', None),
link=attrib.get('linkUrl', None),
image_link=attrib.get('WEBimageUrl', None),
stream=attrib.get('hlsStreamUrl', None),
)
return stations
def _get(self, url):
result = requests.get(url=url)
if result.status_code not in range(200, 300):
result.raise_for_status()
return result
log = logging.getLogger(__name__)
class VLCPlayer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.idle = threading.Event()
self.die = threading.Event()
self.instance = vlc.Instance()
self.player_state = State.STOPPED
self.player = self.instance.media_player_new()
def run(self):
states = {
State.STOPPED: self.player.stop,
State.PLAYING: self.player.play,
State.PAUSED: self.player.pause,
}
while not self.die.is_set():
self.idle.wait()
states[self.player_state]()
self.idle.clear()
def load_stream(self, url):
self.player.set_media(self.instance.media_new(url))
def stream_loaded(self):
return self.player.get_media() is not None
def play(self):
self.set_state(State.PLAYING)
def pause(self):
self.set_state(State.PAUSED)
def stop(self):
self.set_state(State.STOPPED)
def destroy(self):
self.die.set()
self.idle.set()
self.player.stop()
self.player.release()
def set_state(self, state):
log.info("{} -> {}".format(self.player_state, state))
self.player_state = state
self.idle.set()
def is_playing(self):
return self.player.is_playing()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/alsa.py 0000664 0000000 0000000 00000012073 13567273623 0022000 0 ustar 00root root 0000000 0000000 from alsaaudio import Mixer, ALSAAudioError
from math import exp, log, log10, ceil, floor
from i3pystatus import IntervalModule
class ALSA(IntervalModule):
"""
Shows volume of ALSA mixer. You can also use this for inputs, btw.
Requires pyalsaaudio
.. rubric:: Available formatters
* `{volume}` — the current volume in percent
* `{muted}` — the value of one of the `muted` or `unmuted` settings
* `{card}` — the associated soundcard
* `{mixer}` — the associated ALSA mixer
"""
interval = 1
settings = (
"format",
("format_muted", "optional format string to use when muted"),
("mixer", "ALSA mixer"),
("mixer_id", "ALSA mixer id"),
("card", "ALSA sound card"),
("increment", "integer percentage of max volume to in/decrement volume on mousewheel"),
"muted", "unmuted",
"color_muted", "color",
"channel",
("map_volume", "volume display/setting as in AlsaMixer. increment option is ignored then.")
)
muted = "M"
unmuted = ""
color_muted = "#AAAAAA"
color = "#FFFFFF"
format = "♪: {volume}"
format_muted = None
mixer = "Master"
mixer_id = 0
card = -1
channel = 0
increment = 5
map_volume = False
alsamixer = None
has_mute = True
on_upscroll = "increase_volume"
on_downscroll = "decrease_volume"
on_leftclick = "switch_mute"
on_rightclick = on_leftclick
def init(self):
self.create_mixer()
try:
self.alsamixer.getmute()
except ALSAAudioError:
self.has_mute = False
self.fdict = {
"card": self.alsamixer.cardname(),
"mixer": self.mixer,
}
self.dbRng = self.alsamixer.getrange()
self.dbMin = self.dbRng[0]
self.dbMax = self.dbRng[1]
def create_mixer(self):
self.alsamixer = Mixer(
control=self.mixer, id=self.mixer_id, cardindex=self.card)
def run(self):
self.create_mixer()
muted = False
if self.has_mute:
muted = self.alsamixer.getmute()[self.channel] == 1
self.fdict["volume"] = self.get_cur_volume()
self.fdict["muted"] = self.muted if muted else self.unmuted
self.fdict["db"] = self.get_db()
if muted and self.format_muted is not None:
output_format = self.format_muted
else:
output_format = self.format
self.data = self.fdict
self.output = {
"full_text": output_format.format(**self.fdict),
"color": self.color_muted if muted else self.color,
}
def switch_mute(self):
if self.has_mute:
muted = self.alsamixer.getmute()[self.channel]
self.alsamixer.setmute(not muted)
def get_cur_volume(self):
if self.map_volume:
dbCur = self.get_db() * 100.0
dbMin = self.dbMin * 100.0
dbMax = self.dbMax * 100.0
dbCur_norm = self.exp10((dbCur - dbMax) / 6000.0)
dbMin_norm = self.exp10((dbMin - dbMax) / 6000.0)
vol = (dbCur_norm - dbMin_norm) / (1 - dbMin_norm)
vol = int(round(vol * 100, 0))
return vol
else:
return self.alsamixer.getvolume()[self.channel]
def get_new_volume(self, direction):
if direction == "inc":
volume = (self.fdict["volume"] + 1) / 100
elif direction == "dec":
volume = (self.fdict["volume"] - 1) / 100
dbMin = self.dbMin * 100
dbMax = self.dbMax * 100
dbMin_norm = self.exp10((dbMin - dbMax) / 6000.0)
vol = volume * (1 - dbMin_norm) + dbMin_norm
if direction == "inc":
dbNew = min(self.dbMax, ceil(((6000.0 * log10(vol)) + dbMax) / 100))
elif direction == "dec":
dbNew = max(self.dbMin, floor(((6000.0 * log10(vol)) + dbMax) / 100))
volNew = int(round(self.map_db(dbNew, self.dbMin, self.dbMax, 0, 100), 0))
return volNew
def increase_volume(self, delta=None):
if self.map_volume:
vol = self.get_new_volume("inc")
self.alsamixer.setvolume(vol)
else:
vol = self.alsamixer.getvolume()[self.channel]
self.alsamixer.setvolume(min(100, vol + (delta if delta else self.increment)))
def decrease_volume(self, delta=None):
if self.map_volume:
vol = self.get_new_volume("dec")
self.alsamixer.setvolume(vol)
else:
vol = self.alsamixer.getvolume()[self.channel]
self.alsamixer.setvolume(max(0, vol - (delta if delta else self.increment)))
def get_db(self):
db = (((self.dbMax - self.dbMin) / 100) * self.alsamixer.getvolume()[self.channel]) + self.dbMin
db = int(round(db, 0))
return db
def map_db(self, value, dbMin, dbMax, volMin, volMax):
dbRange = dbMax - dbMin
volRange = volMax - volMin
volScaled = float(value - dbMin) / float(dbRange)
return volMin + (volScaled * volRange)
def exp10(self, x):
return exp(x * log(10))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/amdgpu.py 0000664 0000000 0000000 00000004664 13567273623 0022344 0 ustar 00root root 0000000 0000000 import os
from i3pystatus import IntervalModule
class Amdgpu(IntervalModule):
"""
Shows information about gpu's using the amdgpu driver
.. rubric :: Available formatters
* `{temp}`
* `{sclk}` - Gpu clock speed
* `{mclk}` - Memory clock speed
* `{fan_speed}` - Fan speed
* `{gpu_usage}` - Gpu Usage percent
"""
settings = (
'format',
'color',
('card', '[1, 2, ...] card to read (options are in /sys/class/drm/)')
)
card = 0
color = None
format = '{temp} {mclk} {sclk}'
def init(self):
self.info_gatherers = []
self.dev_path = '/sys/class/drm/card{}/device/'.format(self.card)
self.detect_hwmon()
if 'sclk' in self.format:
self.info_gatherers.append(self.get_sclk)
if 'mclk' in self.format:
self.info_gatherers.append(self.get_mclk)
if 'temp' in self.format:
self.info_gatherers.append(self.get_temp)
if 'fan_speed' in self.format:
self.info_gatherers.append(self.get_fan_speed)
if 'gpu_usage' in self.format:
self.info_gatherers.append(self.get_gpu_usage)
def detect_hwmon(self):
hwmon_base = self.dev_path + 'hwmon/'
self.hwmon_path = hwmon_base + os.listdir(hwmon_base)[0] + '/'
def run(self):
self.data = dict()
for gatherer in self.info_gatherers:
gatherer()
self.output = {
'full_text': self.format.format(**self.data)
}
if self.color:
self.output['color'] = self.color
@staticmethod
def parse_clk_reading(reading):
for l in reading.splitlines():
if l.endswith('*'):
return l.split(' ')[1]
def get_mclk(self):
with open(self.dev_path + 'pp_dpm_mclk') as f:
self.data['mclk'] = self.parse_clk_reading(f.read())
def get_sclk(self):
with open(self.dev_path + 'pp_dpm_sclk') as f:
self.data['sclk'] = self.parse_clk_reading(f.read())
def get_temp(self):
with open(self.hwmon_path + 'temp1_input') as f:
self.data['temp'] = float(f.read()) / 1000
def get_fan_speed(self):
with open(self.hwmon_path + 'fan1_input') as f:
self.data['fan_speed'] = f.read().strip()
def get_gpu_usage(self):
with open(self.dev_path + 'gpu_busy_percent') as f:
self.data['gpu_usage'] = f.read().strip()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/anybar.py 0000664 0000000 0000000 00000003475 13567273623 0022342 0 ustar 00root root 0000000 0000000 import threading
import socket
from i3pystatus import IntervalModule
class AnyBar(IntervalModule):
"""
This module shows dot with given color in your panel.
What color means is up to you. When to change color is also up to you.
It's a port of https://github.com/tonsky/AnyBar to i3pystatus.
Color can be changed by sending text to UDP port.
Check the original repo how to do it.
"""
colors = {
"black": "#444444", # 4C4C4C
"black_alt": "#FFFFFF",
"blue": "#4A90E2",
"cyan": "#27F2CB",
"exclamation": "#DE504C", # vary
"green": "#80EB0C",
"orange": "#FF9F00",
"purple": "#9013FE",
"question": "#4C4C4C", # vary
"question_alt": "#FFFFFF",
"red": "#CF0700",
"white": "#4C4C4C", # border
"white_alt": "#FFFFFF",
"yellow": "#FFEC00",
}
color = '#444444'
port = 1738
interval = 1
settings = (
("port", "UDP port to listen"),
("color", "initial color"),
)
def main_loop(self):
""" Mainloop blocks so we thread it."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
port = int(getattr(self, 'port', 1738))
sock.bind(('127.0.0.1', port))
while True:
data, addr = sock.recvfrom(512)
color = data.decode().strip()
self.color = self.colors.get(color, color)
def init(self):
try:
t = threading.Thread(target=self.main_loop)
t.daemon = True
t.start()
except Exception as e:
self.output = {
"full_text": "Error creating new thread!",
"color": "#AE2525"
}
def run(self):
self.output = {
"full_text": "●",
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/backlight.py 0000664 0000000 0000000 00000006055 13567273623 0023013 0 ustar 00root root 0000000 0000000 from i3pystatus.file import File
from i3pystatus import Module
from i3pystatus.core.command import run_through_shell
import glob
import shutil
class Backlight(File):
"""
Screen backlight info
- (Optional) requires `xbacklight` to change the backlight brightness with the scollwheel.
.. rubric:: Available formatters
* `{brightness}` — current brightness relative to max_brightness
* `{max_brightness}` — maximum brightness value
* `{percentage}` — current brightness in percent
"""
settings = (
("format", "format string, formatters: brightness, max_brightness, percentage"),
("format_no_backlight", "format string when no backlight file available"),
("backlight",
"backlight, see `/sys/class/backlight/`. Supports glob expansion, i.e. `*` matches anything. "
"If it matches more than one filename, selects the first one in alphabetical order"),
"color",
)
required = ()
backlight = "*"
format = "{brightness}/{max_brightness}"
format_no_backlight = "No backlight"
base_path = "/sys/class/backlight/{backlight}/"
components = {
"brightness": (int, "brightness"),
"max_brightness": (int, "max_brightness"),
}
transforms = {
"percentage": lambda cdict: round((cdict["brightness"] / cdict["max_brightness"]) * 100),
}
on_upscroll = "lighter"
on_downscroll = "darker"
def init(self):
self.base_path = self.base_path.format(backlight=self.backlight)
backlight_entries = sorted(glob.glob(self.base_path))
if len(backlight_entries) == 0:
self.run = self.run_no_backlight
super().init()
return
self.base_path = backlight_entries[0]
self.has_xbacklight = shutil.which("xbacklight") is not None
# xbacklight expects a percentage as parameter. Calculate the percentage
# for one step (if smaller xbacklight doesn't increases the brightness)
if self.has_xbacklight:
parsefunc = self.components['max_brightness'][0]
maxbfile = self.components['max_brightness'][1]
with open(self.base_path + maxbfile, "r") as f:
max_steps = parsefunc(f.read().strip())
if max_steps:
self.step_size = 100 // max_steps + 1
else:
self.step_size = 5 # default?
super().init()
def run_no_backlight(self):
cdict = {
"brightness": -1,
"max_brightness": -1,
"percentage": -1
}
format = self.format_no_backlight
if not format:
format = self.format
self.data = cdict
self.output = {
"full_text": format.format(**cdict),
"color": self.color
}
def lighter(self):
if self.has_xbacklight:
run_through_shell(["xbacklight", "-inc", str(self.step_size)])
def darker(self):
if self.has_xbacklight:
run_through_shell(["xbacklight", "-dec", str(self.step_size)])
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/battery.py 0000664 0000000 0000000 00000035602 13567273623 0022535 0 ustar 00root root 0000000 0000000 import bisect
import configparser
import os
import re
from i3pystatus import IntervalModule, formatp
from i3pystatus.core.command import run_through_shell
from i3pystatus.core.desktop import DesktopNotification
from i3pystatus.core.util import lchop, TimeWrapper, make_bar, make_glyph, make_vertical_bar
class UEventParser(configparser.ConfigParser):
@staticmethod
def parse_file(file):
parser = UEventParser()
with open(file, "r") as file:
parser.read_string(file.read())
return dict(parser.items("id10t"))
def __init__(self):
super().__init__(default_section="id10t")
def optionxform(self, key):
return lchop(key, "POWER_SUPPLY_")
def read_string(self, string):
super().read_string("[id10t]\n" + string)
class Battery:
@staticmethod
def create(from_file):
battery_info = UEventParser.parse_file(from_file)
if "POWER_NOW" in battery_info:
return BatteryEnergy(battery_info)
else:
return BatteryCharge(battery_info)
def __init__(self, battery_info):
self.battery_info = battery_info
self.normalize_micro()
def normalize_micro(self):
for key, micro_value in self.battery_info.items():
if re.match(r"(VOLTAGE|CHARGE|CURRENT|POWER|ENERGY)_(NOW|FULL|MIN)(_DESIGN)?", key):
self.battery_info[key] = float(micro_value) / 1000000.0
def percentage(self, design=False):
return self._percentage("_DESIGN" if design else "") * 100
def status(self):
if self.consumption() is None:
return self.battery_info["STATUS"]
elif self.consumption() > 0.1 and self.percentage() < 99.9:
return "Discharging" if self.battery_info["STATUS"] == "Discharging" else "Charging"
elif self.consumption() == 0 and self.percentage() == 0.00:
return "Depleted"
else:
return "Full"
def consumption(self, val):
return val if val > 0.1 else 0
class BatteryCharge(Battery):
def __init__(self, bi):
bi["CHARGE_FULL"] = bi["CHARGE_FULL_DESIGN"] if bi["CHARGE_NOW"] > bi["CHARGE_FULL"] else bi["CHARGE_FULL"]
super().__init__(bi)
def consumption(self):
if "VOLTAGE_NOW" in self.battery_info and "CURRENT_NOW" in self.battery_info:
return super().consumption(self.battery_info["VOLTAGE_NOW"] * abs(self.battery_info["CURRENT_NOW"])) # V * A = W
else:
return None
def _percentage(self, design):
return self.battery_info["CHARGE_NOW"] / self.battery_info["CHARGE_FULL" + design]
def wh_remaining(self):
return self.battery_info['CHARGE_NOW'] * self.battery_info['VOLTAGE_NOW']
def wh_total(self):
return self.battery_info['CHARGE_FULL'] * self.battery_info['VOLTAGE_NOW']
def wh_depleted(self):
return (self.battery_info['CHARGE_FULL'] - self.battery_info['CHARGE_NOW']) * self.battery_info['VOLTAGE_NOW']
def remaining(self):
if self.status() == "Discharging":
if "CHARGE_NOW" in self.battery_info and "CURRENT_NOW" in self.battery_info:
# Ah / A = h * 60 min = min
return self.battery_info["CHARGE_NOW"] / self.battery_info["CURRENT_NOW"] * 60
else:
return -1
else:
return (self.battery_info["CHARGE_FULL"] - self.battery_info["CHARGE_NOW"]) / self.battery_info[
"CURRENT_NOW"] * 60
class BatteryEnergy(Battery):
def consumption(self):
return super().consumption(self.battery_info["POWER_NOW"])
def _percentage(self, design):
return self.battery_info["ENERGY_NOW"] / self.battery_info["ENERGY_FULL" + design]
def wh_remaining(self):
return self.battery_info['ENERGY_NOW']
def wh_total(self):
return self.battery_info['ENERGY_FULL']
def wh_depleted(self):
return self.battery_info['ENERGY_FULL'] - self.battery_info['ENERGY_NOW']
def remaining(self):
if self.status() == "Discharging":
# Wh / W = h * 60 min = min
return self.battery_info["ENERGY_NOW"] / self.battery_info["POWER_NOW"] * 60
else:
return (self.battery_info["ENERGY_FULL"] - self.battery_info["ENERGY_NOW"]) / self.battery_info[
"POWER_NOW"] * 60
class BatteryChecker(IntervalModule):
"""
This class uses the /sys/class/power_supply/…/uevent interface to check for the
battery status.
Setting ``battery_ident`` to ``ALL`` will summarise all available batteries
and aggregate the % as well as the time remaining on the charge. This is
helpful when the machine has more than one battery available.
.. rubric:: Available formatters
* `{remaining}` — remaining time for charging or discharging, uses TimeWrapper formatting, default format is `%E%h:%M`
* `{percentage}` — battery percentage relative to the last full value
* `{percentage_design}` — absolute battery charge percentage
* `{consumption (Watts)}` — current power flowing into/out of the battery
* `{status}`
* `{no_of_batteries}` — The number of batteries included
* `{battery_ident}` — the same as the setting
* `{bar}` —bar displaying the relative percentage graphically
* `{bar_design}` —bar displaying the absolute percentage graphically
* `{glyph}` — A single character or string (selected from 'glyphs') representing the current battery percentage
This module supports the :ref:`formatp ` extended string format
syntax. By setting the ``FULL`` status to an empty string, and including
brackets around the ``{status}`` formatter, the text within the brackets
will be hidden when the battery is full, as can be seen in the below
example:
.. code-block:: python
from i3pystatus import Status
status = Status()
status.register(
'battery',
interval=5,
format='{battery_ident}: [{status} ]{percentage_design:.2f}%',
alert=True,
alert_percentage=15,
status={
'DPL': 'DPL',
'CHR': 'CHR',
'DIS': 'DIS',
'FULL': '',
}
)
# status.register(
# 'battery',
# format='{status} {percentage:.0f}%',
# levels={
# 25: "<=25",
# 50: "<=50",
# 75: "<=75",
# },
# )
status.run()
"""
settings = (
("battery_ident", "The name of your battery, usually BAT0 or BAT1"),
"format",
("not_present_text", "Text displayed if the battery is not present. No formatters are available"),
("alert", "Display a libnotify-notification on low battery"),
("critical_level_command", "Runs a shell command in the case of a critical power state"),
"critical_level_percentage",
"alert_percentage",
"alert_timeout",
("alert_format_title", "The title of the notification, all formatters can be used"),
("alert_format_body", "The body text of the notification, all formatters can be used"),
("path", "Override the default-generated path and specify the full path for a single battery"),
("base_path", "Override the default base path for searching for batteries"),
("battery_prefix", "Override the default battery prefix"),
("status", "A dictionary mapping ('DPL', 'DIS', 'CHR', 'FULL') to alternative names"),
("levels", "A dictionary mapping percentages of charge levels to corresponding names."),
("color", "The text color"),
("full_color", "The full color"),
("charging_color", "The charging color"),
("critical_color", "The critical color"),
("not_present_color", "The not present color."),
("not_present_text",
"The text to display when the battery is not present. Provides {battery_ident} as formatting option"),
("no_text_full", "Don't display text when battery is full - 100%"),
("glyphs", "Arbitrarily long string of characters (or array of strings) to represent battery charge percentage"),
)
battery_ident = "ALL"
format = "{status} {remaining}"
status = {
"DPL": "DPL",
"CHR": "CHR",
"DIS": "DIS",
"FULL": "FULL",
}
levels = None
not_present_text = "Battery {battery_ident} not present"
alert = False
critical_level_command = ""
critical_level_percentage = 1
alert_percentage = 10
alert_timeout = -1
alert_format_title = "Low battery"
alert_format_body = "Battery {battery_ident} has only {percentage:.2f}% ({remaining:%E%hh:%Mm}) remaining!"
color = "#ffffff"
full_color = "#00ff00"
charging_color = "#00ff00"
critical_color = "#ff0000"
not_present_color = "#ffffff"
no_text_full = False
glyphs = "▁▂▃▄▅▆▇█"
battery_prefix = 'BAT'
base_path = '/sys/class/power_supply'
path = None
paths = []
notification = None
def percentage(self, batteries, design=False):
total_now = [battery.wh_remaining() for battery in batteries]
total_full = [battery.wh_total() for battery in batteries]
return sum(total_now) / sum(total_full) * 100
def consumption(self, batteries):
consumption = 0
for battery in batteries:
if battery.consumption() is not None:
consumption += battery.consumption()
return consumption
def abs_consumption(self, batteries):
abs_consumption = 0
for battery in batteries:
if battery.consumption() is None:
continue
if battery.status() == 'Discharging':
abs_consumption -= battery.consumption()
elif battery.status() == 'Charging':
abs_consumption += battery.consumption()
return abs_consumption
def battery_status(self, batteries):
abs_consumption = self.abs_consumption(batteries)
if abs_consumption > 0:
return 'Charging'
elif abs_consumption < 0:
return 'Discharging'
else:
return batteries[-1].status()
def remaining(self, batteries):
wh_depleted = 0
wh_remaining = 0
abs_consumption = self.abs_consumption(batteries)
for battery in batteries:
wh_remaining += battery.wh_remaining()
wh_depleted += battery.wh_depleted()
if abs_consumption == 0:
return 0
elif abs_consumption > 0:
return wh_depleted / self.consumption(batteries) * 60
elif abs_consumption < 0:
return wh_remaining / self.consumption(batteries) * 60
def init(self):
if not self.paths or (self.path and self.path not in self.paths):
bat_dir = self.base_path
if os.path.exists(bat_dir) and not self.path:
_, dirs, _ = next(os.walk(bat_dir))
all_bats = [x for x in dirs if x.startswith(self.battery_prefix)]
for bat in all_bats:
self.paths.append(os.path.join(bat_dir, bat, 'uevent'))
if self.path:
self.paths = [self.path]
def run(self):
urgent = False
color = self.color
batteries = []
for path in self.paths:
if self.battery_ident == 'ALL' or path.find(self.battery_ident) >= 0:
try:
batteries.append(Battery.create(path))
except FileNotFoundError:
pass
if not batteries:
format_dict = {'battery_ident': self.battery_ident}
self.output = {
"full_text": formatp(self.not_present_text, **format_dict),
"color": self.not_present_color,
}
return
if self.no_text_full:
if self.battery_status(batteries) == "Full":
self.output = {
"full_text": ""
}
return
fdict = {
"battery_ident": self.battery_ident,
"no_of_batteries": len(batteries),
"percentage": self.percentage(batteries),
"percentage_design": self.percentage(batteries, design=True),
"consumption": self.consumption(batteries),
"remaining": TimeWrapper(0, "%E%h:%M"),
"glyph": make_glyph(self.percentage(batteries), self.glyphs),
"bar": make_bar(self.percentage(batteries)),
"bar_design": make_bar(self.percentage(batteries, design=True)),
"vertical_bar": make_vertical_bar(self.percentage(batteries)),
"vertical_bar_design": make_vertical_bar(self.percentage(batteries, design=True)),
}
status = self.battery_status(batteries)
if status in ["Charging", "Discharging"]:
remaining = self.remaining(batteries)
fdict["remaining"] = TimeWrapper(remaining * 60, "%E%h:%M")
if status == "Discharging":
fdict["status"] = "DIS"
if self.percentage(batteries) <= self.alert_percentage:
urgent = True
color = self.critical_color
else:
fdict["status"] = "CHR"
color = self.charging_color
elif status == 'Depleted':
fdict["status"] = "DPL"
color = self.critical_color
else:
fdict["status"] = "FULL"
color = self.full_color
if self.critical_level_command and fdict["status"] == "DIS" and fdict["percentage"] <= self.critical_level_percentage:
run_through_shell(self.critical_level_command, enable_shell=True)
if self.alert and fdict["status"] == "DIS" and fdict["percentage"] <= self.alert_percentage:
title, body = formatp(self.alert_format_title, **fdict), formatp(self.alert_format_body, **fdict)
if self.notification is None:
self.notification = DesktopNotification(
title=title,
body=body,
icon="battery-caution",
urgency=2,
timeout=self.alert_timeout,
)
self.notification.display()
else:
self.notification.update(title=title,
body=body)
if self.levels and fdict['status'] == 'DIS':
self.levels.setdefault(0, self.status.get('DPL', 'DPL'))
self.levels.setdefault(100, self.status.get('FULL', 'FULL'))
keys = sorted(self.levels.keys())
index = bisect.bisect_left(keys, int(fdict['percentage']))
fdict["status"] = self.levels[keys[index]]
else:
fdict["status"] = self.status[fdict["status"]]
self.data = fdict
self.output = {
"full_text": formatp(self.format, **fdict),
"instance": self.battery_ident,
"urgent": urgent,
"color": color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/bitcoin.py 0000664 0000000 0000000 00000014321 13567273623 0022505 0 ustar 00root root 0000000 0000000 import urllib.request
import json
from datetime import datetime
from i3pystatus import IntervalModule
from i3pystatus.core.util import internet, require, user_open
import locale
import threading
from contextlib import contextmanager
LOCALE_LOCK = threading.Lock()
@contextmanager
def setlocale(name):
# To deal with locales only in this module and keep it thread save
with LOCALE_LOCK:
saved = locale.setlocale(locale.LC_ALL)
try:
yield locale.setlocale(locale.LC_ALL, name)
finally:
locale.setlocale(locale.LC_ALL, saved)
class Bitcoin(IntervalModule):
"""
This module fetches and displays current Bitcoin market prices and
optionally monitors transactions to and from a list of user-specified
wallet addresses. Market data is pulled from the BitcoinAverage Price
Index API and it is possible to specify
the exchange to be monitored.
Transaction data is pulled from blockchain.info
.
.. rubric:: Available formatters
* {last_price}
* {ask_price}
* {bid_price}
* {daily_average}
* {volume}
* {volume_thousand}
* {volume_percent}
* {age}
* {status}
* {last_tx_type}
* {last_tx_addr}
* {last_tx_value}
* {balance_btc}
* {balance_fiat}
* {symbol}
"""
settings = (
("format", "Format string used for output."),
("currency", "Base fiat currency used for pricing."),
("wallet_addresses", "List of wallet address(es) to monitor."),
("color", "Standard color"),
("exchange", "Get ticker from a custom exchange instead"),
("colorize", "Enable color change on price increase/decrease"),
("color_up", "Color for price increases"),
("color_down", "Color for price decreases"),
("interval", "Update interval."),
("symbol", "Symbol for bitcoin sign"),
"status"
)
format = "{symbol} {status}{last_price}"
currency = "USD"
exchange = None
symbol = "฿"
wallet_addresses = ""
color = "#FFFFFF"
colorize = False
color_up = "#00FF00"
color_down = "#FF0000"
interval = 600
status = {
"price_up": "▲",
"price_down": "▼",
}
on_leftclick = "electrum"
on_rightclick = ["open_something", "https://bitcoinaverage.com/"]
_price_prev = 0
def _get_age(self, bitcoinaverage_timestamp):
with setlocale('C'): # Deal with locales (months name differ)
# Assume format is always utc, to avoid import pytz
diff = datetime.utcnow() - \
datetime.fromtimestamp(bitcoinaverage_timestamp)
return int(diff.total_seconds())
def _query_api(self, api_url):
url = "{}BTC{}".format(api_url, self.currency.upper())
response = urllib.request.urlopen(url).read().decode("utf-8")
return json.loads(response)
def _fetch_price_data(self):
if self.exchange is None:
api_url = "https://apiv2.bitcoinaverage.com/indices/global/ticker/"
return self._query_api(api_url)
else:
api_url = "https://api.bitcoinaverage.com/exchanges/"
ret = self._query_api(api_url)
exchange = ret[self.exchange.lower()]
# Adapt values to global ticker format
exchange['ask'] = exchange['rates']['ask']
exchange['bid'] = exchange['rates']['bid']
exchange['last'] = exchange['rates']['last']
exchange['24h_avg'] = None
exchange['timestamp'] = ret['timestamp']
return exchange
def _fetch_blockchain_data(self):
api = "https://blockchain.info/multiaddr?active="
addresses = "|".join(self.wallet_addresses)
url = "{}{}".format(api, addresses)
return json.loads(urllib.request.urlopen(url).read().decode("utf-8"))
@require(internet)
def run(self):
price_data = self._fetch_price_data()
fdict = {
"symbol": self.symbol,
"daily_average": price_data["averages"]["day"],
"ask_price": price_data["ask"],
"bid_price": price_data["bid"],
"last_price": price_data["last"],
"volume": price_data["volume"],
"volume_thousand": float(price_data["volume"]) / 1000,
"volume_percent": price_data["volume_percent"],
"age": self._get_age(price_data['timestamp'])
}
if self._price_prev and fdict["last_price"] > self._price_prev:
color = self.color_up
fdict["status"] = self.status["price_up"]
elif self._price_prev and fdict["last_price"] < self._price_prev:
color = self.color_down
fdict["status"] = self.status["price_down"]
else:
color = self.color
fdict["status"] = ""
self._price_prev = fdict["last_price"]
if not self.colorize:
color = self.color
if self.wallet_addresses:
blockchain_data = self._fetch_blockchain_data()
wallet_data = blockchain_data["wallet"]
balance_btc = wallet_data["final_balance"] / 100000000
fdict["balance_btc"] = round(balance_btc, 2)
balance_fiat = fdict["balance_btc"] * fdict["last_price"]
fdict["balance_fiat"] = round(balance_fiat, 2)
fdict["total_sent"] = wallet_data["total_sent"]
fdict["total_received"] = wallet_data["total_received"]
fdict["transactions"] = wallet_data["n_tx"]
if fdict["transactions"]:
last_tx = blockchain_data["txs"][0]
fdict["last_tx_addr"] = last_tx["out"][0]["addr"]
fdict["last_tx_value"] = last_tx["out"][0]["value"] / 100000000
if fdict["last_tx_addr"] in self.wallet_addresses:
fdict["last_tx_type"] = "recv"
else:
fdict["last_tx_type"] = "sent"
self.data = fdict
self.output = {
"full_text": self.format.format(**fdict),
"color": color,
}
def open_something(self, url_or_command):
"""
Wrapper function, to pass the arguments to user_open
"""
user_open(url_or_command)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/calendar/ 0000775 0000000 0000000 00000000000 13567273623 0022254 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/calendar/__init__.py 0000664 0000000 0000000 00000020153 13567273623 0024366 0 ustar 00root root 0000000 0000000 import inspect
import re
import threading
from abc import abstractmethod
from datetime import datetime, timedelta
from i3pystatus import IntervalModule, formatp, SettingsBase
from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.desktop import DesktopNotification
humanize_imported = False
try:
import humanize
humanize_imported = True
except ImportError:
pass
def strip_microseconds(delta):
return delta - timedelta(microseconds=delta.microseconds)
def formatter(func):
""" Decorator to mark a CalendarEvent method as a formatter. """
func.formatter = True
return func
class CalendarEvent:
"""
Simple class representing an Event. The attributes title, start, end and recurring are required as
these will be used for the formatters. The id attribute is used to uniquely identify the event.
If a backend wishes to provide extra formatters to the user, this can be done by adding additional
methods and decorating them with the @formatter decorator. See the LightningCalendarEvent from the
lightning module for an example of this.
"""
# Unique identifier for this event
id = None
# The title of this event
title = None
# Datetime object representing when this event begins
start = None
# Datetime object representing when this event ends
end = None
# Whether or not this event is a recurring event
recurring = False
def formatters(self):
"""
Build a dictionary containing all those key/value pairs that will be exposed to the user via formatters.
"""
event_dict = dict(
title=self.title,
remaining=self.time_remaining,
humanize_remaining=self.humanize_time_remaining,
)
def is_formatter(x):
return inspect.ismethod(x) and hasattr(x, 'formatter') and getattr(x, 'formatter')
for method_name, method in inspect.getmembers(self, is_formatter):
event_dict[method_name] = method()
return event_dict
@property
def time_remaining(self):
return strip_microseconds(self.start - datetime.now(tz=self.start.tzinfo))
@property
def humanize_time_remaining(self):
if humanize_imported:
return humanize.naturaltime(datetime.now(tz=self.start.tzinfo) - self.start)
def __str__(self):
return "{}(title='{}', start={}, end={}, recurring={})" \
.format(type(self).__name__,
self.title,
repr(self.start),
repr(self.end),
self.recurring)
class CalendarBackend(SettingsBase):
"""
Base class for calendar backend. Subclasses should implement update and populate the events list.
Optionally, subclasses can override on_click to perform actions on the current event when clicked.
"""
def init(self):
self.events = []
@abstractmethod
def update(self):
""" Subclasses should implement this method and populate the events list with CalendarEvents."""
def on_click(self, event):
""" Override this method to do more interesting things with the event. """
DesktopNotification(
title=event.title,
body="{} until {}!".format(event.time_remaining, event.title),
icon='dialog-information',
urgency=1,
timeout=0,
).display()
def __iter__(self):
return iter(self.events)
def __len__(self):
return len(self.events)
class Calendar(IntervalModule, ColorRangeModule):
"""
Generic calendar module. Requires the PyPI package ``colour``.
.. rubric:: Available formatters
* {title} - the title or summary of the event
* {remaining_time} - how long until this event is due
* {humanize_remaining} - how long until this event is due in human readable format
Additional formatters may be provided by the backend, consult their documentation for details.
.. note:: Optionally requires `humanize` to display time in human readable format.
"""
settings = (
('format', 'Format string to display in the bar'),
('backend', 'Backend to use for collecting calendar events'),
('skip_recurring', 'Whether or not to skip recurring events'),
('skip_all_day', 'Whether or not to skip all day events'),
('skip_regex', 'Skip events with titles that match this regex'),
('update_interval', "How often in seconds to call the backend's update method"),
('urgent_seconds', "When within this many seconds of the event, set the urgent flag"),
('urgent_blink', 'Whether or not to blink when within urgent_seconds of the event'),
('dynamic_color', 'Whether or not to change color as the event approaches'),
'color'
)
required = ('backend',)
skip_recurring = False
skip_all_day = False
skip_regex = None
interval = 1
backend = None
update_interval = 600
dynamic_color = True
urgent_seconds = 300
urgent_blink = False
color = None
current_event = None
urgent_acknowledged = False
format = "{title} - {remaining}"
on_rightclick = 'handle_click'
on_leftclick = 'acknowledge'
def init(self):
if 'humanize_remaining' in self.format and not humanize_imported:
raise ImportError('Missing humanize module')
self.condition = threading.Condition()
self.thread = threading.Thread(target=self.update_thread, daemon=True)
self.thread.start()
self.colors = self.get_hex_color_range(self.end_color, self.start_color, self.urgent_seconds * 2)
def update_thread(self):
self.refresh_events()
while True:
with self.condition:
self.condition.wait(self.update_interval)
self.refresh_events()
def refresh_events(self):
self.backend.update()
def valid_event(ev):
if self.skip_all_day and not isinstance(ev.start, datetime):
return False
if self.skip_recurring and ev.recurring:
return False
if self.skip_regex and re.search(self.skip_regex, ev.title) is not None:
return False
elif ev.time_remaining < timedelta(seconds=0):
return False
return True
for event in self.backend:
if valid_event(event):
if self.current_event and self.current_event.id != event.id:
self.urgent_acknowledged = False
self.current_event = event
return
self.current_event = None
def run(self):
if self.current_event and self.current_event.time_remaining > timedelta(seconds=0):
color = None
if self.color is not None:
color = self.color
elif self.dynamic_color:
color = self.get_color()
self.output = {
"full_text": formatp(self.format, **self.current_event.formatters()),
"color": color,
"urgent": self.is_urgent()
}
else:
self.output = {}
def handle_click(self):
if self.current_event:
self.backend.on_click(self.current_event)
def get_color(self):
if self.current_event.time_remaining.days > 0:
color = self.colors[-1]
else:
p = self.percentage(self.current_event.time_remaining.seconds, self.urgent_seconds)
color = self.get_gradient(p, self.colors)
return color
def is_urgent(self):
"""
Determine whether or not to set the urgent flag. If urgent_blink is set, toggles urgent flag
on and off every second.
"""
if not self.current_event:
return False
now = datetime.now(tz=self.current_event.start.tzinfo)
alert_time = now + timedelta(seconds=self.urgent_seconds)
urgent = alert_time > self.current_event.start
if urgent and self.urgent_blink:
urgent = now.second % 2 == 0 and not self.urgent_acknowledged
return urgent
def acknowledge(self):
self.urgent_acknowledged = not self.urgent_acknowledged
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/calendar/google.py 0000664 0000000 0000000 00000011322 13567273623 0024101 0 ustar 00root root 0000000 0000000 import datetime
from datetime import timezone
import httplib2
from oauth2client import file as file_, client, tools
import pytz
from googleapiclient import discovery
from dateutil import parser
from googleapiclient.errors import HttpError
from i3pystatus.calendar import CalendarBackend, CalendarEvent, formatter
from i3pystatus.core.util import user_open, require, internet
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
class GoogleCalendarEvent(CalendarEvent):
def __init__(self, google_event):
self.id = google_event['id']
self.title = google_event['summary']
self.start = self._parse_date(google_event['start'])
self.end = self._parse_date(google_event['end'])
self.recurring = 'recurringEventId' in google_event
self._link = google_event['htmlLink']
self._status = google_event['status']
self._kind = google_event['kind']
@formatter
def htmlLink(self):
return self._link
@formatter
def status(self):
return self._status
@formatter
def kind(self):
return self._kind
def _parse_date(self, date_section):
if 'dateTime' not in date_section:
result = parser.parse(date_section['date'])
else:
result = parser.parse(date_section['dateTime'])
return result.replace(tzinfo=timezone.utc).astimezone(tz=None)
class Google(CalendarBackend):
"""
Calendar backend for interacting with Google Calendar.
Requires the Google Calendar API package - https://developers.google.com/google-apps/calendar/quickstart/python.
Additionally requires the `colour`, `httplib2`, `oauth2client`, `pytz`, `google-api-python-client` and `dateutil` modules.
The first time this module is ran, you will need to specify the location of `credentials.json` (as credentials_json)
acquired from: https://developers.google.com/google-apps/calendar/quickstart/python
this will open a browser window for auth, and save a token to `credential_path`. you will need to reload i3poystatus
afterwards
If you already have a token `credentials_json` is not required (though highly recomended incase your token gets broken)
.. rubric:: Available formatters
* `{kind}` — type of event
* `{status}` — eg, confirmed
* `{htmlLink}` — link to the calendar event
"""
settings = (
('credential_path', 'Path to save credentials to (auto generated the first time this module is ran)'),
('credentials_json', 'path to credentials.json (generated by google)'),
('days', 'Only show events between now and this many days in the future'),
)
required = ('credential_path',)
credentials_json = None
days = 7
def init(self):
self.service = None
self.events = []
@require(internet)
def update(self):
if self.service is None:
self.connect_service()
self.refresh_events()
def on_click(self, event):
user_open(event.htmlLink())
def connect_service(self):
self.logger.debug("Connecting Service..")
store = file_.Storage(self.credential_path)
self.credentials = store.get()
# if the module is being ran for the first time, open up the browser to authenticate
if not self.credentials or self.credentials.invalid:
flow = client.flow_from_clientsecrets(self.credentials_json, SCOPES)
self.credentials = tools.run_flow(flow, store)
self.service = discovery.build('calendar', 'v3', http=self.credentials.authorize(httplib2.Http()))
def refresh_events(self):
"""
Retrieve the next N events from Google.
"""
now = datetime.datetime.now(tz=pytz.UTC)
try:
now, later = self.get_timerange_formatted(now)
events_result = self.service.events().list(
calendarId='primary',
timeMin=now,
timeMax=later,
maxResults=10,
singleEvents=True,
orderBy='startTime',
timeZone='utc'
).execute()
self.events.clear()
for event in events_result.get('items', []):
self.events.append(GoogleCalendarEvent(event))
except HttpError as e:
if e.resp.status in (500, 503):
self.logger.warn("GoogleCalendar received %s while retrieving events" % e.resp.status)
else:
raise
def get_timerange_formatted(self, now):
"""
Return two ISO8601 formatted date strings, one for timeMin, the other for timeMax (to be consumed by get_events)
"""
later = now + datetime.timedelta(days=self.days)
return now.isoformat(), later.isoformat()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/calendar/khal_calendar.py 0000664 0000000 0000000 00000003537 13567273623 0025406 0 ustar 00root root 0000000 0000000 from datetime import date, timedelta
import khal
import khal.cli
import khal.settings
from i3pystatus.calendar import CalendarBackend, CalendarEvent, formatter
class KhalEvent(CalendarEvent):
def __init__(self, khal_event):
self.id = khal_event.uid
self.start = khal_event.start_local
self.end = khal_event.end_local
self.title = khal_event.summary
self.recurring = khal_event.recurring
self._calendar = khal_event.calendar
@formatter
def calendar(self):
return self._calendar
class Khal(CalendarBackend):
"""
Backend for Khal. Requires `khal` to be installed.
.. rubric:: Available formatters
* `{calendar}` — Calendar event is from.
"""
settings = (
('config_path', 'Path to your khal.conf'),
('calendars', 'Restrict to these calendars pass as a list)'),
('days', 'Check for the next X days'),
)
days = 7
config_path = None
calendars = None
def init(self):
self.collection = None
self.events = []
def open_connection(self):
self.logger.debug("Opening collection with config {}".format(self.config_path))
config = khal.settings.get_config(self.config_path)
self.collection = khal.cli.build_collection(config, None)
def update(self):
if self.collection is None:
self.open_connection()
events = []
for days in range(self.days):
events += list(self.collection.get_events_on(
date.today() + timedelta(days=days))
)
# filter out unwanted calendars
self.logger.debug("calendars %s" % self.calendars)
if self.calendars is not None:
events = [evt for evt in events if evt.calendar in self.calendars]
for event in events:
self.events.append(KhalEvent(event))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/calendar/lightning.py 0000664 0000000 0000000 00000006047 13567273623 0024620 0 ustar 00root root 0000000 0000000 import sqlite3
from datetime import datetime
import pytz
from dateutil.tz import tzlocal
from i3pystatus.calendar import CalendarEvent, CalendarBackend, formatter
class Flag:
PRIVATE = 1
HAS_ATTENDEES = 2
HAS_PROPERTIES = 4
EVENT_ALLDAY = 8
HAS_RECURRENCE = 16
HAS_EXCEPTIONS = 32
HAS_ATTACHMENTS = 64
HAS_RELATIONS = 128
HAS_ALARMS = 256
RECURRENCE_ID_ALLDAY = 512
class LightningCalendarEvent(CalendarEvent):
def __init__(self, row):
self.id = row['id']
self.title = row['title']
self._event_start = row['event_start']
self._event_start_tz = row['event_start_tz']
self._event_end = row['event_end']
self._event_end_tz = row['event_end_tz']
self._flags = row['flags']
self._location = row['location'] or ''
@property
def recurring(self):
return (self._flags & Flag.HAS_RECURRENCE) != 0
@property
def end(self):
return self._convert_date(self._event_end, self._event_end_tz)
@property
def start(self):
return self._convert_date(self._event_start, self._event_start_tz)
@formatter
def location(self):
return self._location
def _convert_date(self, microseconds_from_epoch, timezone):
if timezone == 'floating':
tz = tzlocal()
else:
tz = pytz.timezone(timezone)
d = datetime.fromtimestamp(microseconds_from_epoch / 1000000, tz=pytz.UTC)
return d.astimezone(tz)
class Lightning(CalendarBackend):
"""
Backend for querying the Thunderbird's Lightning database. Requires `pytz` and `dateutil`.
.. rubric:: Available formatters
* `{location}` — Where the event occurs
"""
settings = (
('database_path', 'Path to local.sqlite.'),
('days', 'Only show events between now and this many days in the future'),
)
required = ('database_path',)
days = 7
database_path = None
def update(self):
with sqlite3.connect(self.database_path) as connection:
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
cursor.execute("""
SELECT
id,
title,
event_start,
event_start_tz,
event_end,
event_end_tz,
flags,
cal_properties.value AS location
FROM cal_events
LEFT OUTER JOIN cal_properties ON cal_properties.item_id = id AND cal_properties.key = 'LOCATION'
WHERE
datetime(event_start / 1000000, 'unixepoch', 'localtime') < datetime('now', 'localtime', '+' || :days || ' days')
AND
datetime(event_start / 1000000, 'unixepoch', 'localtime') > datetime('now', 'localtime')
ORDER BY event_start ASC
""", dict(days=self.days))
self.events.clear()
for row in cursor:
self.events.append(LightningCalendarEvent(row))
cursor.close()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/circleci.py 0000664 0000000 0000000 00000012505 13567273623 0022635 0 ustar 00root root 0000000 0000000 import os
import dateutil.parser
from circleci.api import Api
from i3pystatus import IntervalModule
from i3pystatus.core.util import TimeWrapper, formatp, internet, require
__author__ = 'chestm007'
class CircleCI(IntervalModule):
"""
Get current status of circleci builds
Requires `circleci` `dateutil.parser`
Formatters:
* `{repo_slug}` - repository owner/repository name
* `{repo_status}` - repository status
* `{repo_name}` - repository name
* `{repo_owner}` - repository owner
* `{last_build_started}` - date of the last finished started
* `{last_build_duration}` - duration of the last build, not populated with workflows(yet)
Examples
.. code-block:: python
status_color_map = {
'passed': '#00FF00',
'failed': '#FF0000',
'errored': '#FFAA00',
'cancelled': '#EEEEEE',
'started': '#0000AA',
}
.. code-block:: python
repo_status_map={
'success': 'success',
'running': 'running',
'failed': 'failed',
}
"""
settings = (
'format',
('circleci_token', 'circleci access token'),
('repo_slug', 'repository identifier eg. "enkore/i3pystatus"'),
('time_format', 'passed directly to .strftime() for `last_build_started`'),
('repo_status_map', 'map representing how to display status'),
('duration_format', '`last_build_duration` format string'),
('status_color_map', 'color for all text based on status'),
('color', 'color for all text not otherwise colored'),
('workflow_name', '[WORKFLOWS_ONLY] if specified, monitor this workflows status. if not specified this module '
'will default to reporting the status of your last build'),
('workflow_branch', '[WORKFLOWS_ONLY] if specified, monitor the workflows in this branch'))
required = ('circleci_token', 'repo_slug')
format = '{repo_owner}/{repo_name}-{repo_status} [({last_build_started}({last_build_duration}))]'
short_format = '{repo_name}-{repo_status}'
time_format = '%m/%d'
duration_format = '%m:%S'
status_color_map = None
repo_slug = None
circleci_token = None
repo_status_map = None
color = '#DDDDDD'
workflow_name = None
workflow_branch = None
circleci = None
on_leftclick = 'open_build_webpage'
def init(self):
self.repo_status = None
self.last_build_duration = None
self.last_build_started = None
self.repo_owner, self.repo_name = self.repo_slug.split('/')
self.workflows = self.workflow_name is not None or self.workflow_branch is not None
def _format_time(self, time):
_datetime = dateutil.parser.parse(time)
return _datetime.strftime(self.time_format)
@require(internet)
def run(self):
if self.circleci is None:
self.circleci = Api(self.circleci_token)
if self.workflows:
if self.workflow_branch and not self.workflow_name:
self.output = dict(
full_text='workflow_name must be specified!'
)
return
project = {p['reponame']: p for p in self.circleci.get_projects()}.get(self.repo_name)
if not self.workflow_branch:
self.workflow_branch = project.get('default_branch')
workflow_info = project['branches'].get(self.workflow_branch)['latest_workflows'].get(self.workflow_name)
self.last_build_started = self._format_time(workflow_info.get('created_at'))
self.repo_status = workflow_info.get('status')
self.last_build_duration = '' # TODO: gather this information once circleCI exposes it
else:
self.repo_summary = self.circleci.get_project_build_summary(
self.repo_owner,
self.repo_name,
limit=1)
if len(self.repo_summary) != 1:
return
self.repo_summary = self.repo_summary[0]
self.repo_status = self.repo_summary.get('status')
self.last_build_started = self._format_time(self.repo_summary.get('queued_at'))
try:
self.last_build_duration = TimeWrapper(
self.repo_summary.get('build_time_millis') / 1000,
default_format=self.duration_format)
except TypeError:
self.last_build_duration = 0
if self.repo_status_map:
self.repo_status = self.repo_status_map.get(self.repo_status, self.repo_status)
self.output = dict(
full_text=formatp(self.format, **vars(self)),
short_text=self.short_format.format(**vars(self)),
)
if self.status_color_map:
self.output['color'] = self.status_color_map.get(self.repo_status, self.color)
else:
self.output['color'] = self.color
def open_build_webpage(self):
if self.repo_summary.get('workflows'):
url_format = 'workflow-run/{}'.format(self.repo_summary['workflows']['workflow_id'])
else:
url_format = 'gh/{repo_owner}/{repo_name}/{job_number}'
os.popen('xdg-open https:/circleci.com/{} > /dev/null'
.format(url_format))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/clock.py 0000664 0000000 0000000 00000011676 13567273623 0022163 0 ustar 00root root 0000000 0000000 import errno
import os
import locale
from datetime import datetime
try:
import pytz
HAS_PYTZ = True
except ImportError:
HAS_PYTZ = False
from i3pystatus import IntervalModule
class Clock(IntervalModule):
"""
This class shows a clock.
.. note:: Optionally requires `pytz` for time zone data when using time
zones other than local time.
Format can be passed in four different ways:
- single string, no timezone, just the strftime-format
- one two-tuple, first is the format, second the timezone
- list of strings - no timezones
- list of two tuples, first is the format, second is timezone
Use mousewheel to cycle between formats.
For complete time format specification see:
::
man strftime
All available timezones are located in directory:
::
/usr/share/zoneinfo/
.. rubric:: Format examples
::
# one format, local timezone
format = '%a %b %-d %b %X'
# multiple formats, local timezone
format = [ '%a %b %-d %b %X', '%X' ]
# one format, specified timezone
format = ('%a %b %-d %b %X', 'Europe/Bratislava')
# multiple formats, specified timezones
format = [ ('%a %b %-d %b %X', 'America/New_York'), ('%X', 'Etc/GMT+9') ]
"""
settings = (
("format", "`None` means to use the default, locale-dependent format."),
("color", "RGB hexadecimal code color specifier, default to #ffffff"),
)
format = None
color = "#ffffff"
interval = 1
on_upscroll = ["scroll_format", 1]
on_downscroll = ["scroll_format", -1]
def init(self):
env_lang = os.environ.get('LC_TIME', None)
if env_lang is None:
env_lang = os.environ.get('LANG', None)
if env_lang is not None:
if env_lang.find('.') != -1:
lang = tuple(env_lang.split('.', 1))
else:
lang = (env_lang, None)
else:
lang = (None, None)
if lang != locale.getlocale(locale.LC_TIME):
# affects language of *.strftime() in whole program
locale.setlocale(locale.LC_TIME, lang)
if self.format is None:
if lang[0] == 'en_US':
# MDY format - United States of America
self.format = ["%a %b %-d %X"]
else:
# DMY format - almost all other countries
self.format = ["%a %-d %b %X"]
elif isinstance(self.format, str) or isinstance(self.format, tuple):
self.format = [self.format]
self.system_tz = self._get_system_tz()
self.format = [self._expand_format(fmt) for fmt in self.format]
self.current_format_id = 0
def _expand_format(self, fmt):
if isinstance(fmt, tuple):
if len(fmt) == 1:
return (fmt[0], None)
else:
if not HAS_PYTZ:
raise RuntimeError("Need `pytz` for timezone data")
return (fmt[0], pytz.timezone(fmt[1]))
return (fmt, self.system_tz)
def _get_system_tz(self):
'''
Get the system timezone for use when no timezone is explicitly provided
Requires pytz, if not available then no timezone will be set when not
explicitly provided.
'''
if not HAS_PYTZ:
return None
def _etc_localtime():
try:
with open('/etc/localtime', 'rb') as fp:
return pytz.tzfile.build_tzinfo('system', fp)
except OSError as exc:
if exc.errno != errno.ENOENT:
self.logger.error(
'Unable to read from /etc/localtime: %s', exc.strerror
)
except pytz.UnknownTimeZoneError:
self.logger.error(
'/etc/localtime contains unrecognized tzinfo'
)
return None
def _etc_timezone():
try:
with open('/etc/timezone', 'r') as fp:
tzname = fp.read().strip()
return pytz.timezone(tzname)
except OSError as exc:
if exc.errno != errno.ENOENT:
self.logger.error(
'Unable to read from /etc/localtime: %s', exc.strerror
)
except pytz.UnknownTimeZoneError:
self.logger.error(
'/etc/timezone contains unrecognized timezone \'%s\'',
tzname
)
return None
return _etc_localtime() or _etc_timezone()
def run(self):
time = datetime.now(self.format[self.current_format_id][1])
self.output = {
"full_text": time.strftime(self.format[self.current_format_id][0]),
"color": self.color,
"urgent": False,
}
def scroll_format(self, step=1):
self.current_format_id = (self.current_format_id + step) % len(self.format)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/cmus.py 0000664 0000000 0000000 00000010075 13567273623 0022027 0 ustar 00root root 0000000 0000000 import os
from i3pystatus import formatp
from i3pystatus import IntervalModule
from i3pystatus.core.command import run_through_shell
from i3pystatus.core.util import TimeWrapper
def _extract_artist_title(input):
artist, title = (input.split('-') + [''])[:2]
return artist.strip(), title.strip()
class Cmus(IntervalModule):
"""
Gets the status and current song info using cmus-remote
.. rubric:: Available formatters
* `{status}` — current status icon (paused/playing/stopped)
* `{song_elapsed}` — song elapsed time (mm:ss format)
* `{song_length}` — total song duration (mm:ss format)
* `{artist}` — artist
* `{title}` — title
* `{album}` — album
* `{tracknumber}` — tracknumber
* `{file}` — file or url name
* `{stream}` — song name from stream
* `{bitrate}` — bitrate
"""
settings = (
('format', 'formatp string'),
('format_not_running', 'Text to show if cmus is not running'),
('color', 'The color of the text'),
('color_not_running', 'The color of the text, when cmus is not running'),
('status', 'Dictionary mapping status to output'),
)
color = '#ffffff'
color_not_running = '#ffffff'
format = '{status} {song_elapsed}/{song_length} {artist} - {title}'
format_not_running = 'Not running'
interval = 1
status = {
'paused': '▷',
'playing': '▶',
'stopped': '◾',
}
on_leftclick = 'playpause'
on_rightclick = 'next_song'
on_upscroll = 'next_song'
on_downscroll = 'previous_song'
def _cmus_command(self, command):
cmdline = 'cmus-remote --{command}'.format(command=command)
return run_through_shell(cmdline, enable_shell=True)
def _query_cmus(self):
response = {}
cmd = self._cmus_command('query')
if not cmd.rc:
for line in cmd.out.splitlines():
category, _, category_value = line.partition(' ')
if category in ('set', 'tag'):
key, _, value = category_value.partition(' ')
key = '_'.join((category, key))
response[key] = value
else:
response[category] = category_value
return response
def run(self):
response = self._query_cmus()
if response:
fdict = {
'file': response.get('file', ''),
'status': self.status[response['status']],
'title': response.get('tag_title', ''),
'stream': response.get('stream', ''),
'album': response.get('tag_album', ''),
'artist': response.get('tag_artist', ''),
'tracknumber': response.get('tag_tracknumber', 0),
'song_length': TimeWrapper(response.get('duration', 0)),
'song_elapsed': TimeWrapper(response.get('position', 0)),
'bitrate': int(response.get('bitrate', 0)),
}
if fdict['stream']:
fdict['artist'], fdict['title'] = _extract_artist_title(fdict['stream'])
elif not fdict['title']:
filename = os.path.basename(fdict['file'])
filebase, _ = os.path.splitext(filename)
fdict['artist'], fdict['title'] = _extract_artist_title(filebase)
self.data = fdict
self.output = {"full_text": formatp(self.format, **fdict),
"color": self.color}
else:
if hasattr(self, "data"):
del self.data
self.output = {"full_text": self.format_not_running,
"color": self.color_not_running}
def playpause(self):
status = self._query_cmus().get('status', '')
if status == 'playing':
self._cmus_command('pause')
if status == 'paused':
self._cmus_command('play')
if status == 'stopped':
self._cmus_command('play')
def next_song(self):
self._cmus_command('next')
def previous_song(self):
self._cmus_command('prev')
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/coin.py 0000664 0000000 0000000 00000005520 13567273623 0022007 0 ustar 00root root 0000000 0000000 import requests
import json
from decimal import Decimal
from i3pystatus import IntervalModule
from i3pystatus.core.util import internet, require
class Coin(IntervalModule):
"""
Fetches live data of all cryptocurrencies availible at coinmarketcap .
Coin setting should be equal to the 'id' field of your coin in .
Example coin settings: bitcoin, bitcoin-cash, ethereum, litecoin, dash, lisk.
Example currency settings: usd, eur, huf.
.. rubric:: Available formatters
* {symbol}
* {price}
* {rank}
* {24h_volume}
* {market_cap}
* {available_supply}
* {total_supply}
* {max_supply}
* {percent_change_1h}
* {percent_change_24h}
* {percent_change_7d}
* {last_updated} - time of last update on the API's part
* {status}
"""
settings = (
("format", "format string used for output."),
("color"),
("coin", "cryptocurrency to fetch"),
("decimal", "round coin price down to this decimal place"),
("currency", "fiat currency to show fiscal data"),
("symbol", "coin symbol"),
("interval", "update interval in seconds"),
("status_interval", "percent change status in the last: '1h' / '24h' / '7d'")
)
symbol = "¤"
color = None
format = "{symbol} {price}{status}"
coin = "ethereum"
currency = "USD"
interval = 600
status_interval = "24h"
decimal = 2
def fetch_data(self):
response = requests.get("https://api.coinmarketcap.com/v1/ticker/{}/?convert={}".format(self.coin, self.currency))
coin_data = response.json()[0]
coin_data["price"] = coin_data.pop("price_{}".format(self.currency.lower()))
coin_data["24h_volume"] = coin_data.pop("24h_volume_{}".format(self.currency.lower()))
coin_data["market_cap"] = coin_data.pop("market_cap_{}".format(self.currency.lower()))
coin_data["symbol"] = self.symbol
return coin_data
def set_status(self, change):
if change > 10:
return '⮅'
elif change > 0:
return '⭡'
elif change < -10:
return '⮇'
elif change < 0:
return '⭣'
else:
return ''
@require(internet)
def run(self):
fdict = self.fetch_data()
symbols = dict(bitcoin='฿', ethereum='Ξ', litecoin='Ł', dash='Đ')
if self.coin in symbols:
fdict["symbol"] = symbols[self.coin]
fdict["status"] = self.set_status(float(fdict["percent_change_{}".format(self.status_interval)]))
fdict["price"] = str(round(Decimal(fdict["price"]), self.decimal))
self.data = fdict
self.output = {"full_text": self.format.format(**fdict)}
if self.color is not None:
self.output['color'] = self.color
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/ 0000775 0000000 0000000 00000000000 13567273623 0021433 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/__init__.py 0000664 0000000 0000000 00000012521 13567273623 0023545 0 ustar 00root root 0000000 0000000 import logging
import os
import sys
from threading import Thread
from i3pystatus.core import io, util
from i3pystatus.core.exceptions import ConfigError
from i3pystatus.core.imputil import ClassFinder
from i3pystatus.core.modules import Module
DEFAULT_LOG_FORMAT = '%(asctime)s [%(levelname)-8s][%(name)s %(lineno)d] %(message)s'
log = logging.getLogger(__name__)
class CommandEndpoint:
"""
Endpoint for i3bar click events: http://i3wm.org/docs/i3bar-protocol.html#_click_events
:param modules: dict-like object with item access semantics via .get()
:param io_handler_factory: function creating a file-like object returning a JSON generator on .read()
"""
def __init__(self, modules, io_handler_factory, io):
self.modules = modules
self.io_handler_factory = io_handler_factory
self.io = io
self.thread = Thread(target=self._command_endpoint)
self.thread.daemon = True
def start(self):
"""Starts the background thread"""
self.thread.start()
def _command_endpoint(self):
for cmd in self.io_handler_factory().read():
target_module = self.modules.get(cmd["instance"])
button = cmd["button"]
kwargs = {"button_id": button}
try:
kwargs.update({"pos_x": cmd["x"],
"pos_y": cmd["y"]})
except Exception:
continue
if target_module:
target_module.on_click(button, **kwargs)
target_module.run()
self.io.async_refresh()
class Status:
"""
The main class used for registering modules and managing I/O
:param bool standalone: Whether i3pystatus should read i3status-compatible input from `input_stream`.
:param int interval: Update interval in seconds.
:param input_stream: A file-like object that provides the input stream, if `standalone` is False.
:param bool click_events: Enable click events, if `standalone` is True.
:param str logfile: Path to log file that will be used by i3pystatus.
:param tuple internet_check: Address of server that will be used to check for internet connection by :py:class:`.internet`.
:param keep_alive: If True, modules that define the keep_alive flag will not be put to sleep when the status bar is hidden.
:param dictionary default_hints: Dictionary of default hints to apply to all modules. Can be overridden at a module level.
"""
def __init__(self, standalone=True, click_events=True, interval=1,
input_stream=None, logfile=None, internet_check=None,
keep_alive=False, logformat=DEFAULT_LOG_FORMAT,
default_hints=None):
self.standalone = standalone
self.default_hints = default_hints
self.click_events = standalone and click_events
input_stream = input_stream or sys.stdin
logger = logging.getLogger("i3pystatus")
if logfile:
for handler in logger.handlers:
logger.removeHandler(handler)
logfile = os.path.expandvars(logfile)
handler = logging.FileHandler(logfile, delay=True)
logger.addHandler(handler)
logger.setLevel(logging.CRITICAL)
if logformat:
for index in range(len(logger.handlers)):
logger.handlers[index].setFormatter(logging.Formatter(logformat))
if internet_check:
util.internet.address = internet_check
self.modules = util.ModuleList(self, ClassFinder(Module))
if self.standalone:
self.io = io.StandaloneIO(self.click_events, self.modules, keep_alive, interval)
if self.click_events:
self.command_endpoint = CommandEndpoint(
self.modules,
lambda: io.JSONIO(io=io.IOHandler(sys.stdin, open(os.devnull, "w")), skiplines=1),
self.io)
else:
self.io = io.IOHandler(input_stream)
def register(self, module, *args, **kwargs):
"""
Register a new module.
:param module: Either a string module name, or a module class,
or a module instance (in which case args and kwargs are
invalid).
:param kwargs: Settings for the module.
:returns: module instance
"""
from i3pystatus.text import Text
if not module:
return
# Merge the module's hints with the default hints
# and overwrite any duplicates with the hint from the module
hints = self.default_hints.copy() if self.default_hints else {}
hints.update(kwargs.get('hints', {}))
if hints:
kwargs['hints'] = hints
try:
return self.modules.append(module, *args, **kwargs)
except Exception as e:
log.exception(e)
return self.modules.append(Text(
color="#FF0000",
text="{i3py_mod}: Fatal Error - {ex}({msg})".format(
i3py_mod=module,
ex=e.__class__.__name__,
msg=e
)
))
def run(self):
"""
Run main loop.
"""
if self.click_events:
self.command_endpoint.start()
for j in io.JSONIO(self.io).read():
for module in self.modules:
module.inject(j)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/color.py 0000664 0000000 0000000 00000003507 13567273623 0023130 0 ustar 00root root 0000000 0000000 from colour import Color
class ColorRangeModule(object):
"""
Class to dynamically generate and select colors.
Requires the PyPI package `colour`
"""
start_color = "#00FF00"
end_color = 'red'
@staticmethod
def get_hex_color_range(start_color, end_color, quantity):
"""
Generates a list of quantity Hex colors from start_color to end_color.
:param start_color: Hex or plain English color for start of range
:param end_color: Hex or plain English color for end of range
:param quantity: Number of colours to return
:return: A list of Hex color values
"""
raw_colors = [c.hex for c in list(Color(start_color).range_to(Color(end_color), quantity))]
colors = []
for color in raw_colors:
# i3bar expects the full Hex value but for some colors the colour
# module only returns partial values. So we need to convert these colors to the full
# Hex value.
if len(color) == 4:
fixed_color = "#"
for c in color[1:]:
fixed_color += c * 2
colors.append(fixed_color)
else:
colors.append(color)
return colors
def get_gradient(self, value, colors, upper_limit=100):
"""
Map a value to a color
:param value: Some value
:return: A Hex color code
"""
index = int(self.percentage(value, upper_limit))
if index >= len(colors):
return colors[-1]
elif index < 0:
return colors[0]
else:
return colors[index]
@staticmethod
def percentage(part, whole):
"""
Calculate percentage
"""
if whole == 0:
return 0
return 100 * float(part) / float(whole)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/command.py 0000664 0000000 0000000 00000005661 13567273623 0023433 0 ustar 00root root 0000000 0000000 import logging
import shlex
import subprocess
from collections import namedtuple
CommandResult = namedtuple("Result", ['rc', 'out', 'err'])
def run_through_shell(command, enable_shell=False):
"""
Retrieve output of a command.
Returns a named tuple with three elements:
* ``rc`` (integer) Return code of command.
* ``out`` (string) Everything that was printed to stdout.
* ``err`` (string) Everything that was printed to stderr.
Don't use this function with programs that outputs lots of data since the
output is saved in one variable.
:param command: A string or a list of strings containing the name and
arguments of the program.
:param enable_shell: If set ot `True` users default shell will be invoked
and given ``command`` to execute. The ``command`` should obviously be a
string since shell does all the parsing.
"""
if not enable_shell and isinstance(command, str):
command = shlex.split(command)
returncode = None
stderr = None
try:
proc = subprocess.Popen(command, stderr=subprocess.PIPE,
stdout=subprocess.PIPE, shell=enable_shell)
out, stderr = proc.communicate()
out = out.decode("UTF-8")
stderr = stderr.decode("UTF-8")
returncode = proc.returncode
except OSError as e:
out = e.strerror
stderr = e.strerror
logging.getLogger("i3pystatus.core.command").exception("")
except subprocess.CalledProcessError as e:
out = e.output
logging.getLogger("i3pystatus.core.command").exception("")
return CommandResult(returncode, out, stderr)
def execute(command, detach=False):
"""
Runs a command in background. No output is retrieved. Useful for running GUI
applications that would block click events.
:param command: A string or a list of strings containing the name and
arguments of the program.
:param detach: If set to `True` the program will be executed using the
`i3-msg` command. As a result the program is executed independent of
i3pystatus as a child of i3 process. Because of how i3-msg parses its
arguments the type of `command` is limited to string in this mode.
"""
if detach:
if not isinstance(command, str):
msg = "Detached mode expects a string as command, not {}".format(
command)
logging.getLogger("i3pystatus.core.command").error(msg)
raise AttributeError(msg)
command = ["i3-msg", "exec", command]
else:
if isinstance(command, str):
command = shlex.split(command)
try:
subprocess.Popen(command, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except OSError:
logging.getLogger("i3pystatus.core.command").exception("")
except subprocess.CalledProcessError:
logging.getLogger("i3pystatus.core.command").exception("")
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/desktop.py 0000664 0000000 0000000 00000006332 13567273623 0023462 0 ustar 00root root 0000000 0000000 import logging
class BaseDesktopNotification:
"""
Class to display a desktop notification
:param title: Title of the notification
:param body: Body text of the notification, depending on the users system configuration HTML may be used, but is not recommended
:param icon: A XDG icon name, see http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
:param urgency: A value between 1 and 3 with 1 meaning low urgency and 3 high urgency.
:param timeout: Timeout in seconds for the notification. Zero means it needs to be dismissed by the user.
"""
def __init__(self, title, body, icon="dialog-information", urgency=1,
timeout=-1, log_level=logging.WARNING):
self.title = title
self.body = body
self.icon = icon
self.urgency = urgency
self.timeout = timeout
self.log_level = log_level
if self.__class__.__name__.startswith("i3pystatus"):
self.logger = logging.getLogger(self.__class__.__name__)
else:
self.logger = logging.getLogger("i3pystatus." + self.__class__.__name__)
self.logger.setLevel(self.log_level)
def display(self):
"""
Display this notification
:returns: boolean indicating success
"""
return False
def update(self, title=None, body=None, icon=None):
"""
Update this notification.
:param title: Title of the notification
:param body: Body text of the notification, depending on the users system configuration HTML may be used, but is not recommended
:param icon: A XDG icon name, see http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
:return boolean indicating success
"""
return False
class DesktopNotification(BaseDesktopNotification):
pass
try:
import gi
gi.require_version('Notify', '0.7')
from gi.repository import Notify
except (ImportError, ValueError, AttributeError):
pass
else:
if not Notify.init("i3pystatus"):
raise ImportError("Couldn't initialize libnotify")
# List of some useful icon names:
# battery, battery-caution, battery-low
# …
class DesktopNotification(DesktopNotification):
URGENCY_LUT = (
Notify.Urgency.LOW,
Notify.Urgency.NORMAL,
Notify.Urgency.CRITICAL,
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.notification = Notify.Notification.new(self.title, self.body, self.icon)
def display(self):
if self.timeout:
self.notification.set_timeout(self.timeout)
self.notification.set_urgency(self.URGENCY_LUT[self.urgency])
try:
return self.notification.show()
except Exception:
self.logger.exception(
'Failed to display desktop notification (is a '
'notification daemon running?)'
)
return False
def update(self, title=None, body=None, icon=None):
self.notification.update(title or self.title, body or self.body, icon or self.icon)
return self.notification.show()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/exceptions.py 0000664 0000000 0000000 00000001554 13567273623 0024173 0 ustar 00root root 0000000 0000000 class ConfigError(Exception):
"""ABC for configuration exceptions"""
def __init__(self, module, *args, **kwargs):
self.message = "Module '{0}': {1}".format(
module, self.format(*args, **kwargs))
super().__init__(self.message)
def format(self, *args, **kwargs):
return ""
class ConfigKeyError(ConfigError, KeyError):
def format(self, key):
return "invalid option '{0}'".format(key)
class ConfigMissingError(ConfigError):
def format(self, missing):
return "missing required options: {0}".format(missing)
class ConfigAmbigiousClassesError(ConfigError):
def format(self, ambigious_classes):
return "ambigious module specification, found multiple classes: {0}".format(ambigious_classes)
class ConfigInvalidModuleError(ConfigError):
def format(self):
return "no class found"
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/imputil.py 0000664 0000000 0000000 00000003664 13567273623 0023501 0 ustar 00root root 0000000 0000000 import inspect
import types
from importlib import import_module
from i3pystatus.core.exceptions import ConfigAmbigiousClassesError, ConfigInvalidModuleError
class ClassFinder:
"""Support class to find classes of specific bases in a module"""
def __init__(self, baseclass):
self.baseclass = baseclass
def predicate_factory(self, module):
def predicate(obj):
return (
inspect.isclass(obj)
and issubclass(obj, self.baseclass)
and obj.__module__ == module.__name__
)
return predicate
def get_matching_classes(self, module):
# Transpose [ (name, list), ... ] to ( [name, ...], [list, ...] )
classes = list(zip(*inspect.getmembers(module, self.predicate_factory(module))))
return classes[1] if classes else []
def get_class(self, module):
classes = self.get_matching_classes(module)
if len(classes) > 1:
# If there are multiple Module clases bundled in one module,
# well, we can't decide for the user.
raise ConfigAmbigiousClassesError(module.__name__, classes)
elif not classes:
raise ConfigInvalidModuleError(module.__name__)
return classes[0]
def get_module(self, module):
return import_module("i3pystatus.{mod}".format(mod=module))
def instanciate_class_from_module(self, module, *args, **kwargs):
if isinstance(module, types.ModuleType):
return self.get_class(module)(*args, **kwargs)
elif isinstance(module, str):
return self.instanciate_class_from_module(self.get_module(module), *args, **kwargs)
elif inspect.isclass(module) and issubclass(module, self.baseclass):
return module(*args, **kwargs)
elif args or kwargs:
raise ValueError(
"Additional arguments are invalid if 'module' is already an object")
return module
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/io.py 0000664 0000000 0000000 00000014274 13567273623 0022424 0 ustar 00root root 0000000 0000000 import json
import signal
import sys
from contextlib import contextmanager
from threading import Condition
from threading import Thread
from i3pystatus.core.modules import IntervalModule
class IOHandler:
def __init__(self, inp=sys.stdin, out=sys.stdout):
self.inp = inp
self.out = out
def write_line(self, message):
"""Unbuffered printing to stdout."""
self.out.write(message + "\n")
self.out.flush()
def read(self):
"""Iterate over all input lines (Generator)"""
while True:
try:
yield self.read_line()
except EOFError:
return
def read_line(self):
"""
Interrupted respecting reader for stdin.
Raises EOFError if the end of stream has been reached
"""
try:
line = self.inp.readline().strip()
except KeyboardInterrupt:
raise EOFError()
# i3status sends EOF, or an empty line
if not line:
raise EOFError()
return line
class StandaloneIO(IOHandler):
"""
I/O handler for standalone usage of i3pystatus (w/o i3status)
Writing works as usual, but reading will always return a empty JSON array,
and the i3bar protocol header
"""
n = -1
proto = [
{
"version": 1,
"click_events": True,
}, "[", "[]", ",[]",
]
def __init__(self, click_events, modules, keep_alive, interval=1):
"""
StandaloneIO instance must be created in main thread to be able to set
the SIGUSR1 signal handler.
"""
super().__init__()
self.interval = interval
self.modules = modules
self.proto[0]['click_events'] = click_events
if keep_alive:
self.proto[0].update(dict(stop_signal=signal.SIGUSR2,
cont_signal=signal.SIGUSR2))
signal.signal(signal.SIGUSR2, self.suspend_signal_handler)
self.proto[0] = json.dumps(self.proto[0])
self.refresh_cond = Condition()
self.treshold_interval = 20.0
self.stopped = False
signal.signal(signal.SIGUSR1, self.refresh_signal_handler)
def read(self):
self.compute_treshold_interval()
self.refresh_cond.acquire()
while True:
try:
self.refresh_cond.wait(timeout=self.interval)
except KeyboardInterrupt:
self.refresh_cond.release()
return
yield self.read_line()
def read_line(self):
self.n += 1
return self.proto[min(self.n, len(self.proto) - 1)]
def compute_treshold_interval(self):
"""
Current method is to compute average from all intervals.
"""
intervals = [m.interval for m in self.modules if hasattr(m, "interval")]
if len(intervals) > 0:
self.treshold_interval = round(sum(intervals) / len(intervals))
def async_refresh(self):
"""
Calling this method will send the status line to i3bar immediately
without waiting for timeout (1s by default).
"""
self.refresh_cond.acquire()
self.refresh_cond.notify()
self.refresh_cond.release()
def refresh_signal_handler(self, signo, frame):
"""
This callback is called when SIGUSR1 signal is received.
It updates outputs of all modules by calling their `run` method.
Interval modules are updated in separate threads if their interval is
above a certain treshold value.
This treshold is computed by :func:`compute_treshold_interval` class
method.
The reasoning is that modules with larger intervals also usually take
longer to refresh their output and that their output is not required in
'real time'.
This also prevents possible lag when updating all modules in a row.
"""
if signo != signal.SIGUSR1:
return
for module in self.modules:
if hasattr(module, "interval"):
if module.interval > self.treshold_interval:
thread = Thread(target=module.run)
thread.start()
else:
module.run()
else:
module.run()
self.async_refresh()
def suspend_signal_handler(self, signo, frame):
"""
By default, i3bar sends SIGSTOP to all children when it is not visible (for example, the screen
sleeps or you enter full screen mode). This stops the i3pystatus process and all threads within it.
For some modules, this is not desirable. Thankfully, the i3bar protocol supports setting the "stop_signal"
and "cont_signal" key/value pairs in the header to allow sending a custom signal when these events occur.
Here we use SIGUSR2 for both "stop_signal" and "cont_signal" and maintain a toggle to determine whether
we have just been stopped or continued. When we have been stopped, notify the IntervalModule managers
that they should suspend any module that does not set the keep_alive flag to a truthy value, and when we
have been continued, notify the IntervalModule managers that they can resume execution of all modules.
"""
if signo != signal.SIGUSR2:
return
self.stopped = not self.stopped
if self.stopped:
[m.suspend() for m in IntervalModule.managers.values()]
else:
[m.resume() for m in IntervalModule.managers.values()]
class JSONIO:
def __init__(self, io, skiplines=2):
self.io = io
for i in range(skiplines):
self.io.write_line(self.io.read_line())
def read(self):
"""Iterate over all JSON input (Generator)"""
for line in self.io.read():
with self.parse_line(line) as j:
yield j
@contextmanager
def parse_line(self, line):
"""Parse a single line of JSON and write modified JSON back."""
prefix = ""
# ignore comma at start of lines
if line.startswith(","):
line, prefix = line[1:], ","
j = json.loads(line)
yield j
self.io.write_line(prefix + json.dumps(j))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/modules.py 0000664 0000000 0000000 00000026406 13567273623 0023465 0 ustar 00root root 0000000 0000000 import inspect
import traceback
from i3pystatus.core.settings import SettingsBase
from i3pystatus.core.threading import Manager
from i3pystatus.core.util import (convert_position,
MultiClickHandler)
from i3pystatus.core.command import execute
def is_method_of(method, object):
"""Decide whether ``method`` is contained within the MRO of ``object``."""
if not callable(method) or not hasattr(method, "__name__"):
return False
if inspect.ismethod(method):
return method.__self__ is object
for cls in inspect.getmro(object.__class__):
if cls.__dict__.get(method.__name__, None) is method:
return True
return False
class Module(SettingsBase):
position = 0
settings = (
('on_leftclick', "Callback called on left click (see :ref:`callbacks`)"),
('on_middleclick', "Callback called on middle click (see :ref:`callbacks`)"),
('on_rightclick', "Callback called on right click (see :ref:`callbacks`)"),
('on_upscroll', "Callback called on scrolling up (see :ref:`callbacks`)"),
('on_downscroll', "Callback called on scrolling down (see :ref:`callbacks`)"),
('on_doubleleftclick', "Callback called on double left click (see :ref:`callbacks`)"),
('on_doubleleftclick', "Callback called on double left click (see :ref:`callbacks`)"),
('on_doublemiddleclick', "Callback called on double middle click (see :ref:`callbacks`)"),
('on_doublerightclick', "Callback called on double right click (see :ref:`callbacks`)"),
('on_doubleupscroll', "Callback called on double scroll up (see :ref:`callbacks`)"),
('on_doubledownscroll', "Callback called on double scroll down (see :ref:`callbacks`)"),
('on_otherclick', "Callback called on other click (see :ref:`callbacks`)"),
('on_doubleotherclick', "Callback called on double other click (see :ref:`callbacks`)"),
('on_change', "Callback called when output is changed (see :ref:`callbacks`)"),
('multi_click_timeout', "Time (in seconds) before a single click is executed."),
('hints', "Additional output blocks for module output (see :ref:`hints`)"),
)
on_leftclick = None
on_middleclick = None
on_rightclick = None
on_upscroll = None
on_downscroll = None
on_doubleleftclick = None
on_doublemiddleclick = None
on_doublerightclick = None
on_doubleupscroll = None
on_doubledownscroll = None
on_otherclick = None
on_change = None
on_doubleotherclick = None
multi_click_timeout = 0.25
hints = {"markup": "none"}
def __init__(self, *args, **kwargs):
self._output = None
super(Module, self).__init__(*args, **kwargs)
self.__multi_click = MultiClickHandler(self.__button_callback_handler,
self.multi_click_timeout)
@property
def output(self):
return self._output
@output.setter
def output(self, value):
self._output = value
if self.on_change:
self.on_change()
def registered(self, status_handler):
"""Called when this module is registered with a status handler"""
self.__status_handler = status_handler
def inject(self, json):
if self.output:
if "name" not in self.output:
self.output["name"] = self.__name__
self.output["instance"] = str(id(self))
if (self.output.get("color", "") or "").lower() == "#ffffff":
del self.output["color"]
if self.hints:
for key, val in self.hints.items():
if key not in self.output:
self.output.update({key: val})
if self.output.get("markup") == "pango":
self.text_to_pango()
json.insert(convert_position(self.position, json), self.output)
def run(self):
pass
def send_output(self):
"""Send a status update with the current module output"""
self.__status_handler.io.async_refresh()
def __log_button_event(self, button, cb, args, action, **kwargs):
msg = "{}: button={}, cb='{}', args={}, kwargs={}, type='{}'".format(
self.__name__, button, cb, args, kwargs, action)
self.logger.debug(msg)
def __button_callback_handler(self, button, cb, **kwargs):
def call_callback(cb, *args, **kwargs):
# Recover the function if wrapped (with get_module for example)
wrapped_cb = getattr(cb, "__wrapped__", None)
if wrapped_cb:
locals()["self"] = self # Add self to the local stack frame
tmp_cb = wrapped_cb
else:
tmp_cb = cb
try:
args_spec = inspect.getargspec(tmp_cb)
except Exception:
args_spec = inspect.ArgSpec([], None, None, None)
# Remove all variables present in kwargs that are not used in the
# callback, except if there is a keyword argument.
if not args_spec.keywords:
kwargs = {k: v for k, v in kwargs.items()
if k in args_spec.args}
cb(*args, **kwargs)
if not cb:
self.__log_button_event(button, None, None,
"No callback attached", **kwargs)
return False
if isinstance(cb, list):
cb, args = (cb[0], cb[1:])
else:
args = []
try:
our_method = is_method_of(cb, self)
if callable(cb) and not our_method:
self.__log_button_event(button, cb, args,
"Python callback", **kwargs)
call_callback(cb, *args, **kwargs)
elif our_method:
self.__log_button_event(button, cb, args,
"Method callback", **kwargs)
call_callback(cb, self, *args, **kwargs)
elif hasattr(self, cb):
if cb != "run":
# CommandEndpoint already calls run() after every
# callback to instantly update any changed state due
# to the callback's actions.
self.__log_button_event(button, cb, args,
"Member callback", **kwargs)
call_callback(getattr(self, cb), *args, **kwargs)
else:
self.__log_button_event(button, cb, args,
"External command", **kwargs)
if hasattr(self, "data"):
kwargs.update(self.data)
args = [str(arg).format(**kwargs) for arg in args]
cb = cb.format(**kwargs)
execute(cb + " " + " ".join(args), detach=True)
except Exception as e:
self.logger.critical("Exception while processing button "
"callback: {!r}".format(e))
self.logger.critical(traceback.format_exc())
# Notify status handler
try:
self.__status_handler.io.async_refresh()
except:
pass
def on_click(self, button, **kwargs):
"""
Maps a click event with its associated callback.
Currently implemented events are:
============ ================ =========
Event Callback setting Button ID
============ ================ =========
Left click on_leftclick 1
Middle click on_middleclick 2
Right click on_rightclick 3
Scroll up on_upscroll 4
Scroll down on_downscroll 5
Others on_otherclick > 5
============ ================ =========
The action is determined by the nature (type and value) of the callback
setting in the following order:
1. If null callback (``None``), no action is taken.
2. If it's a `python function`, call it and pass any additional
arguments.
3. If it's name of a `member method` of current module (string), call
it and pass any additional arguments.
4. If the name does not match with `member method` name execute program
with such name.
.. seealso:: :ref:`callbacks` for more information about
callback settings and examples.
:param button: The ID of button event received from i3bar.
:param kwargs: Further information received from i3bar like the
positions of the mouse where the click occured.
:return: Returns ``True`` if a valid callback action was executed.
``False`` otherwise.
"""
actions = ['leftclick', 'middleclick', 'rightclick',
'upscroll', 'downscroll']
try:
action = actions[button - 1]
except (TypeError, IndexError):
self.__log_button_event(button, None, None, "Other button")
action = "otherclick"
m_click = self.__multi_click
with m_click.lock:
double = m_click.check_double(button)
double_action = 'double%s' % action
if double:
action = double_action
# Get callback function
cb = getattr(self, 'on_%s' % action, None)
double_handler = getattr(self, 'on_%s' % double_action, None)
delay_execution = (not double and double_handler)
if delay_execution:
m_click.set_timer(button, cb, **kwargs)
else:
self.__button_callback_handler(button, cb, **kwargs)
def move(self, position):
self.position = position
return self
def text_to_pango(self):
"""
Replaces all ampersands in `full_text` and `short_text` attributes of
`self.output` with `&`.
It is called internally when pango markup is used.
Can be called multiple times (`&` won't change to `&`).
"""
def replace(s):
s = s.split("&")
out = s[0]
for i in range(len(s) - 1):
if s[i + 1].startswith("amp;"):
out += "&" + s[i + 1]
else:
out += "&" + s[i + 1]
return out
if "full_text" in self.output.keys():
self.output["full_text"] = replace(self.output["full_text"])
if "short_text" in self.output.keys():
self.output["short_text"] = replace(self.output["short_text"])
class IntervalModule(Module):
settings = (
("interval", "interval in seconds between module updates"),
)
interval = 5 # seconds
managers = {}
def registered(self, status_handler):
super(IntervalModule, self).registered(status_handler)
if self.interval in IntervalModule.managers:
IntervalModule.managers[self.interval].append(self)
else:
am = Manager(self.interval)
am.append(self)
IntervalModule.managers[self.interval] = am
am.start()
def __call__(self):
self.run()
def run(self):
"""Called approximately every self.interval seconds
Do not rely on this being called from the same thread at all times.
If you need to always have the same thread context, subclass AsyncModule."""
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/settings.py 0000664 0000000 0000000 00000013407 13567273623 0023652 0 ustar 00root root 0000000 0000000 from i3pystatus.core.util import KeyConstraintDict
from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError
import inspect
import logging
import getpass
class SettingsBaseMeta(type):
"""Add interval setting to `settings` attribute if it does not exist."""
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
cls.settings, cls.required = SettingsBaseMeta.get_merged_settings(cls)
@staticmethod
def get_merged_settings(cls):
def unique(settings):
def name(s):
return s[0] if isinstance(s, tuple) else s
seen = set()
return [setting for setting in settings if not (
name(setting) in seen or seen.add(name(setting)))]
settings = tuple()
required = set()
# getmro returns base classes according to Method Resolution Order,
# which always includes the class itself as the first element.
for base in inspect.getmro(cls):
settings += tuple(getattr(base, "settings", []))
required |= set(getattr(base, "required", []))
# if a derived class defines a default for a setting it is not
# required anymore, provided that default is not set to None.
for base in inspect.getmro(cls):
for r in list(required):
if hasattr(base, r) and getattr(base, r) != getattr(cls, r) \
or hasattr(cls, r) and getattr(cls, r) is not None:
required.remove(r)
return unique(settings), required
class SettingsBase(metaclass=SettingsBaseMeta):
"""
Support class for providing a nice and flexible settings interface
Classes inherit from this class and define what settings they provide and
which are required.
The constructor is either passed a dictionary containing these settings, or
keyword arguments specifying the same.
Settings are stored as attributes of self.
"""
__PROTECTED_SETTINGS = ["password", "email", "username"]
settings = (
("log_level", "Set to true to log error to .i3pystatus- file."),
)
"""settings should be tuple containing two types of elements:
* bare strings, which must be valid Python identifiers.
* two-tuples, the first element being a identifier (as above) and the second a docstring for the particular setting
"""
required = tuple()
"""required can list settings which are required"""
log_level = logging.WARNING
logger = None
def __init__(self, *args, **kwargs):
def get_argument_dict(args, kwargs):
if len(args) == 1 and not kwargs:
# User can also pass in a dict for their settings
# Note: you could do that anyway, with the ** syntax
return args[0]
return kwargs
self.__name__ = "{}.{}".format(self.__module__, self.__class__.__name__)
settings = self.flatten_settings(self.settings)
sm = KeyConstraintDict(settings, self.required)
settings_source = get_argument_dict(args, kwargs)
protected = self.get_protected_settings(settings_source)
settings_source.update(protected)
try:
sm.update(settings_source)
except KeyError as exc:
raise ConfigKeyError(type(self).__name__, key=exc.args[0]) from exc
try:
self.__dict__.update(sm)
except KeyConstraintDict.MissingKeys as exc:
raise ConfigMissingError(
type(self).__name__, missing=exc.keys) from exc
if self.__name__.startswith("i3pystatus"):
self.logger = logging.getLogger(self.__name__)
else:
self.logger = logging.getLogger("i3pystatus." + self.__name__)
self.logger.setLevel(self.log_level)
self.init()
def get_protected_settings(self, settings_source):
"""
Attempt to retrieve protected settings from keyring if they are not already set.
"""
user_backend = settings_source.get('keyring_backend')
found_settings = dict()
for setting_name in self.__PROTECTED_SETTINGS:
# Nothing to do if the setting is already defined.
if settings_source.get(setting_name):
continue
setting = None
identifier = "%s.%s" % (self.__name__, setting_name)
if hasattr(self, 'required') and setting_name in getattr(self, 'required'):
setting = self.get_setting_from_keyring(identifier, user_backend)
elif hasattr(self, setting_name):
setting = self.get_setting_from_keyring(identifier, user_backend)
if setting:
found_settings.update({setting_name: setting})
return found_settings
def get_setting_from_keyring(self, setting_identifier, keyring_backend=None):
"""
Retrieves a protected setting from keyring
:param setting_identifier: must be in the format package.module.Class.setting
"""
# If a custom keyring backend has been defined, use it.
if keyring_backend:
return keyring_backend.get_password(setting_identifier, getpass.getuser())
# Otherwise try and use default keyring.
try:
import keyring
except ImportError:
pass
else:
return keyring.get_password(setting_identifier, getpass.getuser())
def init(self):
"""Convenience method which is called after all settings are set
In case you don't want to type that super()…blabla :-)"""
@staticmethod
def flatten_settings(settings):
def flatten_setting(setting):
return setting[0] if isinstance(setting, tuple) else setting
return tuple(flatten_setting(setting) for setting in settings)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/threading.py 0000664 0000000 0000000 00000012232 13567273623 0023752 0 ustar 00root root 0000000 0000000 import threading
import time
import sys
from i3pystatus.core.util import partition
timer = time.perf_counter if hasattr(time, "perf_counter") else time.clock
def unwrap_workload(workload):
""" Obtain the module from it's wrapper. """
while isinstance(workload, Wrapper):
workload = workload.workload
return workload
class Thread(threading.Thread):
def __init__(self, target_interval, workloads=None, start_barrier=1):
super().__init__()
self.workloads = workloads or []
self.target_interval = target_interval
self.start_barrier = start_barrier
self._suspended = threading.Event()
self.daemon = True
def __iter__(self):
return iter(self.workloads)
def __len__(self):
return len(self.workloads)
def pop(self):
return self.workloads.pop()
def append(self, workload):
self.workloads.append(workload)
@property
def time(self):
return sum(map(lambda workload: workload.time, self))
def wait_for_start_barrier(self):
while len(self) <= self.start_barrier:
time.sleep(0.4)
def execute_workloads(self):
for workload in self:
if self.should_execute(workload):
workload()
self.workloads.sort(key=lambda workload: workload.time)
def should_execute(self, workload):
"""
If we have been suspended by i3bar, only execute those modules that set the keep_alive flag to a truthy
value. See the docs on the suspend_signal_handler method of the io module for more information.
"""
if not self._suspended.is_set():
return True
workload = unwrap_workload(workload)
return hasattr(workload, 'keep_alive') and getattr(workload, 'keep_alive')
def run(self):
while self:
self.execute_workloads()
filltime = self.target_interval - self.time
if filltime > 0:
time.sleep(filltime)
def branch(self, vtime, bound):
if len(self) > 1 and vtime > bound:
remove = self.pop()
return [remove] + self.branch(vtime - remove.time, bound)
return []
def suspend(self):
self._suspended.set()
def resume(self):
self._suspended.clear()
class Wrapper:
def __init__(self, workload):
self.workload = workload
def __repr__(self):
return repr(self.workload)
class ExceptionWrapper(Wrapper):
def __call__(self):
try:
self.workload()
except:
message = "Exception in {thread} at {time}, module {name}".format(
thread=threading.current_thread().name,
time=time.strftime("%c"),
name=self.workload.__class__.__name__
)
if hasattr(self.workload, "logger"):
self.workload.logger.error(message, exc_info=True)
self.workload.output = {
"full_text": self.format_exception(),
"color": "#FF0000",
}
def format_exception(self):
type, value, _ = sys.exc_info()
exception = self.truncate_error("%s: %s" % (type.__name__, value))
return "%s: %s" % (self.workload.__class__.__name__, exception)
def truncate_error(self, exception_message):
if hasattr(self.workload, 'max_error_len'):
error_len = self.workload.max_error_len
if len(exception_message) > error_len:
return exception_message[:error_len] + '…'
else:
return exception_message
else:
return exception_message
class WorkloadWrapper(Wrapper):
time = 0.0
def __call__(self):
tp1 = timer()
self.workload()
self.time = timer() - tp1
class Manager:
def __init__(self, target_interval):
self.target_interval = target_interval
self.upper_bound = target_interval * 1.1
self.lower_bound = target_interval * 0.7
initial_thread = Thread(target_interval, [self.wrap(self)])
self.threads = [initial_thread]
def __call__(self):
separate = []
for thread in self.threads:
separate.extend(thread.branch(thread.time, self.upper_bound))
self.create_threads(self.partition_workloads(separate))
def __repr__(self):
return "Manager"
def wrap(self, workload):
return WorkloadWrapper(ExceptionWrapper(workload))
def partition_workloads(self, workloads):
return partition(workloads, self.lower_bound, lambda workload: workload.time)
def create_threads(self, threads):
for workloads in threads:
self.create_thread(workloads)
def create_thread(self, workloads):
thread = Thread(self.target_interval, workloads, start_barrier=0)
thread.start()
self.threads.append(thread)
def append(self, workload):
self.threads[0].append(self.wrap(workload))
def start(self):
for thread in self.threads:
thread.start()
def suspend(self):
for thread in self.threads:
thread.suspend()
def resume(self):
for thread in self.threads:
thread.resume()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/util.py 0000664 0000000 0000000 00000052010 13567273623 0022760 0 ustar 00root root 0000000 0000000 import collections
import functools
import re
import socket
import string
import inspect
from threading import Timer, RLock
import time
def lchop(string, prefix):
"""Removes a prefix from string
:param string: String, possibly prefixed with prefix
:param prefix: Prefix to remove from string
:returns: string without the prefix
"""
if string.startswith(prefix):
return string[len(prefix):]
return string
def popwhile(predicate, iterable):
"""Generator function yielding items of iterable while predicate holds for each item
:param predicate: function taking an item returning bool
:param iterable: iterable
:returns: iterable (generator function)
"""
while iterable:
item = iterable.pop()
if predicate(item):
yield item
else:
break
def partition(iterable, limit, key=lambda x: x):
def pop_partition():
sum = 0.0
while sum < limit and iterable:
sum += key(iterable[-1])
yield iterable.pop()
partitions = []
iterable.sort(reverse=True)
while iterable:
partitions.append(list(pop_partition()))
return partitions
def round_dict(dic, places):
"""
Rounds all values in a dict containing only numeric types to `places` decimal places.
If places is None, round to INT.
"""
if places is None:
for key, value in dic.items():
dic[key] = round(value)
else:
for key, value in dic.items():
dic[key] = round(value, places)
class ModuleList(collections.UserList):
def __init__(self, status_handler, class_finder):
self.status_handler = status_handler
self.finder = class_finder
super().__init__()
def append(self, module, *args, **kwargs):
module = self.finder.instanciate_class_from_module(
module, *args, **kwargs)
module.registered(self.status_handler)
super().append(module)
return module
def get(self, find_id):
find_id = int(find_id)
for module in self:
if id(module) == find_id:
return module
class KeyConstraintDict(collections.UserDict):
"""
A dict implementation with sets of valid and required keys
:param valid_keys: Set of valid keys
:param required_keys: Set of required keys, must be a subset of valid_keys
"""
class MissingKeys(Exception):
def __init__(self, keys):
self.keys = keys
def __init__(self, valid_keys, required_keys):
super().__init__()
self.valid_keys = valid_keys
self.required_keys = set(required_keys)
self.seen_keys = set()
def __setitem__(self, key, value):
"""Trying to add an invalid key will raise KeyError
"""
if key in self.valid_keys:
self.seen_keys.add(key)
self.data[key] = value
else:
raise KeyError(key)
def __delitem__(self, key):
self.seen_keys.remove(key)
del self.data[key]
def __iter__(self):
"""Iteration will raise a MissingKeys exception unless all required keys are set
"""
if self.missing():
raise self.MissingKeys(self.missing())
return self.data.__iter__()
def missing(self):
"""Returns a set of keys that are required but not set
"""
return self.required_keys - (self.seen_keys & self.required_keys)
def convert_position(pos, json):
if pos < 0:
pos = len(json) + (pos + 1)
return pos
def bytes_info_dict(in_bytes):
power = 2**10 # 2 ** 10 == 1024
n = 0
pow_dict = {0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'}
out_bytes = int(in_bytes)
while out_bytes > power:
out_bytes /= power
n += 1
return {
'value': out_bytes,
'unit': '{prefix}B'.format(prefix=pow_dict[n])
}
def flatten(l):
"""
Flattens a hierarchy of nested lists into a single list containing all elements in order
:param l: list of arbitrary types and lists
:returns: list of arbitrary types
"""
l = list(l)
i = 0
while i < len(l):
while isinstance(l[i], list):
if not l[i]:
l.pop(i)
i -= 1
break
else:
l[i:i + 1] = l[i]
i += 1
return l
def formatp(string, **kwargs):
"""
Function for advanced format strings with partial formatting
This function consumes format strings with groups enclosed in brackets. A
group enclosed in brackets will only become part of the result if all fields
inside the group evaluate True in boolean contexts.
Groups can be nested. The fields in a nested group do not count as fields in
the enclosing group, i.e. the enclosing group will evaluate to an empty
string even if a nested group would be eligible for formatting. Nesting is
thus equivalent to a logical or of all enclosing groups with the enclosed
group.
Escaped brackets, i.e. \\\\[ and \\\\] are copied verbatim to output.
:param string: Format string
:param kwargs: keyword arguments providing data for the format string
:returns: Formatted string
"""
def build_stack(string):
"""
Builds a stack with OpeningBracket, ClosingBracket and String tokens.
Tokens have a level property denoting their nesting level.
They also have a string property containing associated text (empty for
all tokens but String tokens).
"""
class Token:
string = ""
class OpeningBracket(Token):
pass
class ClosingBracket(Token):
pass
class String(Token):
def __init__(self, str):
self.string = str
TOKENS = {
"[": OpeningBracket,
"]": ClosingBracket,
}
stack = []
# Index of next unconsumed char
next = 0
# Last consumed char
prev = ""
# Current char
char = ""
# Current level
level = 0
while next < len(string):
prev = char
char = string[next]
next += 1
if prev != "\\" and char in TOKENS:
token = TOKENS[char]()
token.index = next
if char == "]":
level -= 1
token.level = level
if char == "[":
level += 1
stack.append(token)
else:
if stack and isinstance(stack[-1], String):
stack[-1].string += char
else:
token = String(char)
token.level = level
stack.append(token)
return stack
def build_tree(items, level=0):
"""
Builds a list-of-lists tree (in forward order) from a stack (reversed order),
and formats the elements on the fly, discarding everything not eligible for
inclusion.
"""
subtree = []
while items:
nested = []
while items[0].level > level:
nested.append(items.pop(0))
if nested:
subtree.append(build_tree(nested, level + 1))
item = items.pop(0)
if item.string:
string = item.string
if level == 0:
subtree.append(string.format(**kwargs))
else:
fields = re.findall(r"({(\w+)[^}]*})", string)
successful_fields = 0
for fieldspec, fieldname in fields:
if kwargs.get(fieldname, False):
successful_fields += 1
if successful_fields == len(fields):
subtree.append(string.format(**kwargs))
else:
return []
return subtree
def merge_tree(items):
return "".join(flatten(items)).replace(r"\]", "]").replace(r"\[", "[")
stack = build_stack(string)
tree = build_tree(stack, 0)
return merge_tree(tree)
class TimeWrapper:
"""
A wrapper that implements __format__ and __bool__ for time differences and time spans.
:param seconds: seconds (numeric)
:param default_format: the default format to be used if no explicit format_spec is passed to __format__
Format string syntax:
* %h, %m and %s are the hours, minutes and seconds without leading zeros (i.e. 0 to 59 for minutes and seconds)
* %H, %M and %S are padded with a leading zero to two digits, i.e. 00 to 59
* %l and %L produce hours non-padded and padded but only if hours is not zero. If the hours are zero it produces an empty string.
* %% produces a literal %
* %E (only valid on beginning of the string) if the time is null, don't format anything but rather produce an empty string. If the time is non-null it is removed from the string.
The formatted string is stripped, i.e. spaces on both ends of the result are removed
"""
class TimeTemplate(string.Template):
delimiter = "%"
idpattern = r"[a-zA-Z]"
def __init__(self, seconds, default_format="%m:%S"):
self.seconds = int(seconds)
self.default_format = default_format
def __bool__(self):
""":returns: `bool(seconds)`, i.e. False if seconds == 0 and True otherwise
"""
return bool(self.seconds)
def __format__(self, format_spec):
"""Formats the time span given the format_spec (or the default_format).
"""
format_spec = format_spec or self.default_format
h = self.seconds // 3600
m, s = divmod(self.seconds % 3600, 60)
l = h if h else ""
L = "%02d" % h if h else ""
if format_spec.startswith("%E"):
format_spec = format_spec[2:]
if not self.seconds:
return ""
return self.TimeTemplate(format_spec).substitute(
h=h, m=m, s=s,
H="%02d" % h, M="%02d" % m, S="%02d" % s,
l=l, L=L,
).strip()
def require(predicate):
"""Decorator factory for methods requiring a predicate. If the
predicate is not fulfilled during a method call, the method call
is skipped and None is returned.
:param predicate: A callable returning a truth value
:returns: Method decorator
.. seealso::
:py:class:`internet`
"""
def decorator(method):
@functools.wraps(method)
def wrapper(*args, **kwargs):
if predicate():
return method(*args, **kwargs)
return None
return wrapper
return decorator
class internet:
"""
Checks for internet connection by connecting to a server.
This class exposes two configuration variables:
* address - a tuple containing (host,port) of the server to connect to
* check_frequency - the frequency in seconds for checking the connection
:rtype: bool
.. seealso::
:py:func:`require`
"""
address = ('google.com', 80)
check_frequency = 1
dns_cache = []
last_checked = time.perf_counter() - check_frequency
connected = False
def __new__(cls):
if not internet.connected:
internet.dns_cache = internet.resolve()
now = time.perf_counter()
elapsed = now - internet.last_checked
if not internet.connected or elapsed > internet.check_frequency:
internet.last_checked = now
internet.connected = internet.check_connection()
return internet.connected
@staticmethod
def check_connection():
for res in internet.dns_cache:
try:
if internet.check(res):
return True
except OSError:
pass
return False
@staticmethod
def check(res):
af, socktype, proto, canonname, sa = res
sock = None
try:
sock = socket.socket(af, socktype, proto)
sock.settimeout(1)
sock.connect(sa)
sock.close()
return True
except socket.error:
if sock is not None:
sock.close()
raise
@staticmethod
def resolve():
host, port = internet.address
try:
return socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
except socket.gaierror:
return []
def make_graph(values, lower_limit=0.0, upper_limit=100.0, style="blocks"):
"""
Draws a graph made of unicode characters.
:param values: An array of values to graph.
:param lower_limit: Minimum value for the y axis (or None for dynamic).
:param upper_limit: Maximum value for the y axis (or None for dynamic).
:param style: Drawing style ('blocks', 'braille-fill', 'braille-peak', or 'braille-snake').
:returns: Bar as a string
"""
values = [float(n) for n in values]
mn, mx = min(values), max(values)
mn = mn if lower_limit is None else min(mn, float(lower_limit))
mx = mx if upper_limit is None else max(mx, float(upper_limit))
extent = mx - mn
if style == 'blocks':
bar = '_▁▂▃▄▅▆▇█'
bar_count = len(bar) - 1
if extent == 0:
graph = '_' * len(values)
else:
graph = ''.join(bar[int((n - mn) / extent * bar_count)] for n in values)
elif style in ['braille-fill', 'braille-peak', 'braille-snake']:
# idea from https://github.com/asciimoo/drawille
# unicode values from http://en.wikipedia.org/wiki/Braille
vpad = values if len(values) % 2 == 0 else values + [mn]
vscale = [round(4 * (vp - mn) / extent) for vp in vpad]
l = len(vscale) // 2
# do the 2-character collapse separately for clarity
if 'fill' in style:
vbits = [[0, 0x40, 0x44, 0x46, 0x47][vs] for vs in vscale]
elif 'peak' in style:
vbits = [[0, 0x40, 0x04, 0x02, 0x01][vs] for vs in vscale]
else:
assert('snake' in style)
# there are a few choices for what to put last in vb2.
# arguable vscale[-1] from the _previous_ call is best.
vb2 = [vscale[0]] + vscale + [0]
vbits = []
for i in range(1, l + 1):
c = 0
for j in range(min(vb2[i - 1], vb2[i], vb2[i + 1]), vb2[i] + 1):
c |= [0, 0x40, 0x04, 0x02, 0x01][j]
vbits.append(c)
# 2-character collapse
graph = ''
for i in range(0, l, 2):
b1 = vbits[i]
b2 = vbits[i + 1]
if b2 & 0x40:
b2 = b2 - 0x30
b2 = b2 << 3
graph += chr(0x2800 + b1 + b2)
else:
raise NotImplementedError("Graph drawing style '%s' unimplemented." % style)
return graph
def make_vertical_bar(percentage, width=1, glyphs=None):
"""
Draws a vertical bar made of unicode characters.
:param percentage: A value between 0 and 100
:param width: How many characters wide the bar should be.
:returns: Bar as a String
"""
if glyphs is not None:
bar = make_glyph(percentage, lower_bound=0, upper_bound=100, glyphs=glyphs)
else:
bar = make_glyph(percentage, lower_bound=0, upper_bound=100)
return bar * width
def make_bar(percentage):
"""
Draws a bar made of unicode box characters.
:param percentage: A value between 0 and 100
:returns: Bar as a string
"""
bars = [' ', '▏', '▎', '▍', '▌', '▋', '▋', '▊', '▊', '█']
tens = int(percentage / 10)
ones = int(percentage) - tens * 10
result = tens * '█'
if ones >= 1:
result = result + bars[ones]
result = result + (10 - len(result)) * ' '
return result
def make_glyph(number, glyphs=" _▁▂▃▄▅▆▇█", lower_bound=0, upper_bound=100, enable_boundary_glyphs=False):
"""
Returns a single glyph from the list of glyphs provided relative to where
the number is in the range (by default a percentage value is expected).
This can be used to create an icon based representation of a value with an
arbitrary number of glyphs (e.g. 4 different battery status glyphs for
battery percentage level).
:param number: The number being represented. By default a percentage value\
between 0 and 100 (but any range can be defined with lower_bound and\
upper_bound).
:param glyphs: Either a string of glyphs, or an array of strings. Using an array\
of strings allows for additional pango formatting to be applied such that\
different colors could be shown for each glyph).
:param lower_bound: A custom lower bound value for the range.
:param upper_bound: A custom upper bound value for the range.
:param enable_boundary_glyphs: Whether the first and last glyphs should be used\
for the special case of the number being <= lower_bound or >= upper_bound\
respectively.
:returns: The glyph found to represent the number
"""
# Handle edge cases first
if lower_bound >= upper_bound:
raise Exception("Invalid upper/lower bounds")
elif number <= lower_bound:
return glyphs[0]
elif number >= upper_bound:
return glyphs[-1]
if enable_boundary_glyphs:
# Trim first and last items from glyphs as boundary conditions already
# handled
glyphs = glyphs[1:-1]
# Determine a value 0 - 1 that represents the position in the range
adjusted_value = (number - lower_bound) / (upper_bound - lower_bound)
# Determine the closest glyph to show
# As we have positive indices, we can use int for floor rounding
# Adjusted_value should always be < 1
glyph_index = int(len(glyphs) * adjusted_value)
return glyphs[glyph_index]
def user_open(url_or_command):
"""Open the specified paramater in the web browser if a URL is detected,
othewrise pass the paramater to the shell as a subprocess. This function
is inteded to bu used in on_leftclick/on_rightclick callbacks.
:param url_or_command: String containing URL or command
"""
from urllib.parse import urlparse
scheme = urlparse(url_or_command).scheme
if scheme == 'http' or scheme == 'https':
import webbrowser
import os
# webbrowser.open() sometimes prints a message for some reason and confuses i3
# Redirect stdout briefly to prevent this from happening.
savout = os.dup(1)
os.close(1)
os.open(os.devnull, os.O_RDWR)
try:
webbrowser.open(url_or_command)
finally:
os.dup2(savout, 1)
else:
import subprocess
subprocess.Popen(url_or_command, shell=True)
class MultiClickHandler(object):
def __init__(self, callback_handler, timeout):
self.callback_handler = callback_handler
self.timeout = timeout
self.lock = RLock()
self._timer_id = 0
self.timer = None
self.button = None
self.cb = None
self.kwargs = None
def set_timer(self, button, cb, **kwargs):
with self.lock:
self.clear_timer()
self.timer = Timer(self.timeout,
self._timer_function,
args=[self._timer_id])
self.button = button
self.cb = cb
self.kwargs = kwargs
self.timer.start()
def clear_timer(self):
with self.lock:
if self.timer is None:
return
self._timer_id += 1 # Invalidate existent timer
self.timer.cancel() # Cancel the existent timer
self.timer = None
self.button = None
self.cb = None
def _timer_function(self, timer_id):
with self.lock:
if self._timer_id != timer_id:
return
self.callback_handler(self.button, self.cb, **self.kwargs)
self.clear_timer()
def check_double(self, button):
if self.timer is None:
return False
ret = True
if button != self.button:
self.callback_handler(self.button, self.cb, **self.kwargs)
ret = False
self.clear_timer()
return ret
def get_module(function):
"""Function decorator for retrieving the ``self`` argument from the stack.
Intended for use with callbacks that need access to a modules variables, for example:
.. code:: python
from i3pystatus import Status, get_module
from i3pystatus.core.command import execute
status = Status(...)
# other modules etc.
@get_module
def display_ip_verbose(module):
execute('sh -c "ip addr show dev {dev} | xmessage -file -"'.format(dev=module.interface))
status.register("network", interface="wlan1", on_leftclick=display_ip_verbose)
"""
@functools.wraps(function)
def call_wrapper(*args, **kwargs):
stack = inspect.stack()
caller_frame_info = stack[1]
self = caller_frame_info[0].f_locals["self"]
# not completly sure whether this is necessary
# see note in Python docs about stack frames
del stack
function(self, *args, **kwargs)
return call_wrapper
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/cpu_freq.py 0000664 0000000 0000000 00000005536 13567273623 0022672 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
class CpuFreq(IntervalModule):
"""
class uses by default `/proc/cpuinfo` to determine the current cpu frequency
.. rubric:: Available formatters
* `{avg}` - mean from all cores in MHz `4.3f`
* `{avgg}` - mean from all cores in GHz `1.2f`
* `{coreX}` - frequency of core number `X` in MHz (format `4.3f`), where 0 <= `X` <= number of cores - 1
* `{coreXg}` - frequency of core number `X` in GHz (fromat `1.2f`), where 0 <= `X` <= number of cores - 1
"""
format = "{avgg}"
settings = (
"format",
("color", "The text color"),
("file", "override default path"),
)
file = '/proc/cpuinfo'
color = '#FFFFFF'
def createvaluesdict(self):
"""
function processes the /proc/cpuinfo file, use file=/sys to use kernel >=4.13 location
:return: dictionary used as the full-text output for the module
"""
cpus_offline = 0
if self.file == '/sys':
with open('/sys/devices/system/cpu/online') as f:
line = f.readline()
cpus_online = [int(cpu) for cpu in line.split(',') if cpu.find('-') < 0]
cpus_online_range = [cpu_range for cpu_range in line.split(',') if cpu_range.find('-') > 0]
for cpu_range in cpus_online_range:
cpus_online += [cpu for cpu in range(int(cpu_range.split('-')[0]), int(cpu_range.split('-')[1]) + 1)]
mhz_values = [0.0 for cpu in range(max(cpus_online) + 1)]
ghz_values = [0.0 for cpu in range(max(cpus_online) + 1)]
for cpu in cpus_online:
with open('/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq'.format(cpu)) as f:
line = f.readline()
mhz_values[cpu] = float(line.rstrip()) / 1000.0
ghz_values[cpu] = float(line.rstrip()) / 1000000.0
cpus_offline = mhz_values.count(0.0)
else:
with open(self.file) as f:
mhz_values = [float(line.split(':')[1]) for line in f if line.startswith('cpu MHz')]
ghz_values = [value / 1000.0 for value in mhz_values]
mhz = {"core{}".format(key): "{0:4.3f}".format(value) for key, value in enumerate(mhz_values)}
ghz = {"core{}g".format(key): "{0:1.2f}".format(value) for key, value in enumerate(ghz_values)}
cdict = mhz.copy()
cdict.update(ghz)
cdict['avg'] = "{0:4.3f}".format(sum(mhz_values) / (len(mhz_values) - cpus_offline))
cdict['avgg'] = "{0:1.2f}".format(sum(ghz_values) / (len(ghz_values) - cpus_offline), 2)
return cdict
def run(self):
cdict = self.createvaluesdict()
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": self.color,
"format": self.format,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/cpu_usage.py 0000664 0000000 0000000 00000010744 13567273623 0023036 0 ustar 00root root 0000000 0000000 from collections import defaultdict
from string import Formatter
import re
from i3pystatus import IntervalModule
from i3pystatus.core.color import ColorRangeModule
try:
from natsort import natsorted as sorted
except ImportError:
pass
class CpuUsage(IntervalModule, ColorRangeModule):
"""
Shows CPU usage.
The first output will be inacurate.
Linux only
Requires the PyPI package 'colour'.
.. rubric:: Available formatters
* `{usage}` — usage average of all cores
* `{usage_cpu*}` — usage of one specific core. replace "*" by core number starting at 0
* `{usage_all}` — usage of all cores separate. usess natsort when available(relevant for more than 10 cores)
"""
format = "{usage:02}%"
format_all = "{core}:{usage:02}%"
exclude_average = False
interval = 1
color = '#FFFFFF'
dynamic_color = False
upper_limit = 100
settings = (
("format", "format string."),
("format_all", ("format string used for {usage_all} per core. "
"Available formaters are {core} and {usage}. ")),
("exclude_average", ("If True usage average of all cores will "
"not be in format_all.")),
("color", "HTML color code #RRGGBB"),
("dynamic_color", "Set color dynamically based on CPU usage. Note: this overrides color_up"),
("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"),
("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'")
)
def init(self):
self.prev_total = defaultdict(int)
self.prev_busy = defaultdict(int)
self.formatter = Formatter()
self.key = re.findall(r'usage_cpu\d+', self.format)
if len(self.key) == 1:
self.key = self.key[0]
else:
self.key = 'usage_cpu'
if not self.dynamic_color:
self.start_color = self.color
self.end_color = self.color
self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(self.upper_limit))
def get_cpu_timings(self):
"""
reads and parses /proc/stat
returns dictionary with all available cores including global average
"""
timings = {}
with open('/proc/stat', 'r') as file_obj:
for line in file_obj:
if 'cpu' in line:
line = line.strip().split()
timings[line[0]] = [int(x) for x in line[1:]]
return timings
def calculate_usage(self, cpu, total, busy):
"""
calculates usage
"""
diff_total = total - self.prev_total[cpu]
diff_busy = busy - self.prev_busy[cpu]
self.prev_total[cpu] = total
self.prev_busy[cpu] = busy
if diff_total == 0:
return 0
else:
return int(diff_busy / diff_total * 100)
def gen_format_all(self, usage):
"""
generates string for format all
"""
format_string = " "
core_strings = []
for core, usage in usage.items():
if core == 'usage_cpu' and self.exclude_average:
continue
elif core == 'usage':
continue
core = core.replace('usage_', '')
string = self.formatter.format(self.format_all,
core=core,
usage=usage)
core_strings.append(string)
core_strings = sorted(core_strings)
return format_string.join(core_strings)
def get_usage(self):
"""
parses /proc/stat and calcualtes total and busy time
(more specific USER_HZ see man 5 proc for further informations )
"""
usage = {}
for cpu, timings in self.get_cpu_timings().items():
cpu_total = sum(timings)
del timings[3:5]
cpu_busy = sum(timings)
cpu_usage = self.calculate_usage(cpu, cpu_total, cpu_busy)
usage['usage_' + cpu] = cpu_usage
# for backward compatibility
usage['usage'] = usage['usage_cpu']
return usage
def run(self):
usage = self.get_usage()
usage['usage_all'] = self.gen_format_all(usage)
color = self.get_gradient(usage[self.key], self.colors, int(self.upper_limit))
self.data = usage
self.output = {
"full_text": self.format.format_map(usage),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/cpu_usage_bar.py 0000664 0000000 0000000 00000004166 13567273623 0023663 0 ustar 00root root 0000000 0000000 from i3pystatus.core.color import ColorRangeModule
from i3pystatus.cpu_usage import CpuUsage
from i3pystatus.core.util import make_bar, make_vertical_bar
class CpuUsageBar(CpuUsage, ColorRangeModule):
"""
Shows CPU usage as a bar (made with unicode box characters).
The first output will be inacurate.
Linux only
Requires the PyPI package `colour`.
.. rubric:: Available formatters
* `{usage_bar}` — usage average of all cores
* `{usage_bar_cpu*}` — usage of one specific core. replace "*" by core number starting at 0
"""
format = "{usage_bar}"
bar_type = 'horizontal'
cpu = 'usage_cpu'
settings = (
("format", "format string"),
("bar_type", "whether the bar should be vertical or horizontal. "
"Allowed values: `vertical` or `horizontal`"),
("cpu", "cpu to base the colors on. Choices are 'usage_cpu' for all or 'usage_cpu*'."
" Replace '*' by core number starting at 0."),
("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"),
("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'")
)
def init(self):
super().init()
self.colors = self.get_hex_color_range(self.start_color, self.end_color, 100)
def run(self):
cpu_usage = self.get_usage()
cpu_usage_bar = {}
for core, usage in cpu_usage.items():
core = core.replace('usage', 'usage_bar')
if self.bar_type == 'horizontal':
cpu_usage_bar[core] = make_bar(usage)
elif self.bar_type == 'vertical':
cpu_usage_bar[core] = make_vertical_bar(usage)
else:
raise Exception("bar_type must be 'horizontal' or 'vertical'!")
cpu_usage.update(cpu_usage_bar)
# for backward compatibility
cpu_usage['usage_bar'] = cpu_usage['usage_bar_cpu']
self.data = cpu_usage
self.output = {
"full_text": self.format.format_map(cpu_usage),
'color': self.get_gradient(cpu_usage[self.cpu], self.colors, 100)
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/cpu_usage_graph.py 0000664 0000000 0000000 00000004645 13567273623 0024222 0 ustar 00root root 0000000 0000000 from i3pystatus.core.color import ColorRangeModule
from i3pystatus.cpu_usage import CpuUsage
from i3pystatus.core.util import make_graph
class CpuUsageGraph(CpuUsage, ColorRangeModule):
"""
Shows CPU usage as a Unicode graph.
The first output will be inacurate.
Depends on the PyPI colour module - https://pypi.python.org/pypi/colour/0.0.5
Linux only
.. rubric:: Available formatters
* `{cpu_graph}` — graph of cpu usage.
* `{usage}` — usage average of all cores
* `{usage_cpu*}` — usage of one specific core. replace "*" by core number starting at 0
* `{usage_all}` — usage of all cores separate. usess natsort when available(relevant for more than 10 cores)
"""
settings = (
("cpu", "cpu to monitor, choices are 'usage_cpu' for all or 'usage_cpu*'. R"
"eplace '*' by core number starting at 0."),
("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"),
("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'"),
("graph_width", "Width of the cpu usage graph"),
("graph_style", "Graph style ('blocks', 'braille-fill', 'braille-peak', or 'braille-snake')"),
("direction", "Graph running direction ('left-to-right', 'right-to-left')"),
)
graph_width = 15
graph_style = 'blocks'
format = '{cpu_graph}'
cpu = 'usage_cpu'
direction = 'left-to-right'
def init(self):
super().init()
self.cpu_readings = self.graph_width * [0]
self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(100))
def run(self):
format_options = self.get_usage()
core_reading = format_options[self.cpu]
self.cpu_readings.insert(0, core_reading)
self.cpu_readings = self.cpu_readings[:self.graph_width]
graph = make_graph(self.cpu_readings, 0.0, 100.0, self.graph_style)
if self.direction == "right-to-left":
graph = graph[::-1]
elif self.direction == "left-to-right":
pass
else:
raise Exception("Invalid direction '%s'." % self.direction)
format_options.update({'cpu_graph': graph})
color = self.get_gradient(core_reading, self.colors)
self.data = format_options
self.output = {
"full_text": self.format.format_map(format_options),
'color': color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/deluge.py 0000664 0000000 0000000 00000010144 13567273623 0022322 0 ustar 00root root 0000000 0000000 import time
from deluge_client import DelugeRPCClient, FailedToReconnectException
from i3pystatus import IntervalModule, logger
from i3pystatus.core.util import bytes_info_dict
class Deluge(IntervalModule):
"""
Deluge torrent module
Requires `deluge-client`
.. rubric:: Formatters:
* `{num_torrents}` - number of torrents in deluge
* `{free_space_bytes}` - bytes free in path
* `{used_space_bytes}` - bytes used in path
* `{upload_rate}` - bytes sent per second
* `{download_rate}` - bytes received per second
* `{total_uploaded}` - bytes sent total
* `{total_downloaded}` - bytes received total
"""
settings = (
'format',
'color',
('rounding', 'number of decimal places to round numbers too'),
('host', 'address of deluge server (default: 127.0.0.1)'),
('port', 'port of deluge server (default: 58846)'),
('username', 'username to authenticate with deluge'),
('password', 'password to authenticate to deluge'),
('path', 'override "download path" server-side when checking space used/free'),
('offline_string', 'string to output while unable to connect to deluge daemon')
)
required = ('username', 'password')
host = '127.0.0.1'
port = 58846
path = None
color = None
libtorrent_stats = False
rounding = 2
offline_string = 'offline'
format = '⛆{num_torrents} ✇{free_space_bytes}'
id = int(time.time()) # something random
def init(self):
self.client = DelugeRPCClient(self.host, self.port, self.username, self.password)
self.data = {}
def run(self):
if not self.client.connected:
try:
self.client.connect()
except OSError:
self.output = {
'full_text': self.offline_string
}
return
try:
self.data = self.get_session_statistics()
torrents = self.get_torrents_status()
if torrents:
self.data['num_torrents'] = len(torrents)
if 'free_space_bytes' in self.format:
self.data['free_space_bytes'] = self.get_free_space(self.path)
if 'used_space_bytes' in self.format:
self.data['used_space_bytes'] = self.get_path_size(self.path)
except FailedToReconnectException:
return
self.parse_values(self.data)
self.output = {
'full_text': self.format.format(**self.data)
}
if self.color:
self.output['color'] = self.color
def parse_values(self, values):
for k, v in values.items():
if v:
if k in ['total_upload', 'total_download', 'download_rate', 'upload_rate'] or k.endswith('_bytes'):
values[k] = '{value:.{round}f}{unit}'.format(round=self.rounding, **bytes_info_dict(v))
def get_path_size(self, path=None):
"""
get used space of path in bytes (default: download location)
"""
if path is None:
path = []
return self.client.call('core.get_path_size', path)
def get_free_space(self, path=None):
"""
get free space of path in bytes (default: download location)
"""
if path is None:
path = []
return self.client.call('core.get_free_space', path)
def get_torrents_status(self, torrent_id=None, keys=None):
if torrent_id is None:
torrent_id = []
if keys is None:
keys = []
return self.client.call('core.get_torrents_status', torrent_id, keys)
def get_session_statistics(self):
keys = ['upload_rate', 'download_rate', 'total_upload', 'total_download']
out = {} # some of the values from deluge-client are bytes, the others are ints - we need to decode them
for k, v in self.client.call('core.get_session_status', keys).items():
k = k.decode('utf-8') # keys aswell
if type(v) == bytes:
out[k] = v.decode('utf-8')
else:
out[k] = v
return out
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/disk.py 0000664 0000000 0000000 00000005447 13567273623 0022021 0 ustar 00root root 0000000 0000000 import os
from i3pystatus import IntervalModule
from .core.util import round_dict
class Disk(IntervalModule):
"""
Gets ``{used}``, ``{free}``, ``{avail}`` and ``{total}`` amount of bytes on the given mounted filesystem.
These values can also be expressed as percentages with the ``{percentage_used}``, ``{percentage_free}``
and ``{percentage_avail}`` formats.
"""
settings = (
"format",
"path",
("divisor", "divide all byte values by this value, default is 1024**3 (gigabyte)"),
("display_limit", "if more space is available than this limit the module is hidden"),
("critical_limit", "critical space limit (see critical_color)"),
("critical_color", "the critical color"),
("color", "the common color"),
("round_size", "precision, None for INT"),
("mounted_only", "display only if path is a valid mountpoint"),
"format_not_mounted",
"color_not_mounted"
)
required = ("path",)
color = "#FFFFFF"
color_not_mounted = "#FFFFFF"
critical_color = "#FF0000"
format = "{free}/{avail}"
format_not_mounted = None
divisor = 1024 ** 3
display_limit = float('Inf')
critical_limit = 0
round_size = 2
mounted_only = False
def not_mounted(self):
if self.mounted_only:
self.output = {}
else:
self.output = {} if not self.format_not_mounted else {
"full_text": self.format_not_mounted,
"color": self.color_not_mounted,
}
def run(self):
if os.path.isdir(self.path) and not os.path.ismount(self.path):
if len(os.listdir(self.path)) == 0:
self.not_mounted()
return
try:
stat = os.statvfs(self.path)
except Exception:
self.not_mounted()
return
available = (stat.f_bsize * stat.f_bavail) / self.divisor
if available > self.display_limit:
self.output = {}
return
critical = available < self.critical_limit
cdict = {
"total": (stat.f_bsize * stat.f_blocks) / self.divisor,
"free": (stat.f_bsize * stat.f_bfree) / self.divisor,
"avail": available,
"used": (stat.f_bsize * (stat.f_blocks - stat.f_bfree)) / self.divisor,
"percentage_free": stat.f_bfree / stat.f_blocks * 100,
"percentage_avail": stat.f_bavail / stat.f_blocks * 100,
"percentage_used": (stat.f_blocks - stat.f_bfree) / stat.f_blocks * 100,
}
round_dict(cdict, self.round_size)
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": self.critical_color if critical else self.color,
"urgent": critical
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/dota2wins.py 0000664 0000000 0000000 00000010044 13567273623 0022766 0 ustar 00root root 0000000 0000000 from dota2py import api
from i3pystatus import IntervalModule
class Dota2wins(IntervalModule):
"""
Displays the win/loss ratio of a given Dota account.
Requires: dota2py
"""
settings = (
("matches", "Number of recent matches to calculate"),
("steamid", "Steam ID or username to track"),
("steam_api_key", "Steam API key "
"(http://steamcommunity.com/dev/apikey)"),
("good_threshold", "Win percentage (or higher) which you are happy "
"with"),
("bad_threshold", "Win percentage you want to be alerted (difference "
"between good_threshold and bad_threshold is cautious_threshold)"),
("interval", "Update interval (games usually last at least 20 min)."),
("good_color", "Color of text while win percentage is above "
"good_threshold"),
("bad_color", "Color of text while win percentage is below "
"bad_threshold"),
("caution_color", "Color of text while win precentage is between good "
"and bad thresholds"),
("screenname", "If set to 'retrieve', requests for the users's "
"screenname via API calls. Else, use the supplied string as the "
"user's screename"),
"format"
)
required = ("steamid", "steam_api_key")
good_color = "#00FF00" # green
caution_color = "#FFFF00" # yellow
bad_color = "#FF0000" # red
good_threshold = 50
bad_threshold = 45
matches = 25
interval = 1800
screenname = 'retrieve'
format = "{screenname} {wins}W:{losses}L {win_percent:.2f}%"
def run(self):
api.set_api_key(self.steam_api_key)
if not isinstance(self.steamid, int):
# find by username
self.steamid = int(api.get_steam_id(self.steamid)['response']['steamid'])
hist = api.get_match_history(account_id=self.steamid)['result']
recent_matches = []
while len(recent_matches) < self.matches:
recent_matches.append(hist['matches'].pop(0))
player_team_per_match = []
# create a list of tuples where each tuple is:
# [match_id, bool]
# The bool will be true if the player is on Radiant and alse if they
# are on Dire.
for match in recent_matches:
this_match = [match['match_id']]
for player in match['players']:
# 64bit player ID
long_id = player['account_id'] + 76561197960265728
if long_id == self.steamid:
if player['player_slot'] < 128:
this_match.append(True)
else:
this_match.append(False)
player_team_per_match.append(this_match)
outcomes = []
for match in player_team_per_match:
if api.get_match_details(match[0])['result']['radiant_win'] == match[1]:
outcomes.append(1)
else:
outcomes.append(0)
wins = outcomes.count(1)
losses = outcomes.count(0)
win_percent = float(sum(outcomes) / float(len(outcomes))) * 100
if win_percent >= float(self.good_threshold):
color = self.good_color
elif win_percent <= float(self.bad_threshold):
color = self.bad_color
else:
color = self.caution_color
if self.screenname == 'retrieve':
from urllib.request import urlopen
import json
response = urlopen(
'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s' %
(self.steam_api_key, self.steamid))
screenname = json.loads(bytes.decode(response.read()))['response']['players'][0]['personaname']
else:
screenname = self.screenname
cdict = {
"screenname": screenname,
"wins": wins,
"losses": losses,
"win_percent": win_percent,
}
self.output = {
"full_text": self.format.format(**cdict),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/dpms.py 0000664 0000000 0000000 00000002405 13567273623 0022021 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.command import run_through_shell
class DPMS(IntervalModule):
"""
Shows and toggles status of DPMS which prevents screen from blanking.
.. rubric:: Available formatters
* `{status}` — the current status of DPMS
@author Georg Sieber
"""
interval = 5
settings = (
"format",
"format_disabled",
"color",
"color_disabled",
)
color_disabled = "#AAAAAA"
color = "#FFFFFF"
format = "DPMS: {status}"
format_disabled = "DPMS: {status}"
on_leftclick = "toggle_dpms"
status = False
def run(self):
self.status = run_through_shell("xset -q | grep -q 'DPMS is Enabled'", True).rc == 0
if self.status:
self.output = {
"full_text": self.format.format(status="on"),
"color": self.color
}
else:
self.output = {
"full_text": self.format_disabled.format(status="off"),
"color": self.color_disabled
}
def toggle_dpms(self):
if self.status:
run_through_shell("xset -dpms s off", True)
else:
run_through_shell("xset +dpms s on", True)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/exmo.py 0000664 0000000 0000000 00000005514 13567273623 0022032 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import requests
from i3pystatus import IntervalModule, formatp
from i3pystatus.core.util import internet, require, user_open
class Exmo(IntervalModule):
"""
This module fetching and displays exchange rates with EXMO.
Using API .
.. rubric:: Available formatters
* {buy_price}
* {status}
* {pair}
"""
settings = (
('format', 'Format string used for output'),
('pair', 'Currency pair for display on output'),
('color', 'Standard color'),
('colorize', 'Enable color change on price increase/decrease'),
('color_up', 'Color for price increases'),
('color_down', 'Color for price decreases'),
('interval', 'Update interval.'),
'status'
)
format = '{buy_price}[ {status}] {pair}'
pair = 'BTC_USD'
color = '#FFFFFF'
colorize = False
color_up = '#00FF00'
color_down = '#FF0000'
interval = 60
status = {
'price_up': '▲',
'price_down': '▼',
}
_prev_price = 0
_prev_status = ''
_prev_color = '#FFFFFF'
def __init__(self, *args, **kwargs):
super(Exmo, self).__init__(*args, **kwargs)
self.on_leftclick = [
'open_something',
'https://exmo.me/ru/trade#?pair={}'.format(self.pair)
]
def fetch_data(self):
response = requests.get('https://api.exmo.com/v1/ticker/')
return response.json()
@require(internet)
def run(self):
try:
price_data = self.fetch_data().get(self.pair)
except Exception as e:
self.output = {
'full_text': 'Failed fetching data from server: ' + str(e),
'color': '#FF0000'
}
return
fdict = {
'pair': self.pair.replace('_', '/'),
'buy_price': price_data.get('buy_price', 0),
'status': ''
}
color = self.color
if self._prev_price and fdict['buy_price'] > self._prev_price:
color = self.color_up
fdict['status'] = self.status['price_up']
elif self._prev_price and fdict['buy_price'] < self._prev_price:
color = self.color_down
fdict['status'] = self.status['price_down']
else:
color = self._prev_color
fdict['status'] = self._prev_status
self._prev_price = price_data.get('buy_price', 0)
self._prev_status = fdict['status']
self._prev_color = color
if not self.colorize:
color = self.color
self.output = {
'full_text': formatp(self.format, **fdict).strip(),
'color': color
}
def open_something(self, url_or_command):
"""
Wrapper function, to pass the arguments to user_open
"""
user_open(url_or_command)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/external_ip.py 0000664 0000000 0000000 00000005047 13567273623 0023375 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule, formatp
from i3pystatus.core.util import internet, require
import GeoIP
import urllib.request
class ExternalIP(IntervalModule):
"""
Shows the external IP with the country code/name.
Requires the PyPI package `GeoIP`.
.. rubric:: Available formatters
* {country_name} the full name of the country from the IP (eg. 'United States')
* {country_code} the country code of the country from the IP (eg. 'US')
* {ip} the ip
"""
interval = 15
settings = (
"format",
"color",
("color_down", "color when the http request failed"),
("color_hide", "color when the user has decide to switch to the hide format"),
("format_down", "format when the http request failed"),
("format_hide", "format when the user has decide to switch to the hide format"),
("ip_website", "http website where the IP is directly available as raw"),
("timeout", "timeout in seconds when the http request is taking too much time"),
)
format = "{country_name} {country_code} {ip}"
format_hide = "{country_code}"
format_down = "Timeout"
ip_website = "https://api.ipify.org"
timeout = 5
color = "#FFFFFF"
color_hide = "#FFFF00"
color_down = "#FF0000"
on_leftclick = "switch_hide"
on_rightclick = "run"
@require(internet)
def get_external_ip(self):
try:
request = urllib.request.urlopen(self.ip_website,
timeout=self.timeout)
return request.read().decode().strip()
except Exception:
return None
def run(self):
ip = self.get_external_ip()
if not ip:
return self.disable()
gi = GeoIP.GeoIP(GeoIP.GEOIP_STANDARD)
country_code = gi.country_code_by_addr(ip)
country_name = gi.country_name_by_addr(ip)
if not country_code:
return self.disable() # fail here in the case of a bad IP
fdict = {
"country_name": country_name,
"country_code": country_code,
"ip": ip
}
self.output = {
"full_text": formatp(self.format, **fdict).strip(),
"color": self.color
}
def disable(self):
self.output = {
"full_text": self.format_down,
"color": self.color_down
}
def switch_hide(self):
self.format, self.format_hide = self.format_hide, self.format
self.color, self.color_hide = self.color_hide, self.color
self.run()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/file.py 0000664 0000000 0000000 00000002562 13567273623 0022001 0 ustar 00root root 0000000 0000000 from os.path import join
from i3pystatus import IntervalModule
class File(IntervalModule):
"""
Rip information from text files
components is a dict of pairs of the form:
::
name => (callable, file)
* Where `name` is a valid identifier, which is used in the format string to access
the value of that component.
* `callable` is some callable to convert the contents of `file`. A common choice is
float or int.
* `file` names a file, relative to `base_path`.
transforms is a optional dict of callables taking a single argument (a dictionary containing the values
of all components). The return value is bound to the key.
"""
settings = (
"format",
"components",
"transforms",
"base_path",
"color", "interval",
)
required = ("format", "components")
base_path = "/"
transforms = {}
color = "#FFFFFF"
def run(self):
cdict = {}
for key, (component, file) in self.components.items():
with open(join(self.base_path, file), "r") as f:
cdict[key] = component(f.read().strip())
for key, transform in self.transforms.items():
cdict[key] = transform(cdict)
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/github.py 0000664 0000000 0000000 00000063003 13567273623 0022341 0 ustar 00root root 0000000 0000000 import copy
import json
import re
import threading
import time
from urllib.request import urlopen
from i3pystatus import IntervalModule, formatp
from i3pystatus.core import ConfigError
from i3pystatus.core.desktop import DesktopNotification
from i3pystatus.core.util import user_open, internet, require
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
API_METHODS_URL = 'https://www.githubstatus.com/api/v2/summary.json'
STATUS_URL = 'https://www.githubstatus.com'
NOTIFICATIONS_URL = 'https://github.com/notifications'
ACCESS_TOKEN_AUTH_URL = 'https://api.github.com/notifications?access_token=%s'
BASIC_AUTH_URL = 'https://api.github.com/notifications'
class Github(IntervalModule):
'''
This module checks the GitHub system status, and optionally the number of
unread notifications.
.. versionchanged:: 3.36
Module now checks system status in addition to unread notifications.
.. note::
For notification checking, the following is required:
- The requests_ module must be installed.
- Either ``access_token`` (recommended) or ``username`` and
``password`` must be used to authenticate to GitHub.
Using an access token is the recommended authentication method. Click
here__ to generate a new access token. Fill in the **Token
description** box, and enable the **notifications** scope by checking
the appropriate checkbox. Then, click the **Generate token** button.
.. important::
An access token is the only supported means of authentication for
this module, if `2-factor authentication`_ is enabled.
.. _requests: https://pypi.python.org/pypi/requests
.. __: https://github.com/settings/tokens/new
.. _`2-factor authentication`: https://help.github.com/articles/about-two-factor-authentication/
See here__ for more information on GitHub's authentication API.
.. __: https://developer.github.com/v3/#authentication
If you would rather use a username and password pair, you can either
pass them as arguments when registering the module, or use i3pystatus'
:ref:`credential management ` support to store them in a
keyring. Keep in mind that if you do not pass a ``username`` or
``password`` parameter when registering the module, i3pystatus will
still attempt to retrieve these values from a keyring if the keyring_
Python module is installed. This could result in i3pystatus aborting
during startup if it cannot find a usable keyring backend. If you do
not plan to use credential management at all in i3pystatus, then you
should either ensure that A) keyring_ is not installed, or B) both
keyring_ and keyrings.alt_ are installed, to avoid this error.
.. _keyring: https://pypi.python.org/pypi/keyring
.. _keyrings.alt: https://pypi.python.org/pypi/keyrings.alt
.. rubric:: Available formatters
* `{status}` — Current GitHub status. This formatter can be different
depending on the current outage status (``none``, ``minor``, ``major``,
or ``critical``). The content displayed for each of these statuses is
defined in the **status** config option.
* `{unread}` — When there are unread notifications, this formatter will
contain the value of the **unread_marker** marker config option.
there are no unread notifications, it formatter will be an empty string.
* `{unread_count}` — The number of unread notifications
notifications, it will be an empty string.
* `{update_error}` — When an error is encountered updating this module,
this formatter will be set to the value of the **update_error**
config option.
.. rubric:: Click events
This module responds to 4 different click events:
- **Left-click** — Forces an update of the module.
- **Right-click** — Triggers a desktop notification showing the most recent
update to the GitHub status. This is useful when the status changes when
you are away from your computer, so that the updated status can be seen
without visiting the `GitHub Status Dashboard`_. This click event
requires **notify_status** to be set to ``True``.
- **Double left-click** — Opens the GitHub `notifications page`_ in your web
browser.
- **Double right-click** — Opens the `GitHub Status Dashboard`_ in your web
browser.
.. rubric:: Desktop notifications
.. versionadded:: 3.36
If **notify_status** is set to ``True``, a notification will be displayed
when the status reported by the `GitHub Status API`_ changes.
If **notify_unread** is set to ``True``, a notification will be displayed
when new unread notifications are found. Double-clicking the module will
launch the GitHub notifications dashboard in your browser.
.. note::
A notification will be displayed if there was a problem querying the
`GitHub Status API`_, irrespective of whether or not **notify_status**
or **notify_unread** is set to ``True``.
.. rubric:: Example configuration
The below example enables desktop notifications, enables Pango hinting for
differently-colored **update_error** and **refresh_icon** text, and alters
the both the status text and the colors used to visually denote the current
status level. It also sets the log level to debug, for troubleshooting
purposes.
.. code-block:: python
status.register(
'github',
log_level=logging.DEBUG,
notify_status=True,
notify_unread=True,
access_token='0123456789abcdef0123456789abcdef01234567',
hints={'markup': 'pango'},
update_error='!',
refresh_icon='⟳',
status={
'none': '✓',
'minor': '!',
'major': '!!',
'critical': '!!!',
},
colors={
'none': '#008700',
'minor': '#d7ff00',
'major': '#af0000',
'critical': '#640000',
},
)
.. note::
Setting debug logging and authenticating with an access token will
include the access token in the log file, as the notification URL is
logged at this level.
.. _`GitHub Status API`: https://www.githubstatus.com/api
.. _`GitHub Status Dashboard`: https://www.githubstatus.com/
.. _`notifications page`: https://github.com/notifications
.. rubric:: Extended string formatting
.. versionadded:: 3.36
This module supports the :ref:`formatp ` extended string format
syntax. This allows for values to be hidden when they evaluate as False.
The default ``format`` string value for this module makes use of this
syntax to conditionally show the value of the ``update_error`` config value
when the backend encounters an error during an update, but this can also
be used to only show the number of unread notifications when that number is
not **0**. The below example would show the unread count as **(3)** when
there are 3 unread notifications, but would show nothing when there are no
unread notifications.
.. code-block:: python
status.register(
'github',
notify_status=True,
notify_unread=True,
access_token='0123456789abcdef0123456789abcdef01234567',
format='{status}[ ({unread_count})][ {update_error}]'
)
'''
settings = (
('format', 'format string'),
('status', 'Dictionary mapping statuses to the text which represents '
'that status type. This defaults to ``GitHub`` for all '
'status types.'),
('colors', 'Dictionary mapping statuses to the color used to display '
'the status text'),
('refresh_icon', 'Text to display (in addition to any text currently '
'shown by the module) when refreshing the GitHub '
'status. **NOTE:** Depending on how quickly the '
'update is performed, the icon may not be displayed.'),
('update_error', 'Value for the ``{update_error}`` formatter when an '
'error is encountered while checking GitHub status'),
('keyring_backend', 'alternative keyring backend for retrieving '
'credentials'),
('username', ''),
('password', ''),
('access_token', ''),
('unread_marker', 'Defines the string that the ``{unread}`` formatter '
'shows when there are pending notifications'),
('notify_status', 'Set to ``True`` to display a desktop notification '
'on status changes'),
('notify_unread', 'Set to ``True`` to display a desktop notification '
'when new notifications are detected'),
('unread_notification_template',
'String with no more than one ``%d``, which will be replaced by '
'the number of new unread notifications. Useful for those with '
'non-English locales who would like the notification to be in '
'their native language. The ``%d`` can be omitted if desired.'),
('api_methods_url', 'URL from which to retrieve the API endpoint URL '
'which this module will use to check the GitHub '
'Status'),
('status_url', 'The URL to the status page (opened when the module is '
'double-clicked with the right mouse button'),
('notifications_url', 'The URL to the GitHub notifications page '
'(opened when the module is double-clicked with '
'the left mouse button'),
)
# Defaults for module configurables
_default_status = {
'none': 'GitHub',
'minor': 'GitHub',
'major': 'GitHub',
'critical': 'GitHub',
}
_default_colors = {
'none': '#2ecc71',
'minor': '#f1c40f',
'major': '#e67e22',
'critical': '#e74c3c',
}
# Module configurables
format = '{status}[ {unread}][ {update_error}]'
status = _default_status
colors = _default_colors
refresh_icon = '⟳'
update_error = '!'
username = ''
password = ''
access_token = ''
unread_marker = '•'
notify_status = False
notify_unread = False
unread_notification_template = 'You have %d new notification(s)'
api_methods_url = API_METHODS_URL
status_url = STATUS_URL
notifications_url = NOTIFICATIONS_URL
# Global configurables
interval = 600
max_error_len = 50
keyring_backend = None
# Other
unread = ''
unknown_color = None
unknown_status = '?'
failed_update = False
__previous_json = None
__current_json = None
new_unread = None
previous_unread = None
current_unread = None
config_error = None
data = {'status': '',
'unread': 0,
'unread_count': '',
'update_error': ''}
output = {'full_text': '', 'color': None}
# Click events
on_leftclick = ['perform_update']
on_rightclick = ['show_status_notification']
on_doubleleftclick = ['launch_notifications_url']
on_doublerightclick = ['launch_status_url']
@require(internet)
def launch_status_url(self):
self.logger.debug('Launching %s in browser', self.status_url)
user_open(self.status_url)
@require(internet)
def launch_notifications_url(self):
self.logger.debug('Launching %s in browser', self.notifications_url)
user_open(self.notifications_url)
def init(self):
if self.status != self._default_status:
new_status = copy.copy(self._default_status)
new_status.update(self.status)
self.status = new_status
if self.colors != self._default_colors:
new_colors = copy.copy(self._default_colors)
new_colors.update(self.colors)
self.colors = new_colors
self.logger.debug('status = %s', self.status)
self.logger.debug('colors = %s', self.colors)
self.condition = threading.Condition()
self.thread = threading.Thread(target=self.update_loop, daemon=True)
self.thread.start()
def update_loop(self):
try:
self.perform_update()
while True:
with self.condition:
self.condition.wait(self.interval)
self.perform_update()
except Exception:
msg = 'Exception in {thread} at {time}, module {name}'.format(
thread=threading.current_thread().name,
time=time.strftime('%c'),
name=self.__class__.__name__,
)
self.logger.error(msg, exc_info=True)
@require(internet)
def status_api_request(self, url):
self.logger.debug('Making GitHub Status API request to %s', url)
try:
with urlopen(url) as content:
try:
content_type = dict(content.getheaders())['Content-Type']
charset = re.search(r'charset=(.*)', content_type).group(1)
except AttributeError:
charset = 'utf-8'
response_json = content.read().decode(charset).strip()
if not response_json:
self.logger.error('JSON response from %s was blank', url)
return {}
try:
response = json.loads(response_json)
except json.decoder.JSONDecodeError as exc:
self.logger.error('Error loading JSON: %s', exc)
self.logger.debug('JSON text that failed to load: %s',
response_json)
return {}
self.logger.log(5, 'API response: %s', response)
return response
except Exception as exc:
self.logger.error(
'Failed to make API request to %s. Exception follows:', url,
exc_info=True
)
return {}
def detect_status_change(self, response=None):
if response is not None:
# Compare last update to current and exit without displaying a
# notification if one is not needed.
if self.__previous_json is None:
# This is the first time status has been updated since
# i3pystatus was started. Set self.__previous_json and exit.
self.__previous_json = response
return
if response.get('status', {}).get('description') == self.__previous_json.get('status', {}).get('description'):
# No change, so no notification
return
self.__previous_json = response
if self.__previous_json is None:
# The only way this would happen is if we invoked the right-click
# event before we completed the initial status check.
return
self.show_status_notification()
@staticmethod
def notify(message):
return DesktopNotification(title='GitHub', body=message).display()
def skip_notify(self, message):
self.logger.debug(
'Desktop notifications turned off. Skipped notification: %s',
message
)
return False
def show_status_notification(self):
message = self.current_status_description
self.skip_notify(message) \
if not self.notify_status or (self.previous_status is None and self.current_status == 'none') \
else self.notify(message)
def show_unread_notification(self):
if '%d' not in self.unread_notification_template:
formatted = self.unread_notification_template
else:
try:
new_unread = len(self.new_unread)
except TypeError:
new_unread = 0
try:
formatted = self.unread_notification_template % new_unread
except TypeError as exc:
self.logger.error(
'Failed to format {0!r}: {1}'.format(
self.unread_notification_template,
exc
)
)
return False
return self.skip_notify(formatted) \
if not self.notify_unread \
else self.notify(formatted)
@require(internet)
def perform_update(self):
self.output['full_text'] = \
self.refresh_icon + self.output.get('full_text', '')
self.failed_update = False
self.update_status()
try:
self.config_error = None
self.update_unread()
except ConfigError as exc:
self.config_error = exc
self.data['update_error'] = self.update_error \
if self.failed_update \
else ''
self.refresh_display()
@property
def current_incidents(self):
try:
return self.__current_json['incidents']
except (KeyError, TypeError):
return []
@property
def previous_incidents(self):
try:
return self.__previous_json['incidents']
except (KeyError, TypeError):
return []
@property
def current_status(self):
try:
return self.__current_json['status']['indicator']
except (KeyError, TypeError):
return None
@property
def previous_status(self):
try:
return self.__previous_json['status']['indicator']
except (KeyError, TypeError):
return None
@property
def current_status_description(self):
try:
return self.__current_json['status']['description']
except (KeyError, TypeError):
return None
@require(internet)
def update_status(self):
try:
# Get most recent update
self.__current_json = self.status_api_request(self.api_methods_url)
if not self.__current_json:
self.failed_update = True
return
self.data['status'] = self.status.get(self.current_status)
if self.current_incidents != self.previous_incidents:
self.show_status_notification()
self.__previous_json = self.__current_json
except Exception:
# Don't let an uncaught exception kill the update thread
self.logger.error(
'Uncaught error occurred while checking GitHub status. '
'Exception follows:', exc_info=True
)
self.failed_update = True
@require(internet)
def update_unread(self):
# Reset the new_unread attribute to prevent spurious notifications
self.new_unread = None
try:
if not self.username and not self.password and not self.access_token:
# Auth not configured
self.logger.debug(
'No auth configured, notifications will not be checked')
return True
if not HAS_REQUESTS:
self.logger.error(
'The requests module is required to check GitHub notifications')
self.failed_update = True
return False
self.logger.debug(
'Checking unread notifications using %s',
'access token' if self.access_token else 'username/password'
)
old_unread_url = None
if self.access_token:
unread_url = ACCESS_TOKEN_AUTH_URL % self.access_token
else:
unread_url = BASIC_AUTH_URL
self.current_unread = set()
page_num = 0
while old_unread_url != unread_url:
old_unread_url = unread_url
page_num += 1
self.logger.debug(
'Reading page %d of notifications (%s)',
page_num, unread_url
)
try:
if self.access_token:
response = requests.get(unread_url)
else:
response = requests.get(
unread_url,
auth=(self.username, self.password)
)
self.logger.log(
5,
'Raw return from GitHub notification check: %s',
response.text)
unread_data = json.loads(response.text)
except (requests.ConnectionError, requests.Timeout) as exc:
self.logger.error(
'Failed to check unread notifications: %s', exc)
self.failed_update = True
return False
except json.decoder.JSONDecodeError as exc:
self.logger.error('Error loading JSON: %s', exc)
self.logger.debug(
'JSON text that failed to load: %s', response.text)
self.failed_update = True
return False
# Bad credentials or some other error
if isinstance(unread_data, dict):
raise ConfigError(
unread_data.get(
'message',
'Unknown error encountered retrieving unread notifications'
)
)
# Update the current count of unread notifications
self.current_unread.update(
[x['id'] for x in unread_data if 'id' in x]
)
# Check 'Link' header for next page of notifications
# (https://tools.ietf.org/html/rfc5988#section-5)
self.logger.debug('Checking for next page of notifications')
try:
link_header = response.headers['Link']
except AttributeError:
self.logger.error(
'No headers present in response. This might be due to '
'an API change in the requests module.'
)
self.failed_update = True
continue
except KeyError:
self.logger.debug('Only one page of notifications present')
continue
else:
# Process 'Link' header
try:
links = requests.utils.parse_header_links(link_header)
except Exception as exc:
self.logger.error(
'Failed to parse \'Link\' header: %s', exc
)
self.failed_update = True
continue
for link in links:
try:
link_rel = link['rel']
if link_rel != 'next':
# Link does not refer to the next page, skip it
continue
# Set the unread_url so that when we reach the top
# of the outer loop, we have a new URL to check.
unread_url = link['url']
break
except TypeError:
# Malformed hypermedia link
self.logger.warning(
'Malformed hypermedia link (%s) in \'Link\' '
'header (%s)', link, links
)
continue
else:
self.logger.debug('No more pages of notifications remain')
if self.failed_update:
return False
self.data['unread_count'] = len(self.current_unread)
self.data['unread'] = self.unread_marker \
if self.data['unread_count'] > 0 \
else ''
if self.previous_unread is not None:
if not self.current_unread.issubset(self.previous_unread):
self.new_unread = self.current_unread - self.previous_unread
if self.new_unread:
self.show_unread_notification()
self.previous_unread = self.current_unread
return True
except ConfigError as exc:
# This will be caught by the calling function
raise exc
except Exception as exc:
# Don't let an uncaught exception kill the update thread
self.logger.error(
'Uncaught error occurred while checking GitHub notifications. '
'Exception follows:', exc_info=True
)
self.failed_update = True
return False
def refresh_display(self):
previous_color = self.output.get('color')
try:
color = self.colors.get(
self.current_status,
self.unknown_color)
except TypeError:
# Shouldn't get here, but this would happen if this function is
# called before we check the current status for the first time.
color = previous_color
self.output = {'full_text': formatp(self.format, **self.data).strip(),
'color': color}
def run(self):
if self.config_error is not None:
raise self.config_error
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/gpu_mem.py 0000664 0000000 0000000 00000004142 13567273623 0022507 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from .utils import gpu
class GPUMemory(IntervalModule):
"""
Shows GPU memory load
Currently Nvidia only and nvidia-smi required
.. rubric:: Available formatters
* {avail_mem}
* {percent_used_mem}
* {used_mem}
* {total_mem}
"""
settings = (
("format", "format string used for output."),
("divisor", "divide all megabyte values by this value, default is 1 (megabytes)"),
("warn_percentage", "minimal percentage for warn state"),
("alert_percentage", "minimal percentage for alert state"),
("color", "standard color"),
("warn_color", "defines the color used when warn percentage is exceeded"),
("alert_color", "defines the color used when alert percentage is exceeded"),
("round_size", "defines number of digits in round"),
("gpu_number", "set the gpu number when you have several GPU"),
)
format = "{avail_mem} MiB"
divisor = 1
color = "#00FF00"
warn_color = "#FFFF00"
alert_color = "#FF0000"
warn_percentage = 50
alert_percentage = 80
round_size = 1
gpu_number = 0
def run(self):
info = gpu.query_nvidia_smi(self.gpu_number)
if info.used_mem is not None and info.total_mem is not None:
mem_percent = 100 * info.used_mem / info.total_mem
else:
mem_percent = None
if mem_percent >= self.alert_percentage:
color = self.alert_color
elif mem_percent >= self.warn_percentage:
color = self.warn_color
else:
color = self.color
cdict = {
"used_mem": info.used_mem / self.divisor,
"avail_mem": info.avail_mem / self.divisor,
"total_mem": info.total_mem / self.divisor,
"percent_used_mem": mem_percent,
}
for key, value in cdict.items():
if value is not None:
cdict[key] = round(value, self.round_size)
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/gpu_temp.py 0000664 0000000 0000000 00000002176 13567273623 0022703 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from .utils import gpu
class GPUTemperature(IntervalModule):
"""
Shows GPU temperature
Currently Nvidia only and nvidia-smi required
.. rubric:: Available formatters
* `{temp}` — the temperature in integer degrees celsius
"""
settings = (
("format", "format string used for output. {temp} is the temperature in integer degrees celsius"),
("display_if", "snippet that gets evaluated. if true, displays the module output"),
("gpu_number", "set the gpu number when you have several GPU"),
"color",
"alert_temp",
"alert_color",
)
format = "{temp} °C"
color = "#FFFFFF"
alert_temp = 90
alert_color = "#FF0000"
display_if = 'True'
gpu_number = 0
def run(self):
temp = gpu.query_nvidia_smi(self.gpu_number).temp
temp_alert = temp is None or temp >= self.alert_temp
if eval(self.display_if):
self.output = {
"full_text": self.format.format(temp=temp),
"color": self.color if not temp_alert else self.alert_color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/gpu_usage.py 0000664 0000000 0000000 00000003062 13567273623 0023035 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from .utils import gpu
class GPUUsage(IntervalModule):
"""
Shows GPU load in percent
Currently Nvidia only and nvidia-smi required
.. rubric:: Available formatters
* {usage}
"""
settings = (
("format", "format string used for output."),
("warn_percentage", "minimal percentage for warn state"),
("alert_percentage", "minimal percentage for alert state"),
("color", "standard color"),
("warn_color", "defines the color used when warn percentage is exceeded"),
("alert_color", "defines the color used when alert percentage is exceeded"),
("gpu_number", "set the gpu number when you have several GPU"),
)
format = "{usage}%"
divisor = 1
color = "#00FF00"
warn_color = "#FFFF00"
alert_color = "#FF0000"
warn_percentage = 50
alert_percentage = 80
round_size = 1
gpu_number = 0
def run(self):
info = gpu.query_nvidia_smi(self.gpu_number)
gpu_percent = info.usage_gpu
if gpu_percent >= self.alert_percentage:
color = self.alert_color
elif gpu_percent >= self.warn_percentage:
color = self.warn_color
else:
color = self.color
cdict = {
"usage": gpu_percent,
}
for key, value in cdict.items():
if value is not None:
cdict[key] = value
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/group.py 0000664 0000000 0000000 00000004113 13567273623 0022210 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule, Status, Module
from i3pystatus.core import util
from i3pystatus.core.imputil import ClassFinder
class Group(Module, Status):
"""
Module for grouping modules together
Cycles trough groups by means of scrolling
.. code-block:: python
group = Group()
group.register("network",
interface="eth0",
divisor=1024,
start_color='white',
format_up="{bytes_recv}K / {bytes_sent}K"
)
group.register("network",
interface="eth0",
color_up='#FFFFFF',
format_up="{v4}"
)
status.register(group)
"""
on_upscroll = ['cycle_module', 1]
on_downscroll = ['cycle_module', -1]
def __init__(self, *args, **kwargs):
Module.__init__(self, *args, **kwargs)
self.modules = util.ModuleList(self, ClassFinder(Module))
self.active = 0
self.__name__ = 'Group'
def get_active_module(self):
if self.active > len(self.modules):
return
return self.modules[self.active]
def run(self):
activemodule = self.get_active_module()
if not activemodule:
return
self.output = activemodule.output
def register(self, *args, **kwargs):
module = Status.register(self, *args, **kwargs)
if module:
module.on_change = self.run
return module
def cycle_module(self, increment=1):
active = self.active + increment
if active >= len(self.modules):
active = 0
elif active < 0:
active = len(self.modules) - 1
self.active = active
def on_click(self, button, **kwargs):
"""
Capture scrollup and scorlldown to move in groups
Pass everthing else to the module itself
"""
if button in (4, 5):
return super().on_click(button, **kwargs)
else:
activemodule = self.get_active_module()
if not activemodule:
return
return activemodule.on_click(button, **kwargs)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/iinet.py 0000664 0000000 0000000 00000006527 13567273623 0022177 0 ustar 00root root 0000000 0000000 import requests
from i3pystatus import IntervalModule
from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.util import internet, require
__author__ = 'facetoe'
class IINet(IntervalModule, ColorRangeModule):
"""
Check IINet Internet usage.
Requires `requests` and `colour`
Formatters:
* `{percentage_used}` — percentage of your quota that is used
* `{percentage_available}` — percentage of your quota that is available
* `{used}` - GB of your quota used
"""
settings = (
"format",
("username", "Username for IINet"),
("password", "Password for IINet"),
("start_color", "Beginning color for color range"),
("end_color", "End color for color range")
)
format = '{percent_used}'
start_color = "#00FF00"
end_color = "#FF0000"
username = None
password = None
keyring_backend = None
def init(self):
self.token = None
self.service_token = None
self.colors = self.get_hex_color_range(self.start_color, self.end_color, 100)
def set_tokens(self):
if not self.token or not self.service_token:
response = requests.get('https://toolbox.iinet.net.au/cgi-bin/api.cgi?'
'_USERNAME=%(username)s&'
'_PASSWORD=%(password)s'
% self.__dict__).json()
if self.valid_response(response):
self.token = response['token']
self.service_token = self.get_service_token(response['response']['service_list'])
else:
raise Exception("Failed to retrieve token for user: %s" % self.username)
def get_service_token(self, service_list):
for service in service_list:
if service['pk_v'] == self.username:
return service['s_token']
raise Exception("Failed to retrieve service token for user: %s" % self.username)
def valid_response(self, response):
return "success" in response and response['success'] == 1
@require(internet)
def run(self):
self.set_tokens()
usage = self.get_usage()
allocation = usage['allocation']
used = usage['used']
percent_used = self.percentage(used, allocation)
percent_avaliable = self.percentage(allocation - used, allocation)
color = self.get_gradient(percent_used, self.colors)
usage['percent_used'] = '{0:.2f}%'.format(percent_used)
usage['percent_available'] = '{0:.2f}%'.format(percent_avaliable)
usage['used'] = '{0:.2f}'.format(used / 1000 ** 3)
self.data = usage
self.output = {
"full_text": self.format.format(**usage),
"color": color
}
def get_usage(self):
response = requests.get('https://toolbox.iinet.net.au/cgi-bin/api.cgi?Usage&'
'_TOKEN=%(token)s&'
'_SERVICE=%(service_token)s' % self.__dict__).json()
if self.valid_response(response):
for traffic_type in response['response']['usage']['traffic_types']:
if traffic_type['name'] in ('anytime', 'usage'):
return traffic_type
else:
raise Exception("Failed to retrieve usage information for: %s" % self.username)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/keyboard_locks.py 0000664 0000000 0000000 00000003420 13567273623 0024047 0 ustar 00root root 0000000 0000000 import subprocess
from i3pystatus import IntervalModule
class Keyboard_locks(IntervalModule):
"""
Shows the status of CAPS LOCK, NUM LOCK and SCROLL LOCK
.. rubric:: Available formatters
* `{caps}` — the current status of CAPS LOCK
* `{num}` — the current status of NUM LOCK
* `{scroll}` — the current status of SCROLL LOCK
"""
interval = 1
settings = (
("format", "Format string"),
("caps_on", "String to show in {caps} when CAPS LOCK is on"),
("caps_off", "String to show in {caps} when CAPS LOCK is off"),
("num_on", "String to show in {num} when NUM LOCK is on"),
("num_off", "String to show in {num} when NUM LOCK is off"),
("scroll_on", "String to show in {scroll} when SCROLL LOCK is on"),
("scroll_off", "String to show in {scroll} when SCROLL LOCK is off"),
"color"
)
format = "{caps} {num} {scroll}"
caps_on = "CAP"
caps_off = "___"
num_on = "NUM"
num_off = "___"
scroll_on = "SCR"
scroll_off = "___"
color = "#FFFFFF"
data = {}
def get_status(self):
xset = str(subprocess.check_output(["xset", "q"]))
cap = xset.split("Caps Lock:")[1][0:8]
num = xset.split("Num Lock:")[1][0:8]
scr = xset.split("Scroll Lock:")[1][0:8]
return("on" in cap, "on" in num, "on" in scr)
def run(self):
(cap, num, scr) = self.get_status()
self.data["caps"] = self.caps_on if cap else self.caps_off
self.data["num"] = self.num_on if num else self.num_off
self.data["scroll"] = self.scroll_on if scr else self.scroll_off
output_format = self.format
self.output = {
"full_text": output_format.format(**self.data),
"color": self.color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/lastfm.py 0000664 0000000 0000000 00000003416 13567273623 0022347 0 ustar 00root root 0000000 0000000 from urllib.request import urlopen
import json
from i3pystatus import IntervalModule
class LastFM(IntervalModule):
"""
Displays currently playing song as reported by last.fm. Get your API key
from http://www.last.fm/api.
"""
settings = (
("apikey", "API key used to make calls to last.fm."),
("user", "Name of last.fm user to track."),
("playing_format", "Output format when a song is playing"),
("stopped_format", "Output format when nothing is playing"),
"playing_color",
"stopped_color",
"interval",
)
required = ("apikey", "user")
playing_color = 'FFFFFF'
stopped_color = '000000'
interval = 5
playing_format = "{artist} - {track}"
stopped_format = ""
def run(self):
apiurl = 'http://ws.audioscrobbler.com/2.0/'
uri = '?method=user.getrecenttracks'\
'&user=%s&api_key=%s' \
'&format=json&'\
'limit=1' % (self.user, self.apikey)
content = urlopen(apiurl + uri).read()
responsestr = content.decode('utf-8')
response = json.loads(responsestr)
try:
track = response['recenttracks']['track'][0]
if track['@attr']['nowplaying'] == 'true':
cdict = {
"artist": track['artist']['#text'],
"track": track['name'],
"album": track['album']['#text'],
}
self.data = cdict
self.output = {
"full_text": self.playing_format.format(**cdict),
"color": self.playing_color
}
except KeyError:
self.output = {
"full_text": self.stopped_format,
"color": self.stopped_color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/load.py 0000664 0000000 0000000 00000002461 13567273623 0021777 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
try:
from os import cpu_count
except ImportError:
from multiprocessing import cpu_count
class Load(IntervalModule):
"""
Shows system load
.. rubric:: Available formatters
* `{avg1}` — the load average of the last minute
* `{avg5}` — the load average of the last five minutes
* `{avg15}` — the load average of the last fifteen minutes
* `{tasks}` — the number of tasks (e.g. 1/285, which indiciates that one out of 285 total tasks is runnable)
"""
format = "{avg1} {avg5}"
settings = (
"format",
("color", "The text color"),
("critical_limit", "Limit above which the load is considered critical, defaults to amount of cores."),
("critical_color", "The critical color"),
)
file = "/proc/loadavg"
color = "#ffffff"
critical_limit = cpu_count()
critical_color = "#ff0000"
def run(self):
with open(self.file, "r") as f:
avg1, avg5, avg15, tasks, lastpid = f.read().split(" ", 5)
urgent = float(avg1) > self.critical_limit
self.output = {
"full_text": self.format.format(avg1=avg1, avg5=avg5, avg15=avg15, tasks=tasks),
"urgent": urgent,
"color": self.critical_color if urgent else self.color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/ 0000775 0000000 0000000 00000000000 13567273623 0021425 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/__init__.py 0000664 0000000 0000000 00000006065 13567273623 0023545 0 ustar 00root root 0000000 0000000 from i3pystatus import SettingsBase, IntervalModule
from i3pystatus.core.command import run_through_shell
class Backend(SettingsBase):
"""Handles the details of checking for mail"""
unread = 0
settings = (("account", "Account name"),)
account = "Default account"
"""Number of unread mails
You'll probably implement that as a property"""
class Mail(IntervalModule):
"""
Generic mail checker
The `backends` setting determines the backends to use. For available backends see :ref:`mailbackends`.
"""
settings = (
("backends", "List of backends (instances of ``i3pystatus.mail.xxx.zzz``, e.g. :py:class:`.imap.IMAP`)"),
"color", "color_unread", "format", "format_plural",
("hide_if_null", "Don't output anything if there are no new mails"),
("email_client", "The command to run on left click. "
"For example, to launch Thunderbird set ``email_client` to ``thunderbird``. "
"Alternatively, to bring Thunderbird into focus, "
"set ``email_client`` to ``i3-msg -q [class=\"^Thunderbird$\"] focus``. "
"Hint: To discover the X window class of your email client run 'xprop | grep -i class' "
"and click on it's window\n"),
)
required = ("backends",)
color = "#ffffff"
color_unread = "#ff0000"
format = "{unread} new email"
format_plural = "{account} : {current_unread}/{unread} new emails"
hide_if_null = True
email_client = None
on_leftclick = "open_client"
on_upscroll = ["scroll_backend", 1]
on_downscroll = ["scroll_backend", -1]
current_backend = 0
def init(self):
for backend in self.backends:
pass
def run(self):
"""
Returns the sum of unread messages across all registered backends
"""
unread = 0
current_unread = 0
for id, backend in enumerate(self.backends):
temp = backend.unread or 0
unread = unread + temp
if id == self.current_backend:
current_unread = temp
if not unread:
color = self.color
urgent = "false"
if self.hide_if_null:
self.output = None
return
else:
color = self.color_unread
urgent = "true"
format = self.format
if unread > 1:
format = self.format_plural
account_name = getattr(self.backends[self.current_backend], "account", "No name")
self.output = {
"full_text": format.format(unread=unread, current_unread=current_unread, account=account_name),
"urgent": urgent,
"color": color,
}
def scroll_backend(self, step):
self.current_backend = (self.current_backend + step) % len(self.backends)
def open_client(self):
if self.email_client:
retcode, _, stderr = run_through_shell(self.email_client)
if retcode != 0 and stderr:
self.logger.error(stderr)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/ews.py 0000664 0000000 0000000 00000004527 13567273623 0022605 0 ustar 00root root 0000000 0000000 import exchangelib
import contextlib
import time
from i3pystatus.mail import Backend
class ExchangeMailAccount(Backend):
"""
Checks for mail on an Exchange account.
Requires the python exchangelib library - https://github.com/ecederstrand/exchangelib.
"""
settings = (
("host", 'The url to connect to. If unset, autodiscover is tried with the email address domain. If set, autodiscover is disabled.'),
"username", "password", "email_address",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
)
required = ("username", "password", "email_address")
keyring_backend = None
host = None
account = None
last = 0
@contextlib.contextmanager
def ensure_connection(self):
try:
if not self.account:
credentials = exchangelib.ServiceAccount(
username=self.username,
password=self.password)
if self.host:
config = exchangelib.Configuration(
server=self.host,
credentials=credentials)
self.account = exchangelib.Account(
primary_smtp_address=self.email_address,
config=config,
autodiscover=False,
access_type=exchangelib.DELEGATE)
else:
self.account = exchangelib.Account(
primary_smtp_address=self.email_address,
credentials=credentials,
autodiscover=True,
access_type=exchangelib.DELEGATE)
yield
except Exception as e:
# NOTE(sileht): retry just once if the connection have been
# broken to ensure this is not a sporadic connection lost.
# Like wifi reconnect, sleep wake up
# Wait a bit when disconnection occurs to not hog the cpu
self.logger.warn(e)
time.sleep(1)
self.connection = None
def count_new_mail(self):
self.account.inbox.refresh()
self.last = self.account.inbox.unread_count
@property
def unread(self):
with self.ensure_connection():
self.count_new_mail()
return self.last
Backend = ExchangeMailAccount
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/imap.py 0000664 0000000 0000000 00000005277 13567273623 0022740 0 ustar 00root root 0000000 0000000 from i3pystatus.core.util import require, internet
try:
from imaplib2.imaplib2 import IMAP4, IMAP4_SSL
use_idle = True
except ImportError:
from imaplib import IMAP4, IMAP4_SSL
use_idle = False
import contextlib
import time
import socket
from threading import Thread
from i3pystatus.mail import Backend
IMAP_EXCEPTIONS = (socket.error, socket.gaierror, IMAP4.abort, IMAP4.error)
class IMAP(Backend):
"""
Checks for mail on a IMAP server
"""
settings = (
"host", "port",
"username", "password",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
"ssl",
"mailbox",
)
required = ("host", "username", "password")
keyring_backend = None
port = 993
ssl = True
mailbox = "INBOX"
imap_class = IMAP4
connection = None
last = 0
def init(self):
if self.ssl:
self.imap_class = IMAP4_SSL
if use_idle:
self.thread = Thread(target=self._idle_thread)
self.daemon = True
self.thread.start()
@contextlib.contextmanager
def ensure_connection(self):
try:
if self.connection:
self.connection.select(self.mailbox)
if not self.connection:
self.connection = self.imap_class(self.host, self.port)
self.connection.login(self.username, self.password)
self.connection.select(self.mailbox)
yield
except IMAP_EXCEPTIONS:
# NOTE(sileht): retry just once if the connection have been
# broken to ensure this is not a sporadic connection lost.
# Like wifi reconnect, sleep wake up
try:
self.connection.close()
except IMAP_EXCEPTIONS:
pass
try:
self.connection.logout()
except IMAP_EXCEPTIONS:
pass
# Wait a bit when disconnection occurs to not hog the cpu
time.sleep(1)
self.connection = None
def _idle_thread(self):
# update mail count on startup
with self.ensure_connection():
self.count_new_mail()
while True:
with self.ensure_connection():
# Block until new mails
self.connection.idle()
# Read how many
self.count_new_mail()
def count_new_mail(self):
self.last = len(self.connection.search(None, "UnSeen")[1][0].split())
@property
@require(internet)
def unread(self):
if not use_idle:
with self.ensure_connection():
self.count_new_mail()
return self.last
Backend = IMAP
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/maildir.py 0000664 0000000 0000000 00000000565 13567273623 0023426 0 ustar 00root root 0000000 0000000 import os
from i3pystatus.mail import Backend
class MaildirMail(Backend):
"""
Checks for local mail in Maildir
"""
settings = (
"directory",
)
required = ("directory",)
directory = ""
@property
def unread(self):
path = os.path.join(self.directory, "new")
return len(os.listdir(path))
Backend = MaildirMail
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/mbox.py 0000664 0000000 0000000 00000001112 13567273623 0022737 0 ustar 00root root 0000000 0000000 import sys
from i3pystatus.mail import Backend
import subprocess
class MboxMail(Backend):
"""
Checks for local mail in mbox
"""
settings = ()
required = ()
@property
def unread(self):
p = subprocess.Popen(['messages.mailutils'], stdout=subprocess.PIPE)
stdout, stderr = p.communicate()
stdout = stdout.decode('utf8')
assert p.returncode == 0, "messages.mailutils returned non-zero return code"
s_stuff, message_number = stdout.strip().rsplit(':', 1)
return int(message_number.strip())
Backend = MboxMail
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/notmuchmail.py 0000664 0000000 0000000 00000002443 13567273623 0024322 0 ustar 00root root 0000000 0000000 # note that this needs the notmuch python bindings. For more info see:
# http://notmuchmail.org/howto/#index4h2
import notmuch
import configparser
import os
from i3pystatus.mail import Backend
class Notmuch(Backend):
"""
This class uses the notmuch python bindings to check for the
number of messages in the notmuch database with the tags "inbox"
and "unread"
"""
settings = (
("db_path", "Path to the directory of your notmuch database"),
("query", "Same query notmuch would accept, by default 'tag:unread and tag:inbox'"),
)
db_path = None
query = "tag:unread and tag:inbox"
def init(self):
if not self.db_path:
defaultConfigFilename = os.path.expanduser("~/.notmuch-config")
config = configparser.RawConfigParser()
# read tries to read and returns successfully read filenames
successful = config.read([
os.environ.get("NOTMUCH_CONFIG", defaultConfigFilename),
defaultConfigFilename
])
self.db_path = config.get("database", "path")
@property
def unread(self):
db = notmuch.Database(self.db_path)
result = notmuch.Query(db, self.query).count_messages()
db.close()
return result
Backend = Notmuch
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/thunderbird.py 0000664 0000000 0000000 00000003125 13567273623 0024312 0 ustar 00root root 0000000 0000000 # This plugin listens for dbus signals emitted by the
# thunderbird-dbus-sender extension for TB:
# https://github.com/janoliver/thunderbird-dbus-sender
# The plugin must be active and thunderbird running for the module to work
# properly.
from functools import partial
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GObject
from i3pystatus.mail import Backend
class Thunderbird(Backend):
"""
This class listens for dbus signals emitted by
the dbus-sender extension for thunderbird.
Requires python-dbus
"""
_unread = set()
def init(self):
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
bus.add_signal_receiver(self.new_msg,
dbus_interface="org.mozilla.thunderbird.DBus",
signal_name="NewMessageSignal")
bus.add_signal_receiver(self.changed_msg,
dbus_interface="org.mozilla.thunderbird.DBus",
signal_name="ChangedMessageSignal")
loop = GObject.MainLoop()
dbus.mainloop.glib.threads_init()
self.context = loop.get_context()
self.run = partial(self.context.iteration, False)
def new_msg(self, id, author, subject):
if id not in self._unread:
self._unread.add(id)
def changed_msg(self, id, event):
if event == "read" and id in self._unread:
self._unread.remove(id)
@property
def unread(self):
self.run()
return len(self._unread)
Backend = Thunderbird
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/makewatch.py 0000664 0000000 0000000 00000002332 13567273623 0023021 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
import psutil
import getpass
class MakeWatch(IntervalModule):
"""
Watches for make jobs and notifies when they are completed.
requires: psutil
"""
settings = (
("name", "Listen for a job other than 'make' jobs"),
("running_color", "Text color while the job is running"),
("idle_color", "Text color while the job is not running"),
"format",
)
running_color = "#FF0000" # red
idle_color = "#00FF00" # green
name = 'make'
format = "{name}: {status}"
def run(self):
status = 'idle'
for proc in psutil.process_iter():
cur_proc = proc.as_dict(attrs=['name', 'username'])
if getpass.getuser() in cur_proc['username']:
if cur_proc['name'] == self.name:
status = proc.as_dict(attrs=['status'])['status']
if status == 'idle':
color = self.idle_color
else:
color = self.running_color
cdict = {
"name": self.name,
"status": status
}
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mem.py 0000664 0000000 0000000 00000003512 13567273623 0021634 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
import psutil
from .core.util import round_dict
class Mem(IntervalModule):
"""
Shows memory load
.. rubric:: Available formatters
* {avail_mem}
* {percent_used_mem}
* {used_mem}
* {total_mem}
Requires psutil (from PyPI)
"""
format = "{avail_mem} MiB"
divisor = 1024 ** 2
color = "#00FF00"
warn_color = "#FFFF00"
alert_color = "#FF0000"
warn_percentage = 50
alert_percentage = 80
round_size = 1
settings = (
("format", "format string used for output."),
("divisor",
"divide all byte values by this value, default is 1024**2 (megabytes)"),
("warn_percentage", "minimal percentage for warn state"),
("alert_percentage", "minimal percentage for alert state"),
("color", "standard color"),
("warn_color",
"defines the color used when warn percentage is exceeded"),
("alert_color",
"defines the color used when alert percentage is exceeded"),
("round_size", "defines number of digits in round"),
)
def run(self):
memory_usage = psutil.virtual_memory()
if memory_usage.percent >= self.alert_percentage:
color = self.alert_color
elif memory_usage.percent >= self.warn_percentage:
color = self.warn_color
else:
color = self.color
cdict = {
"used_mem": max(0, memory_usage.used) / self.divisor,
"avail_mem": memory_usage.available / self.divisor,
"total_mem": memory_usage.total / self.divisor,
"percent_used_mem": memory_usage.percent,
}
round_dict(cdict, self.round_size)
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mem_bar.py 0000664 0000000 0000000 00000003354 13567273623 0022464 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from psutil import virtual_memory
from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.util import make_bar
class MemBar(IntervalModule, ColorRangeModule):
"""
Shows memory load as a bar.
.. rubric:: Available formatters
* {used_mem_bar}
Requires psutil and colour (from PyPI)
"""
format = "{used_mem_bar}"
color = "#00FF00"
warn_color = "#FFFF00"
alert_color = "#FF0000"
warn_percentage = 50
alert_percentage = 80
multi_colors = False
def init(self):
self.colors = self.get_hex_color_range(self.color, self.alert_color, 100)
settings = (
("format", "format string used for output."),
("warn_percentage", "minimal percentage for warn state"),
("alert_percentage", "minimal percentage for alert state"),
("color", "standard color"),
("warn_color",
"defines the color used when warn percentage is exceeded"),
("alert_color",
"defines the color used when alert percentage is exceeded"),
("multi_colors", "whether to use range of colors from 'color' to 'alert_color' based on memory usage."),
)
def run(self):
memory_usage = virtual_memory()
if self.multi_colors:
color = self.get_gradient(memory_usage.percent, self.colors)
elif memory_usage.percent >= self.alert_percentage:
color = self.alert_color
elif memory_usage.percent >= self.warn_percentage:
color = self.warn_color
else:
color = self.color
self.output = {
"full_text": self.format.format(
used_mem_bar=make_bar(memory_usage.percent)),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/moc.py 0000664 0000000 0000000 00000006065 13567273623 0021642 0 ustar 00root root 0000000 0000000 import re
from i3pystatus import IntervalModule
from i3pystatus import formatp
from i3pystatus.core.command import run_through_shell
from i3pystatus.core.util import TimeWrapper
class Moc(IntervalModule):
"""
Display various information from MOC (music on console)
.. rubric:: Available formatters
* `{status}` — current status icon (paused/playing/stopped)
* `{song_elapsed}` — song elapsed time (mm:ss format)
* `{song_length}` — total song duration (mm:ss format)
* `{artist}` — artist
* `{title}` — title
* `{album}` — album
* `{tracknumber}` — tracknumber
* `{file}` — file or url name
"""
settings = (
('format', 'formatp string'),
('format_not_running', 'Text to show if MOC is not running'),
('color', 'The color of the text'),
('color_not_running', 'The color of the text, when MOC is not running'),
('status', 'Dictionary mapping status to output'),
)
color = '#ffffff'
color_not_running = '#ffffff'
format = '{status} {song_elapsed}/{song_length} {artist} - {title}'
format_not_running = 'Not running'
interval = 1
status = {
'pause': '▷',
'play': '▶',
'stop': '◾',
}
on_leftclick = 'toggle_pause'
on_rightclick = 'next_song'
on_upscroll = 'next_song'
on_downscroll = 'previous_song'
def _moc_command(self, command):
cmdline = 'mocp --{command}'.format(command=command)
return run_through_shell(cmdline, enable_shell=True)
def _query_moc(self):
response = {}
# Get raw information
cmd = self._moc_command('info')
# Now we make it useful
if not cmd.rc:
for line in cmd.out.splitlines():
key, _, value = line.partition(': ')
response[key] = value
return response
def run(self):
response = self._query_moc()
if response:
fdict = {
'album': response.get('Album', ''),
'artist': response.get('Artist', ''),
'file': response.get('File', ''),
'song_elapsed': TimeWrapper(response.get('CurrentSec', 0)),
'song_length': TimeWrapper(response.get('TotalSec', 0)),
'status': self.status[response['State'].lower()],
'title': response.get('SongTitle', ''),
'tracknumber': re.match(r'(\d*).*', response.get('Title', '')).group(1) or 0,
}
self.data = fdict
self.output = {
'full_text': formatp(self.format, **self.data),
'color': self.color,
}
else:
if hasattr(self, "data"):
del self.data
self.output = {
'full_text': self.format_not_running,
'color': self.color_not_running,
}
def toggle_pause(self):
self._moc_command('toggle-pause')
def next_song(self):
self._moc_command('next')
def previous_song(self):
self._moc_command('previous')
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/modsde.py 0000664 0000000 0000000 00000005574 13567273623 0022343 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import urllib.request
import urllib.parse
import urllib.error
import re
import http.cookiejar
import xml.etree.ElementTree as ET
import webbrowser
from i3pystatus import IntervalModule
class ModsDeChecker(IntervalModule):
"""
This class returns i3status parsable output of the number of
unread posts in any bookmark in the mods.de forums.
"""
settings = (
("format",
"""Use {unread} as the formatter for number of unread posts"""),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
("offset", """subtract number of posts before output"""),
"color", "username", "password"
)
required = ("username", "password")
keyring_backend = None
color = "#7181fe"
offset = 0
format = "{unread} new posts in bookmarks"
login_url = "http://login.mods.de/"
bookmark_url = "http://forum.mods.de/bb/xml/bookmarks.php"
opener = None
cj = None
logged_in = False
on_leftclick = "open_browser"
def init(self):
self.cj = http.cookiejar.CookieJar()
self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(self.cj))
def run(self):
unread = self.get_unread_count()
if not unread:
self.output = None
else:
self.output = {
"full_text": self.format.format(unread=unread),
"urgent": "true",
"color": self.color
}
def get_unread_count(self):
if not self.logged_in:
self.login()
try:
f = self.opener.open(self.bookmark_url)
root = ET.fromstring(f.read())
return int(root.attrib["newposts"]) - self.offset
except Exception:
self.cj.clear()
self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(self.cj))
self.logged_in = False
def login(self):
data = urllib.parse.urlencode({
"login_username": self.username,
"login_password": self.password,
"login_lifetime": "31536000"
})
try:
response = self.opener.open(self.login_url, data.encode("ascii"))
except Exception:
return
page = response.read().decode("ISO-8859-15")
m = re.search("http://forum.mods.de/SSO.php[^']*", page)
self.cj.clear()
if m and m.group(0):
# get the cookie
response = self.opener.open(m.group(0))
for cookie in self.cj:
self.cj.clear
self.logged_in = True
self.opener.addheaders.append(
("Cookie", "{}={}".format(cookie.name, cookie.value)))
return True
return False
def open_browser(self):
webbrowser.open_new_tab("http://forum.mods.de/bb/")
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/moon.py 0000664 0000000 0000000 00000006164 13567273623 0022034 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule, formatp
import datetime
import math
import decimal
import os
from i3pystatus.core.util import TimeWrapper
dec = decimal.Decimal
class MoonPhase(IntervalModule):
"""
Available Formatters
status: Allows for mapping of current moon phase
- New Moon:
- Waxing Crescent:
- First Quarter:
- Waxing Gibbous:
- Full Moon:
- Waning Gibbous:
- Last Quarter:
- Waning Crescent:
"""
settings = (
"format",
("status", "Current moon phase"),
("illum", "Percentage that is illuminated"),
("color", "Set color"),
("moonicon", 'Set icon')
)
format = "{illum} {status} {moonicon}"
interval = 60 * 60 * 2 # every 2 hours
status = {
"New Moon": "NM",
"Waxing Crescent": "WaxCres",
"First Quarter": "FQ",
"Waxing Gibbous": "WaxGib",
"Full Moon": "FM",
"Waning Gibbous": "WanGib",
"Last Quarter": "LQ",
"Waning Crescent": "WanCres",
}
color = {
"New Moon": "#00BDE5",
"Waxing Crescent": "#138DD8",
"First Quarter": "#265ECC",
"Waxing Gibbous": "#392FBF",
"Full Moon": "#4C00B3",
"Waning Gibbous": "#871181",
"Last Quarter": "#C32250",
"Waning Crescent": "#FF341F",
}
moonicon = {
"New Moon": b'\xf0\x9f\x8c\x91'.decode(),
"Waxing Crescent": b'\xf0\x9f\x8c\x92'.decode(),
"First Quarter": b'\xf0\x9f\x8c\x93'.decode(),
"Waxing Gibbous": b'\xf0\x9f\x8c\x94'.decode(),
"Full Moon": b'\xf0\x9f\x8c\x95'.decode(),
"Waning Gibbous": b'\xf0\x9f\x8c\x96'.decode(),
"Last Quarter": b'\xf0\x9f\x8c\x97'.decode(),
"Waning Crescent": b'\xf0\x9f\x8c\x98'.decode()
}
def pos(now=None):
days_in_second = 86400
now = datetime.datetime.now()
difference = now - datetime.datetime(2001, 1, 1)
days = dec(difference.days) + (dec(difference.seconds) / dec(days_in_second))
lunarCycle = dec("0.20439731") + (days * dec("0.03386319269"))
return lunarCycle % dec(1)
def current_phase(self):
lunarCycle = self.pos()
index = (lunarCycle * dec(8)) + dec("0.5")
index = math.floor(index)
return {
0: "New Moon",
1: "Waxing Crescent",
2: "First Quarter",
3: "Waxing Gibbous",
4: "Full Moon",
5: "Waning Gibbous",
6: "Last Quarter",
7: "Waning Crescent",
}[int(index) & 7]
def illum(self):
phase = 0
lunarCycle = float(self.pos()) * 100
if lunarCycle > 50:
phase = 100 - lunarCycle
else:
phase = lunarCycle * 2
return phase
def run(self):
fdict = {
"status": self.status[self.current_phase()],
"illum": self.illum(),
"moonicon": self.moonicon[self.current_phase()]
}
self.data = fdict
self.output = {
"full_text": formatp(self.format, **fdict),
"color": self.color[self.current_phase()],
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mpd.py 0000664 0000000 0000000 00000017400 13567273623 0021637 0 ustar 00root root 0000000 0000000 from collections import defaultdict
import socket
from os.path import basename
from math import floor
from i3pystatus import IntervalModule, formatp
from i3pystatus.core.util import TimeWrapper
class MPD(IntervalModule):
"""
Displays various information from MPD (the music player daemon)
.. rubric:: Available formatters (uses :ref:`formatp`)
* `{title}` — (the title of the current song)
* `{album}` — (the album of the current song, can be an empty string \
(e.g. for online streams))
* `{artist}` — (can be empty, too)
* `{album_artist}` — (can be empty)
* `{filename}` — (file name with out extension and path; empty unless \
title is empty)
* `{song_elapsed}` — (Position in the currently playing song, uses \
:ref:`TimeWrapper`, default is `%m:%S`)
* `{song_length}` — (Length of the current song, same as song_elapsed)
* `{pos}` — (Position of current song in playlist, one-based)
* `{len}` — (Songs in playlist)
* `{status}` — (play, pause, stop mapped through the `status` dictionary)
* `{bitrate}` — (Current bitrate in kilobit/s)
* `{volume}` — (Volume set in MPD)
.. rubric:: Available callbacks
* ``switch_playpause`` — Plays if paused or stopped, otherwise pauses. \
Emulates ``mpc toggle``.
* ``stop`` — Stops playback. Emulates ``mpc stop``.
* ``next_song`` — Goes to next track in the playlist. Emulates ``mpc \
next``.
* ``previous_song`` — Goes to previous track in the playlist. Emulates \
``mpc prev``.
* ``mpd_command`` — Send a command directly to MPD's socket. The command \
is the second element of the list. Documentation for available commands can \
be found at https://www.musicpd.org/doc/protocol/command_reference.html
Example module registration with callbacks:
::
status.register("mpd",
on_leftclick="switch_playpause",
on_rightclick=["mpd_command", "stop"],
on_middleclick=["mpd_command", "shuffle"],
on_upscroll=["mpd_command", "seekcur -10"],
on_downscroll=["mpd_command", "seekcur +10"])
Note that ``next_song`` and ``previous_song``, and their ``mpd_command`` \
equivalents, are ignored while mpd is stopped.
"""
interval = 1
settings = (
("host"),
("port", "MPD port. If set to 0, host will we interpreted as a Unix \
socket."),
("format", "formatp string"),
("status", "Dictionary mapping pause, play and stop to output"),
("color", "The color of the text"),
("color_map", "The mapping from state to color of the text"),
("max_field_len", "Defines max length for in truncate_fields defined \
fields, if truncated, ellipsis are appended as indicator. It's applied \
*before* max_len. Value of 0 disables this."),
("max_len", "Defines max length for the hole string, if exceeding \
fields specefied in truncate_fields are truncated equaly. If truncated, \
ellipsis are appended as indicator. It's applied *after* max_field_len. Value \
of 0 disables this."),
("truncate_fields", "fields that will be truncated if exceeding \
max_field_len or max_len."),
("hide_inactive", "Hides status information when MPD is not running"),
("password", "A password for access to MPD. (This is sent in \
cleartext to the server.)"),
)
host = "localhost"
port = 6600
password = None
s = None
format = "{title} {status}"
status = {
"pause": "▷",
"play": "▶",
"stop": "◾",
}
color = "#FFFFFF"
color_map = {}
max_field_len = 25
max_len = 100
truncate_fields = ("title", "album", "artist", "album_artist")
hide_inactive = False
on_leftclick = "switch_playpause"
on_rightclick = "next_song"
on_upscroll = on_rightclick
on_downscroll = "previous_song"
def _mpd_command(self, sock, command):
try:
sock.send((command + "\n").encode("utf-8"))
except Exception as e:
if self.port != 0:
self.s = socket.create_connection((self.host, self.port))
else:
self.s = socket.socket(family=socket.AF_UNIX)
self.s.connect(self.host)
sock = self.s
sock.recv(8192)
if self.password is not None:
sock.send('password "{}"\n'.format(self.password).
encode("utf-8"))
sock.recv(8192)
sock.send((command + "\n").encode("utf-8"))
try:
reply = sock.recv(16384).decode("utf-8", "replace")
replylines = reply.split("\n")[:-2]
return dict(
(line.split(": ", 1)) for line in replylines
)
except Exception as e:
return None
def run(self):
try:
status = self._mpd_command(self.s, "status")
playback_state = status["state"]
if playback_state == "stop":
currentsong = {}
else:
currentsong = self._mpd_command(self.s, "currentsong") or {}
except Exception:
if self.hide_inactive:
self.output = {
"full_text": ""
}
if hasattr(self, "data"):
del self.data
return
fdict = {
"pos": int(status.get("song", 0)) + 1,
"len": int(status.get("playlistlength", 0)),
"status": self.status[playback_state],
"volume": int(status.get("volume", 0)),
"title": currentsong.get("Title", ""),
"album": currentsong.get("Album", ""),
"artist": currentsong.get("Artist", ""),
"album_artist": currentsong.get("AlbumArtist", ""),
"song_length": TimeWrapper(currentsong.get("Time", 0)),
"song_elapsed": TimeWrapper(float(status.get("elapsed", 0))),
"bitrate": int(status.get("bitrate", 0)),
}
if not fdict["title"] and "file" in currentsong:
fdict["filename"] = '.'.join(
basename(currentsong["file"]).split('.')[:-1])
else:
fdict["filename"] = ""
if self.max_field_len > 0:
for key in self.truncate_fields:
if len(fdict[key]) > self.max_field_len:
fdict[key] = fdict[key][:self.max_field_len - 1] + "…"
self.data = fdict
full_text = formatp(self.format, **fdict).strip()
full_text_len = len(full_text)
if full_text_len > self.max_len and self.max_len > 0:
shrink = floor((self.max_len - full_text_len)
/ len(self.truncate_fields)) - 1
for key in self.truncate_fields:
fdict[key] = fdict[key][:shrink] + "…"
full_text = formatp(self.format, **fdict).strip()
color_map = defaultdict(lambda: self.color, self.color_map)
self.output = {
"full_text": full_text,
"color": color_map[playback_state],
}
def switch_playpause(self):
try:
self._mpd_command(self.s, "play"
if self._mpd_command(self.s, "status")["state"]
in ["pause", "stop"] else "pause 1")
except Exception as e:
pass
def stop(self):
try:
self._mpd_command(self.s, "stop")
except Exception as e:
pass
def next_song(self):
try:
self._mpd_command(self.s, "next")
except Exception as e:
pass
def previous_song(self):
try:
self._mpd_command(self.s, "previous")
except Exception as e:
pass
def mpd_command(self, command):
try:
self._mpd_command(self.s, command)
except Exception as e:
pass
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/net_speed.py 0000664 0000000 0000000 00000006606 13567273623 0023033 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
import speedtest
import requests
import time
import os
from urllib.parse import urlparse
import contextlib
import sys
from io import StringIO
class NetSpeed(IntervalModule):
"""
Attempts to provide an estimation of internet speeds.
Requires: speedtest-cli/modularize-2
speedtest-cli/modularize-2 can be installed using pip:
`pip install git+https://github.com/sivel/speedtest-cli.git@modularize-2`
"""
settings = (
("units", "Valid values are B, b, bytes, or bits"),
"format",
'color'
)
color = "#FFFFFF"
interval = 300
units = 'bits'
format = "↓{speed_down:.1f}{down_units} ↑{speed_up:.1f}{up_units} ({hosting_provider})"
def form_b(self, n: float)->tuple:
"""
formats a bps as bps/kbps/mbps/gbps etc
handles whether its meant to be in bytes
:param n: input float
:rtype tuple:
:return: tuple of float-number of mbps etc, str-units
"""
unit = 'bps'
kilo = 1000
mega = 1000000
giga = 1000000000
bps = 0
if self.units == 'bytes' or self.units == 'B':
unit = 'Bps'
kilo = 8000
mega = 8000000
giga = 8000000000
if n < kilo:
bps = float(n)
if n >= kilo and n < mega:
unit = "K" + unit
bps = float(n / 1024.0)
if n >= mega and n < giga:
unit = "M" + unit
bps = float(n / (1024.0 * 1024.0))
if n >= giga:
unit = "G" + unit
bps = float(n / (1024.0 * 1024.0 * 1024.0))
return bps, unit
def run(self):
# since speedtest_cli likes to print crap, we need to squelch it
@contextlib.contextmanager
def nostdout():
save_stdout = sys.stdout
sys.stdout = StringIO()
yield
sys.stdout = save_stdout
cdict = {
"speed_up": 0.0,
"speed_down": 0.0,
"down_units": "",
"up_units": "",
"hosting_provider": 'null'
}
st = None
with nostdout():
try:
# this is now the canonical way to use speedtest_cli as a module.
st = speedtest.Speedtest()
except speedtest.ConfigRetrievalError:
# log('Cannot retrieve speedtest configuration')
self.output = {}
if st:
try:
# get the servers
st.get_servers()
st.get_best_server()
except speedtest.ServersRetrievalError:
# log this somehow
# log('Cannot retrieve speedtest server list')
pass
results = st.results
down, up = st.download(), st.upload()
speed_down, down_units = self.form_b(down)
speed_up, up_units = self.form_b(up)
cdict = {
"speed_down": speed_down,
"speed_up": speed_up,
"up_units": up_units,
"down_units": down_units,
"hosting_provider": results.server.get("sponsor", "Unknown Provider")
}
self.output = {
"full_text": self.format.format(**cdict),
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/network.py 0000664 0000000 0000000 00000050453 13567273623 0022555 0 ustar 00root root 0000000 0000000 from fnmatch import fnmatch
import netifaces
from i3pystatus import IntervalModule, formatp
from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.util import make_graph, round_dict, make_bar, bytes_info_dict
def count_bits(integer):
bits = 0
while integer:
integer &= integer - 1
bits += 1
return bits
def cidr6(addr, bits):
return "{addr}/{bits}".format(addr=addr, bits=bits)
def v4_to_int(v4):
sum = 0
mul = 1
for part in reversed(v4.split(".")):
sum += int(part) * mul
mul *= 2 ** 8
return sum
def prefix4(mask):
return count_bits(v4_to_int(mask))
def cidr4(addr, mask):
return "{addr}/{bits}".format(addr=addr, bits=prefix4(mask))
def get_bonded_slaves():
try:
with open("/sys/class/net/bonding_masters") as f:
masters = f.read().split()
except FileNotFoundError:
return {}
slaves = {}
for master in masters:
with open("/sys/class/net/{}/bonding/slaves".format(master)) as f:
for slave in f.read().split():
slaves[slave] = master
return slaves
def sysfs_interface_up(interface, unknown_up=False):
try:
with open("/sys/class/net/{}/operstate".format(interface)) as f:
status = f.read().strip()
except FileNotFoundError:
# Interface doesn't exist
return False
return status == "up" or unknown_up and status == "unknown"
def detect_active_interface(ignore_ifaces, default_interface):
default_gateway = netifaces.gateways()['default']
for af in (netifaces.AF_INET, netifaces.AF_INET6):
_, interface = default_gateway.get(af, (None, None))
if interface and interface not in ignore_ifaces:
return interface
return default_interface
class NetworkInfo:
"""
Retrieve network information.
"""
def __init__(self, interface, ignore_interfaces, detached_down, unknown_up, freq_divisor, get_wifi_info=False):
if interface not in netifaces.interfaces() and not detached_down:
raise RuntimeError(
"Unknown interface {iface}!".format(iface=interface))
self.ignore_interfaces = ignore_interfaces
self.detached_down = detached_down
self.unknown_up = unknown_up
self.get_wifi_info = get_wifi_info
if freq_divisor == 0:
raise RuntimeError("Frequency divider cannot be 0!")
else:
self.freq_divisor = freq_divisor
def get_info(self, interface):
format_dict = dict(v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="")
iface_up = sysfs_interface_up(interface, self.unknown_up)
if not iface_up:
return format_dict
network_info = netifaces.ifaddresses(interface)
slaves = get_bonded_slaves()
try:
master = slaves[interface]
except KeyError:
pass
else:
if sysfs_interface_up(interface, self.unknown_up):
master_info = netifaces.ifaddresses(master)
for af in (netifaces.AF_INET, netifaces.AF_INET6):
try:
network_info[af] = master_info[af]
except KeyError:
pass
try:
mac = network_info[netifaces.AF_PACKET][0]["addr"]
except KeyError:
mac = "NONE"
format_dict['mac'] = mac
if iface_up:
format_dict.update(self.extract_network_info(network_info))
format_dict.update(self.extract_wireless_info(interface))
return format_dict
@staticmethod
def extract_network_info(network_info):
info = dict()
if netifaces.AF_INET in network_info:
for v4 in network_info[netifaces.AF_INET]:
info["v4"] = v4["addr"]
info["v4mask"] = v4["netmask"]
info["v4cidr"] = cidr4(v4["addr"], v4["netmask"])
if not v4["addr"].startswith("169.254"): # prefer non link-local addresses
break
if netifaces.AF_INET6 in network_info:
for v6 in network_info[netifaces.AF_INET6]:
info["v6"] = v6["addr"]
try:
mask, bits = v6["netmask"].split("/")
info["v6mask"] = mask
info["v6cidr"] = cidr6(v6["addr"], bits)
except ValueError:
info["v6cidr"] = v6["addr"]
info["v6mask"] = v6["netmask"]
if not v6["addr"].startswith("fe80::"): # prefer non link-local addresses
break
return info
def extract_wireless_info(self, interface):
info = dict(essid="", freq="", quality=0.0, quality_bar="")
# Just return empty values if we're not using any Wifi functionality
if not self.get_wifi_info:
return info
import basiciw
try:
iwi = basiciw.iwinfo(interface)
except Exception:
# Not a wireless interface
return info
info["essid"] = iwi["essid"]
info["freq"] = iwi["freq"] / self.freq_divisor
quality = iwi["quality"]
if quality["quality_max"] > 0:
info["quality"] = quality["quality"] / quality["quality_max"]
else:
info["quality"] = quality["quality"]
info["quality"] *= 100
info["quality_bar"] = make_bar(info["quality"])
info["quality"] = round(info["quality"])
return info
class NetworkTraffic:
"""
Retrieve network traffic information
"""
pnic = None
pnic_before = None
def __init__(self, unknown_up):
self.unknown_up = unknown_up
def update_counters(self, interface):
import psutil
self.pnic_before = self.pnic
counters = psutil.net_io_counters(pernic=True)
self.pnic = counters[interface] if interface in counters else None
def clear_counters(self):
self.pnic_before = None
self.pnic = None
def get_bytes_sent(self):
return self.pnic.bytes_sent - self.pnic_before.bytes_sent
def get_bytes_received(self):
return self.pnic.bytes_recv - self.pnic_before.bytes_recv
def get_packets_sent(self):
return self.pnic.packets_sent - self.pnic_before.packets_sent
def get_packets_received(self):
return self.pnic.packets_recv - self.pnic_before.packets_recv
def get_rx_total(self, interface):
try:
with open("/sys/class/net/{}/statistics/rx_bytes".format(interface)) as f:
return int(f.readline().split('\n')[0])
except FileNotFoundError:
return False
def get_tx_total(self, interface):
try:
with open("/sys/class/net/{}/statistics/tx_bytes".format(interface)) as f:
return int(f.readline().split('\n')[0])
except FileNotFoundError:
return False
def get_usage(self, interface):
self.update_counters(interface)
usage = dict(bytes_sent=0, bytes_recv=0, packets_sent=0, packets_recv=0, rx_total=0, tx_total=0)
if not sysfs_interface_up(interface, self.unknown_up) or not self.pnic_before:
return usage
else:
usage["bytes_sent"] = self.get_bytes_sent()
usage["bytes_recv"] = self.get_bytes_received()
usage["packets_sent"] = self.get_packets_sent()
usage["packets_recv"] = self.get_packets_received()
usage["rx_total"] = self.get_rx_total(interface)
usage["tx_total"] = self.get_tx_total(interface)
return usage
class Network(IntervalModule, ColorRangeModule):
"""
Displays network information for an interface.
formatp support
if u wanna display recv/send speed separate in dynamic color mode, please enable pango hint.
Requires the PyPI packages `colour`, `netifaces`, `psutil` (optional, see below)
and `basiciw` (optional, see below).
.. rubric:: Available formatters
Network Information Formatters:
* `{interface}` — same as setting
* `{v4}` — IPv4 address
* `{v4mask}` — subnet mask
* `{v4cidr}` — IPv4 address in cidr notation (i.e. 192.168.2.204/24)
* `{v6}` — IPv6 address
* `{v6mask}` — subnet mask
* `{v6cidr}` — IPv6 address in cidr notation
* `{mac}` — MAC of interface
Wireless Information Formatters (requires PyPI package `basiciw`):
* `{essid}` — ESSID of currently connected wifi
* `{freq}` — Current frequency
* `{freq_divisor}` — Frequency divisor
* `{quality}` — Link quality in percent
* `{quality_bar}` —Bar graphically representing link quality
Network Traffic Formatters (requires PyPI package `psutil`):
* `{interface}` — the configured network interface
* `{network_graph_recv}` – Unicode graph representing incoming network traffic
* `{network_graph_sent}` – Unicode graph representing outgoing network traffic
* `{bytes_sent}` — bytes sent per second (divided by divisor | auto calculated if auto_units == True)
* `{bytes_recv}` — bytes received per second (divided by divisor | auto calculated if auto_units == True)
* `{packets_sent}` — packets sent per second
* `{packets_recv}` — packets received per second
* `{rx_tot_Mbytes}` — total Mbytes received
* `{tx_tot_Mbytes}` — total Mbytes sent
* `{rx_tot}` — total traffic recieved (rounded to nearest unit: KB, MB, GB)
* `{tx_tot}` — total traffic sent (rounded to nearest unit: KB, MB, GB)
"""
settings = (
("format_up", "format string"),
("format_active_up", "Dictionary containing format strings for auto-detected interfaces. "
"Each key can be either a full interface name, or a pattern matching "
"a interface, eg 'e*' for ethernet interfaces. "
"Fallback to format_up if no pattern could be matched."),
("format_down", "format string"),
"color_up",
"color_down",
("interface", "Interface to watch, eg 'eth0'"),
("dynamic_color", "Set color dynamically based on network traffic. Note: this overrides color_up"),
("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"),
("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'"),
("graph_width", "Width of the network traffic graph"),
("graph_style", "Graph style ('blocks', 'braille-fill', 'braille-peak', or 'braille-snake')"),
("graph_direction", 'left-to-right/right-to-left'),
("separate_color", "display recv/send color separate in dynamic color mode."
"Note: only network speed formatters will display with range color "),
("coloring_type", "Whether to use the sent or received kb/s for dynamic coloring with non-separate colors. "
"Allowed values 'recv' or 'sent'"),
("divisor", "divide all byte values by this value"),
("recv_limit", "Expected max KiB/s. This value controls the drawing color of receive speed"),
("sent_limit", "Expected max KiB/s. similar with receive_limit"),
("freq_divisor", "divide Wifi frequency by this value"),
("ignore_interfaces", "Array of interfaces to ignore when cycling through "
"on click, eg, ['lo']"),
("round_size", "defines number of digits in round"),
("detached_down", "If the interface doesn't exist, display it as if it were down"),
("unknown_up", "If the interface is in unknown state, display it as if it were up"),
("next_if_down", "Change to next interface if current one is down"),
("detect_active", "Attempt to detect the active interface"),
("auto_units", "if true, unit of measurement is switched automatically (KB/MB/GB/...)"),
)
# Continue processing statistics when i3bar is hidden.
keep_alive = True
interval = 1
interface = 'eth0'
format_up = "{interface} {network_graph_recv}{bytes_recv}KB/s"
format_active_up = {}
format_down = "{interface}: DOWN"
color_up = "#00FF00"
color_down = "#FF0000"
dynamic_color = True
coloring_type = 'recv'
graph_width = 15
graph_style = 'blocks'
graph_direction = 'left-to-right'
recv_limit = 2048
sent_limit = 1024
separate_color = False
next_if_down = False
detect_active = False
# Network traffic settings
divisor = 1024
round_size = 0
auto_units = False
# Network info settings
detached_down = True
unknown_up = False
ignore_interfaces = ["lo"]
freq_divisor = 1
on_leftclick = "nm-connection-editor"
on_rightclick = "cycle_interface"
on_upscroll = ['cycle_interface', 1]
on_downscroll = ['cycle_interface', -1]
def init(self):
# Don't require importing basiciw unless using the functionality it offers.
if any(s in self.format_down or s in self.format_up
or any(s in f for f in self.format_active_up.values())
for s in ['essid', 'freq', 'quality', 'quality_bar']):
get_wifi_info = True
else:
get_wifi_info = False
self.network_info = NetworkInfo(self.interface, self.ignore_interfaces, self.detached_down, self.unknown_up,
self.freq_divisor, get_wifi_info)
# Don't require importing psutil unless using the functionality it offers.
if any(s in self.format_up or s in self.format_down for s in
['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', 'network_graph_recv',
'network_graph_sent', 'rx_tot_Mbytes', 'tx_tot_Mbytes', 'tx_tot', 'rx_tot']):
self.network_traffic = NetworkTraffic(self.unknown_up)
else:
self.network_traffic = None
if not self.dynamic_color:
self.end_color = self.start_color = self.color_up
self.colors = self.get_hex_color_range(self.start_color, self.end_color, 100)
self.kbs_recv_arr = [0.0] * self.graph_width
self.kbs_sent_arr = [0.0] * self.graph_width
self.pango_enabled = self.hints.get("markup", False) and self.hints["markup"] == "pango"
# convert settings from the nominated unit to bytes (backwards compatibility)
self.sent_limit *= 1024
self.recv_limit *= 1024
self.graph_direction = self.graph_direction.lower()
if self.graph_direction not in ('left-to-right', 'right-to-left'):
raise Exception("Invalid direction '%s'." % self.graph_direction)
def cycle_interface(self, increment=1):
"""Cycle through available interfaces in `increment` steps. Sign indicates direction."""
interfaces = [i for i in netifaces.interfaces() if i not in self.ignore_interfaces]
if self.interface in interfaces:
next_index = (interfaces.index(self.interface) + increment) % len(interfaces)
self.interface = interfaces[next_index]
elif len(interfaces) > 0:
self.interface = interfaces[0]
if self.network_traffic:
self.network_traffic.clear_counters()
self.kbs_arr = [0.0] * self.graph_width
def get_network_graph_recv(self, kbs, limit):
# Cycle array by inserting at the start and chopping off the last element
self.kbs_recv_arr.insert(0, kbs)
self.kbs_recv_arr = self.kbs_recv_arr[:self.graph_width]
graph = make_graph(self.kbs_recv_arr, 0.0, limit, self.graph_style)
if self.graph_direction == 'right-to-left':
return graph[::-1]
else:
return graph
def get_network_graph_sent(self, kbs, limit):
# Cycle array by inserting at the start and chopping off the last element
self.kbs_sent_arr.insert(0, kbs)
self.kbs_sent_arr = self.kbs_sent_arr[:self.graph_width]
graph = make_graph(self.kbs_sent_arr, 0.0, limit, self.graph_style)
if self.graph_direction == 'right-to-left':
return graph[::-1]
else:
return graph
def run(self):
format_values = dict(network_graph_recv="", network_graph_sent="", bytes_sent="", bytes_recv="",
packets_sent="", packets_recv="", rx_tot_Mbytes="", tx_tot_Mbytes="",
interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="",
essid="", freq="", quality="", quality_bar="", rx_tot='', tx_tot="")
if self.detect_active:
self.interface = detect_active_interface(self.ignore_interfaces, self.interface)
if self.network_traffic:
network_usage = self.network_traffic.get_usage(self.interface)
format_values.update(network_usage)
format_values['network_graph_recv'] = self.get_network_graph_recv(network_usage['bytes_recv'], self.recv_limit)
format_values['network_graph_sent'] = self.get_network_graph_sent(network_usage['bytes_sent'], self.sent_limit)
format_values['tx_tot_Mbytes'] = network_usage['tx_total'] / (1024 * 1024)
format_values['rx_tot_Mbytes'] = network_usage['rx_total'] / (1024 * 1024)
format_values['rx_tot'] = '{value:.{round}f}{unit}'.format(
round=self.round_size, **bytes_info_dict(network_usage['rx_total']))
format_values['tx_tot'] = '{value:.{round}f}{unit}'.format(
round=self.round_size, **bytes_info_dict(network_usage['tx_total']))
if self.dynamic_color:
if self.separate_color and self.pango_enabled:
color = self.color_up
color_template = "{}"
per_recv = network_usage["bytes_recv"] / self.recv_limit
per_sent = network_usage["bytes_sent"] / self.sent_limit
c_recv = self.get_gradient(int(per_recv * 100), self.colors, 100)
c_sent = self.get_gradient(int(per_sent * 100), self.colors, 100)
format_values['network_graph_recv'] = color_template.format(c_recv, format_values["network_graph_recv"])
format_values['network_graph_sent'] = color_template.format(c_sent, format_values["network_graph_sent"])
else:
if self.coloring_type == "recv":
color = self.get_gradient(network_usage['bytes_recv'], self.colors, self.recv_limit)
elif self.coloring_type == "sent":
color = self.get_gradient(network_usage['bytes_sent'], self.colors, self.sent_limit)
else:
raise Exception("coloring_type must be either 'recv' or 'sent'!")
else:
color = None
else:
color = None
if sysfs_interface_up(self.interface, self.unknown_up):
if not color:
color = self.color_up
format_str = self.format_up
if self.detect_active:
for pattern in self.format_active_up:
if fnmatch(self.interface, pattern):
format_str = self.format_active_up.get(pattern, self.format_up)
else:
color = self.color_down
format_str = self.format_down
if self.next_if_down:
self.cycle_interface()
network_info = self.network_info.get_info(self.interface)
format_values.update(network_info)
format_values['interface'] = self.interface
if self.network_traffic:
for metric in ('bytes_recv', 'bytes_sent'):
if self.auto_units:
format_values[metric] = '{value:.{round}f}{unit}'.format(
round=self.round_size, **bytes_info_dict(format_values[metric]))
else:
format_values[metric] = '{:.{round}f}'.format(format_values[metric] / self.divisor,
round=self.round_size)
if self.dynamic_color and self.separate_color and self.pango_enabled:
format_values["bytes_recv"] = color_template.format(c_recv, format_values["bytes_recv"])
format_values["bytes_sent"] = color_template.format(c_sent, format_values["bytes_sent"])
self.data = format_values
self.output = {
"full_text": formatp(format_str, **format_values).strip(),
'color': color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/now_playing.py 0000664 0000000 0000000 00000020471 13567273623 0023407 0 ustar 00root root 0000000 0000000 from os.path import basename
import dbus
from i3pystatus import IntervalModule, formatp
from i3pystatus.core.util import TimeWrapper
class Dbus:
obj_dbus = "org.freedesktop.DBus"
path_dbus = "/org/freedesktop/DBus"
obj_player = "org.mpris.MediaPlayer2"
path_player = "/org/mpris/MediaPlayer2"
intf_props = obj_dbus + ".Properties"
intf_player = obj_player + ".Player"
class NoPlayerException(Exception):
pass
class NowPlaying(IntervalModule):
"""
Shows currently playing track information. Supports media players that \
conform to the Media Player Remote Interfacing Specification.
* Requires ``python-dbus`` from your distro package manager, or \
``dbus-python`` from PyPI.
Left click on the module to play/pause, and right click to go to the next \
track.
.. rubric:: Available formatters (uses :ref:`formatp`)
* `{title}` — (the title of the current song)
* `{album}` — (the album of the current song, can be an empty string \
(e.g. for online streams))
* `{artist}` — (can be empty, too)
* `{filename}` — (file name with out extension and path; empty unless \
title is empty)
* `{song_elapsed}` — (position in the currently playing song, uses \
:ref:`TimeWrapper`, default is `%m:%S`)
* `{song_length}` — (length of the current song, same as song_elapsed)
* `{status}` — (play, pause, stop mapped through the `status` dictionary)
* `{volume}` — (volume)
.. rubric:: Available callbacks
* ``playpause`` — Plays if paused or stopped, otherwise pauses.
* ``next_song`` — Goes to next track in the playlist.
* ``player_command`` — Invoke a command with the `MediaPlayer2.Player` \
interface. The method name and its arguments are appended as list elements.
* ``player_prop`` — Get or set a property of the `MediaPlayer2.Player` \
interface. Append the property name to get, or the name and a value to set.
`MediaPlayer2.Player` methods and properties are documented at \
https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html
Your player may not support the full interface.
Example module registration with callbacks:
::
status.register("now_playing",
on_leftclick=["player_command", "PlayPause"],
on_rightclick=["player_command", "Stop"],
on_middleclick=["player_prop", "Shuffle", True],
on_upscroll=["player_command", "Seek", -10000000],
on_downscroll=["player_command", "Seek", +10000000])
"""
interval = 1
settings = (
("player", "Player name. If not set, compatible players will be \
detected automatically."),
("status", "Dictionary mapping pause, play and stop to output text"),
("format", "formatp string"),
("color", "Text color"),
("format_no_player", "Text to show if no player is detected"),
("color_no_player", "Text color when no player is detected"),
("hide_no_player", "Hide output if no player is detected"),
)
hide_no_player = True
format_no_player = "No Player"
color_no_player = "#ffffff"
format = "{title} {status}"
color = "#ffffff"
status = {
"pause": "▷",
"play": "▶",
"stop": "◾",
}
statusmap = {
"Playing": "play",
"Paused": "pause",
"Stopped": "stop",
}
on_leftclick = "playpause"
on_rightclick = "next_song"
on_upscroll = 'volume_up'
on_downscroll = 'volume_down'
player = None
old_player = None
def find_player(self):
obj = dbus.SessionBus().get_object(Dbus.obj_dbus, Dbus.path_dbus)
def get_players(methodname):
method = obj.get_dbus_method(methodname, Dbus.obj_dbus)
return [a for a in method() if a.startswith(Dbus.obj_player + ".")]
players = get_players('ListNames')
if not players:
players = get_players('ListActivatableNames')
if self.old_player in players:
return self.old_player
if not players:
raise NoPlayerException()
self.old_player = players[0]
return players[0]
def get_player(self):
if self.player:
player = Dbus.obj_player + "." + self.player
try:
return dbus.SessionBus().get_object(player, Dbus.path_player)
except dbus.exceptions.DBusException:
raise NoPlayerException()
else:
player = self.find_player()
return dbus.SessionBus().get_object(player, Dbus.path_player)
def run(self):
try:
currentsong = self.get_player_prop("Metadata")
fdict = {
"status": self.status[self.statusmap[
self.get_player_prop("PlaybackStatus")]],
# TODO: Use optional(!) TrackList interface for this to
# gain 100 % mpd<->now_playing compat
"len": 0,
"pos": 0,
"volume": int(self.get_player_prop("Volume", 0) * 100),
"title": currentsong.get("xesam:title", ""),
"album": currentsong.get("xesam:album", ""),
"artist": ", ".join(currentsong.get("xesam:artist", "")),
"song_length": TimeWrapper(
(currentsong.get("mpris:length") or 0) / 1000 ** 2),
"song_elapsed": TimeWrapper(
(self.get_player_prop("Position") or 0) / 1000 ** 2),
"filename": "",
}
if not fdict["title"]:
fdict["filename"] = '.'.join(
basename((currentsong.get("xesam:url") or "")).
split('.')[:-1])
self.data = fdict
self.output = {
"full_text": formatp(self.format, **fdict).strip(),
"color": self.color,
}
except NoPlayerException:
if self.hide_no_player:
self.output = None
else:
self.output = {
"full_text": self.format_no_player,
"color": self.color_no_player,
}
if hasattr(self, "data"):
del self.data
return
except dbus.exceptions.DBusException as e:
if self.hide_no_player:
self.output = None
else:
self.output = {
"full_text": "DBus error: " + e.get_dbus_message(),
"color": "#ff0000",
}
if hasattr(self, "data"):
del self.data
return
def playpause(self):
self.player_command('PlayPause')
def next_song(self):
self.player_command('Next')
def volume_up(self):
self.set_player_prop('Volume', self.volume + 1.0)
def volume_down(self):
self.set_player_prop('Volume', self.volume - 1.0)
@property
def volume(self):
return self.get_player_prop('Volume')
def player_command(self, command, *args):
try:
interface = dbus.Interface(self.get_player(), Dbus.intf_player)
return getattr(interface, command)(*args)
except NoPlayerException:
return
except dbus.exceptions.DBusException:
return
def get_player_prop(self, name, default=None):
properties = dbus.Interface(self.get_player(), Dbus.intf_props)
try:
return properties.Get(Dbus.intf_player, name)
except dbus.exceptions.DBusException:
return default
def set_player_prop(self, name, value):
properties = dbus.Interface(self.get_player(), Dbus.intf_props)
try:
return properties.Set(Dbus.intf_player, name, value)
except dbus.exceptions.DBusException as e:
self.logger.error('error setting player property: %s', e)
return
def player_prop(self, name, value=None):
try:
properties = dbus.Interface(self.get_player(), Dbus.intf_props)
# None/null/nil implies get because it's not a valid DBus datatype.
if value is None:
return properties.Get(Dbus.intf_player, name)
else:
properties.Set(Dbus.intf_player, name, value)
except NoPlayerException:
return
except dbus.exceptions.DBusException:
return
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/online.py 0000664 0000000 0000000 00000001563 13567273623 0022346 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.util import internet
class Online(IntervalModule):
"""Show internet connection status."""
settings = (
("color", "Text color when online"),
('color_offline', 'Text color when offline'),
('format_online', 'Status text when online'),
('format_offline', 'Status text when offline'),
("interval", "Update interval"),
)
color = '#ffffff'
color_offline = '#ff0000'
format_online = 'online'
format_offline = 'offline'
interval = 10
def run(self):
if internet():
self.output = {
"color": self.color,
"full_text": self.format_online,
}
else:
self.output = {
"color": self.color_offline,
"full_text": self.format_offline,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/openfiles.py 0000664 0000000 0000000 00000001375 13567273623 0023047 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
class Openfiles(IntervalModule):
"""
Displays the current/max open files.
"""
settings = (
("filenr_path", "Location to file-nr (usually /proc/sys/fs/file-nr"),
"color",
"format"
)
color = 'FFFFFF'
interval = 30
filenr_path = '/proc/sys/fs/file-nr'
format = "open/max: {openfiles}/{maxfiles}"
def run(self):
cur_filenr = open(self.filenr_path, 'r')
openfiles, unused, maxfiles = cur_filenr.readlines()[0].split()
cur_filenr.close()
cdict = {'openfiles': openfiles,
'maxfiles': maxfiles}
self.output = {
"full_text": self.format.format(**cdict),
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/openstack_vms.py 0000664 0000000 0000000 00000004346 13567273623 0023740 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
# requires python-novaclient
from novaclient import client
import webbrowser
class Openstack_vms(IntervalModule):
"""
Displays the number of VMs in an openstack cluster in ACTIVE and
non-ACTIVE states.
Requires: python-novaclient
"""
settings = (
("auth_url", "OpenStack cluster authentication URL (OS_AUTH_URL)"),
("username", "Username for OpenStack authentication (OS_USERNAME)"),
("password", "Password for Openstack authentication (OS_PASSWORD)"),
("tenant_name", "Tenant/Project name to view (OS_TENANT_NAME)"),
("color", "Display color when non-active VMs are =< `threshold`"),
("crit_color", "Display color when non-active VMs are => `threshold`"),
("threshold", "Set critical indicators when non-active VM pass this "
"number"),
("horizon_url", "When clicked, open this URL in a browser"),
"format"
)
required = ("auth_url", "password", "tenant_name", "username")
color = "#00FF00"
crit_color = "#FF0000"
threshold = 0
horizon_url = None
format = "{tenant_name}: {active_servers} up, "\
"{nonactive_servers} down"
on_leftclick = "openurl"
def run(self):
nclient = client.Client(
'2.0',
self.username,
self.password,
self.tenant_name,
self.auth_url
)
active_servers = 0
nonactive_servers = 0
server_list = nclient.servers.list()
for server in server_list:
if server.status == 'ACTIVE':
active_servers = active_servers + 1
else:
nonactive_servers = nonactive_servers + 1
if nonactive_servers > self.threshold:
display_color = self.crit_color
else:
display_color = self.color
cdict = {
"tenant_name": self.tenant_name,
"active_servers": active_servers,
"nonactive_servers": nonactive_servers,
}
self.data = cdict
self.output = {
"full_text": self.format.format(**cdict),
"color": display_color
}
def openurl(self):
webbrowser.open_new_tab(self.horizon_url)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/openvpn.py 0000664 0000000 0000000 00000006352 13567273623 0022550 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.command import run_through_shell
__author__ = 'facetoe'
class OpenVPN(IntervalModule):
"""
Monitor OpenVPN connections.
.. note::
This module currently only supports systemd. Additionally, as of
OpenVPN 2.4 the unit names have changed, as the OpenVPN server and
client now have distinct unit files (``openvpn-server@.service`` and
``openvpn-client@.service``, respectively). Those who have updated to
OpenVPN 2.4 will need to manually set the ``status_command``,
``vpn_up_command``, and ``vpn_down_command``.
Formatters:
* {vpn_name} — Same as setting.
* {status} — Unicode up or down symbol.
* {output} — Output of status_command.
* {label} — Label for this connection, if defined.
"""
color_up = "#00ff00"
color_down = "#FF0000"
status_up = '▲'
status_down = '▼'
format = "{vpn_name} {status}"
use_new_service_name = False
status_command = "bash -c 'systemctl show openvpn@%(vpn_name)s | grep ActiveState=active'"
vpn_up_command = "sudo /bin/systemctl start openvpn@%(vpn_name)s.service"
vpn_down_command = "sudo /bin/systemctl stop openvpn@%(vpn_name)s.service"
connected = False
label = ''
vpn_name = ''
settings = (
("format", "Format string"),
("color_up", "VPN is up"),
("color_down", "VPN is down"),
("status_down", "Symbol to display when down"),
("status_up", "Symbol to display when up"),
("vpn_name", "Name of VPN"),
("use_new_service_name", "Use new openvpn service names (openvpn 2.4^)"),
("vpn_up_command", "Command to bring up the VPN - default requires editing /etc/sudoers"),
("vpn_down_command", "Command to bring up the VPN - default requires editing /etc/sudoers"),
("status_command", "command to find out if the VPN is active"),
)
def init(self):
if not self.vpn_name:
raise Exception("vpn_name is required")
if self.use_new_service_name:
self.status_command = "bash -c 'systemctl show openvpn-client@%(vpn_name)s | grep ActiveState=active'"
self.vpn_up_command = "sudo /bin/systemctl start openvpn-client@%(vpn_name)s.service"
self.vpn_down_command = "sudo /bin/systemctl stop openvpn-client@%(vpn_name)s.service"
def toggle_connection(self):
if self.connected:
command = self.vpn_down_command
else:
command = self.vpn_up_command
run_through_shell(command % {'vpn_name': self.vpn_name}, enable_shell=True)
def on_click(self, button, **kwargs):
self.toggle_connection()
def run(self):
command_result = run_through_shell(self.status_command % {'vpn_name': self.vpn_name}, enable_shell=True)
self.connected = True if command_result.out.strip() else False
if self.connected:
color, status = self.color_up, self.status_up
else:
color, status = self.color_down, self.status_down
vpn_name = self.vpn_name
label = self.label
self.data = locals()
self.output = {
"full_text": self.format.format(**locals()),
'color': color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pagerduty.py 0000664 0000000 0000000 00000004455 13567273623 0023071 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.util import internet, require, formatp
import pypd
__author__ = 'chestm007'
class PagerDuty(IntervalModule):
"""
Module to get the current incidents in PD
Requires `pypd`
Formatters:
* `{num_incidents}` - current number of incidents unresolved
* `{num_acknowledged_incidents}` - as it sounds
* `{num_triggered_incidents}` - number of unacknowledged incidents
Example:
.. code-block:: python
status.register(
'pagerduty',
api_key='mah_api_key',
user_id='LKJ19QW'
)
"""
settings = (
'format',
('api_key', 'pagerduty api key'),
('color', 'module text color'),
('interval', 'refresh interval'),
('user_id', 'your pagerduty user id, shows up in the url when viewing your profile '
'`https://subdomain.pagerduty.com/users/`')
)
required = ['api_key']
format = '{num_triggered_incidents} triggered {num_acknowledged_incidents} acknowledged'
api_key = None
color = '#AA0000'
interval = 60
user_id = None
api_search_dict = dict(statuses=['triggered', 'acknowledged'])
num_acknowledged_incidents = None
num_triggered_incidents = None
num_incidents = None
def init(self):
pypd.api_key = self.api_key
if self.user_id:
self.api_search_dict['user_ids'] = [self.user_id]
@require(internet)
def run(self):
pd_incidents = pypd.Incident.find(**self.api_search_dict)
incidents = {
'acknowledged': [],
'triggered': [],
'all': []
}
for incident in pd_incidents:
incidents['all'].append(incident)
status = incident.get('status')
if status == 'acknowledged':
incidents['acknowledged'].append(incident)
elif status == 'triggered':
incidents['triggered'].append(incident)
self.num_acknowledged_incidents = len(incidents.get('acknowledged'))
self.num_triggered_incidents = len(incidents.get('triggered'))
self.num_incidents = len(incidents.get('all'))
self.output = dict(
full_text=formatp(self.format, **vars(self)),
color=self.color
)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/parcel.py 0000664 0000000 0000000 00000013305 13567273623 0022325 0 ustar 00root root 0000000 0000000 from urllib.request import urlopen
import webbrowser
import lxml.html
from lxml.cssselect import CSSSelector
from i3pystatus import IntervalModule
from i3pystatus.core.util import internet, require
class TrackerAPI:
def __init__(self, idcode):
pass
def status(self):
return {}
def get_url(self):
return ""
class DPD(TrackerAPI):
URL = "https://tracking.dpd.de/cgi-bin/simpleTracking.cgi?parcelNr={idcode}&type=1"
def __init__(self, idcode):
self.idcode = idcode
self.url = self.URL.format(idcode=self.idcode)
def status(self):
ret = {}
progress = "n/a"
status = "n/a"
with urlopen(self.url) as page:
page = page.read()
page = page.decode("UTF-8")
page = page[1:-1] # strip parenthesis of the data
try:
import json
data = json.loads(page)
status = data["TrackingStatusJSON"]["statusInfos"][-1]["contents"][0]["label"]
delivery_status = data["TrackingStatusJSON"]["shipmentInfo"]["deliveryStatus"]
# I'm not exactly sure what the deliveryStatus values mean.
# This may break if the package can't get delivered etc.
progress = delivery_status * 20
except:
pass
ret["progress"] = progress
ret["status"] = status
return ret
def get_url(self):
return "https://tracking.dpd.de/parcelstatus?query={idcode}".format(idcode=self.idcode)
class DHL(TrackerAPI):
URL = "http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc={idcode}"
def __init__(self, idcode):
self.idcode = idcode
self.url = self.URL.format(idcode=self.idcode)
def get_progress(self, page):
elements = page.xpath('//div[contains(@class, "package-status")]/div/ol/li')
progress = "n/a"
status = 0
for i, element in enumerate(elements, 1):
picture_link = ''.join(element.xpath('./img/@src')).lower()
if picture_link.endswith("_on.svg"):
status = ''.join(element.xpath('./img/@alt'))
progress = '%i' % (i / len(elements) * 100)
return progress, status
def status(self):
ret = {}
with urlopen(self.url) as page:
page = lxml.html.fromstring(page.read())
progress, status = self.get_progress(page)
ret["progress"] = progress
ret["status"] = status
return ret
def get_url(self):
return self.url
class UPS(TrackerAPI):
URL = "http://wwwapps.ups.com/WebTracking/processRequest?HTMLVersion=5.0&Requester=NES&AgreeToTermsAndConditions=yes&loc=en_US&tracknum={idcode}"
def __init__(self, idcode):
self.idcode = idcode
self.url = self.URL.format(idcode=self.idcode)
error_selector = CSSSelector(".secBody .error")
self.error = lambda page: len(error_selector(page)) >= 1
self.status_selector = CSSSelector("#tt_spStatus")
self.progress_selector = CSSSelector(".pkgProgress div")
def status(self):
ret = {}
with urlopen(self.url) as page:
page = lxml.html.fromstring(page.read())
if self.error(page):
ret["progress"] = ret["status"] = "n/a"
else:
ret["status"] = self.status_selector(page)[0].text.strip()
progress_cls = int(
int(self.progress_selector(page)[0].get("class").strip("staus")) / 5 * 100)
ret["progress"] = progress_cls
return ret
def get_url(self):
return self.url
class Itella(TrackerAPI):
def __init__(self, idcode, lang="fi"):
self.idcode = idcode
self.lang = lang
def status(self):
from bs4 import BeautifulSoup as BS
page = BS(urlopen(
"http://www.itella.fi/itemtracking/itella/search_by_shipment_id"
"?lang={lang}&ShipmentId={s_id}".format(
s_id=self.idcode, lang=self.lang)
).read())
events = page.find(id="shipment-event-table")
newest = events.find(id="shipment-event-table-cell")
status = newest.find(
"div", {"class": "shipment-event-table-header"}
).text.strip()
time, location = [
d.text.strip() for d in
newest.find_all("span", {"class": "shipment-event-table-data"})
][:2]
progress = "{status} {time} {loc}".format(status=status, time=time, loc=location)
return {
"name": self.name,
"status": status,
"location": location,
"time": time,
"progress": progress,
}
class ParcelTracker(IntervalModule):
"""
Used to track parcel/shipments.
Supported carriers: DHL, UPS, DPD, Itella
- parcel.UPS("")
- parcel.DHL("")
- parcel.DPD("")
- parcel.Itella(""[, "en"|"fi"|"sv"])
Second parameter is language. Requires beautiful soup 4 (bs4)
Requires lxml and cssselect.
"""
interval = 60
settings = (
("instance", "Tracker instance, for example ``parcel.UPS('your_id_code')``"),
"format",
"name",
)
required = ("instance", "name")
format = "{name}:{progress}"
on_leftclick = "open_browser"
@require(internet)
def run(self):
fdict = {
"name": self.name,
}
fdict.update(self.instance.status())
self.data = fdict
self.output = {
"full_text": self.format.format(**fdict).strip(),
"instance": self.name,
}
def open_browser(self):
webbrowser.open_new_tab(self.instance.get_url())
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pianobar.py 0000664 0000000 0000000 00000003001 13567273623 0022642 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
class Pianobar(IntervalModule):
"""
Shows the title and artist name of the current music
In pianobar config file must be setted the fifo and event_command options
(see man pianobar for more information)
For the event_cmd use:
https://github.com/jlucchese/pianobar/blob/master/contrib/pianobar-song-i3.sh
Mouse events:
- Left click play/pauses
- Right click plays next song
- Scroll up/down changes volume
"""
settings = (
("format"),
("songfile", "File generated by pianobar eventcmd"),
("ctlfile", "Pianobar fifo file"),
("color", "The color of the text"),
)
format = "{songtitle} -- {songartist}"
required = ("format", "songfile", "ctlfile")
color = "#FFFFFF"
on_leftclick = "playpause"
on_rightclick = "next_song"
on_upscroll = "increase_volume"
on_downscroll = "decrease_volume"
def run(self):
with open(self.songfile, "r") as f:
contents = f.readlines()
sn = contents[0].strip()
sa = contents[1].strip()
self.output = {
"full_text": self.format.format(songtitle=sn, songartist=sa),
"color": self.color
}
def playpause(self):
open(self.ctlfile, "w").write("p")
def next_song(self):
open(self.ctlfile, "w").write("n")
def increase_volume(self):
open(self.ctlfile, "w").write(")")
def decrease_volume(self):
open(self.ctlfile, "w").write("(")
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/ping.py 0000664 0000000 0000000 00000005133 13567273623 0022014 0 ustar 00root root 0000000 0000000 import subprocess
from i3pystatus import IntervalModule
class Ping(IntervalModule):
"""
This module display the ping value between your computer and a host.
``switch_state`` callback can disable the Ping when desired.
``host`` propertie can be changed for set a specific host.
.. rubric:: Available formatters
* {ping} the ping value in milliseconds.
"""
interval = 5
settings = (
"color",
"format",
("color_disabled", "color when disabled"),
("color", "color when latency is below threshold"),
("color_bad", "color when latency is above threshold"),
("color_down", "color when ping fail"),
("format_disabled", "format string when disabled"),
("format_down", "format string when ping fail"),
("latency_threshold", "latency threshold in ms"),
("host", "host to ping")
)
color = "#FFFFFF"
color_bad = "#FFFF00"
color_down = "#FF0000"
color_disabled = None
disabled = False
format = "{ping} ms"
format_down = "down"
format_disabled = None
latency_threshold = 120
host = "8.8.8.8"
on_leftclick = "switch_state"
def init(self):
if not self.color_bad:
self.color_bad = self.color
if not self.color_down:
self.color_down = self.color
if not self.format_disabled:
self.format_disabled = self.format_down
if not self.color_disabled:
self.color_disabled = self.color_down
def switch_state(self):
self.disabled = not self.disabled
def ping_host(self):
p = subprocess.Popen(["ping", "-c1", "-w%d" % self.interval,
self.host], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
out, _ = p.communicate()
if p.returncode == 0:
return float(out.decode().split("\n")[1]
.split("time=")[1].split()[0])
else:
return None
def run(self):
if self.disabled:
self.output = {
"full_text": self.format_disabled,
"color": self.color_disabled
}
return
ping = self.ping_host()
if not ping:
self.output = {
"full_text": self.format_down,
"color": self.color_down
}
return
color = self.color
if ping > self.latency_threshold:
color = self.color_bad
self.output = {
"full_text": self.format.format(ping=ping),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/plexstatus.py 0000664 0000000 0000000 00000005772 13567273623 0023304 0 ustar 00root root 0000000 0000000 import xml.etree.ElementTree as ET
from i3pystatus import IntervalModule
from urllib.request import urlopen
class Plexstatus(IntervalModule):
"""
Displays what is currently being streamed from your Plex Media Server.
If you dont have an apikey you will need to follow this
https://support.plex.tv/hc/en-us/articles/204059436-Finding-your-account-token-X-Plex-Token
.. rubric:: Formatters
* `{title}` - title currently being streamed
* `{platform}` - plex recognised platform of the streamer
* `{product}` - plex product name on the streamer (Plex Web, Plex Media Player)
* `{address}` - address of the streamer
* `{streamer_os}` - operating system on the streaming device
"""
settings = (
"format",
"color",
("apikey", "Your Plex API authentication key"),
("address", "Hostname or IP address of the Plex Media Server"),
("port", "Port which Plex Media Server is running on"),
("interval", "Update interval"),
("stream_divider", "divider between stream info when multiple streams are active"),
("format_no_streams", "String that is shown if nothing is being streamed"),
)
required = ("apikey", "address")
color = "#00FF00" # green
no_stream_color = "#FF0000" # red
port = 32400
interval = 120
format_no_streams = None
format = "{platform}: {title}"
stream_divider = '-'
def run(self):
PMS_URL = '%s%s%s%s' % ('http://', self.address, ':', self.port)
PMS_STATUS_URI = '/status/sessions/?X-Plex-Token='
PMS_STATUS_URL = PMS_URL + PMS_STATUS_URI + self.apikey
response = urlopen(PMS_STATUS_URL)
xml_response = response.read()
tree = ET.fromstring(xml_response)
streams = []
for vid in tree.iter('Video'):
info = {'title': '',
'platform': '',
'product': '',
'address': '',
'streamer_os': ''}
try:
info['title'] = vid.attrib['title']
except AttributeError as e:
self.logger.error(e)
for play in vid.iter('Player'):
try:
info['platform'] = play.attrib['platform']
info['product'] = play.attrib['product']
info['address'] = play.attrib['address']
info['streamer_os'] = play.attrib['device']
except AttributeError as e:
self.logger.error(e)
streams.append(info)
self.data = streams
if len(streams) < 1:
self.output = {} if not self.format_no_streams else {
"full_text": self.format_no_streams,
"color": self.no_stream_color
}
else:
full_text = self.stream_divider.join(self.format.format(**s) for s in streams)
self.output = {
"full_text": full_text,
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pomodoro.py 0000664 0000000 0000000 00000010153 13567273623 0022713 0 ustar 00root root 0000000 0000000 import subprocess
from datetime import datetime, timedelta
from i3pystatus import IntervalModule
from i3pystatus.core.desktop import DesktopNotification
STOPPED = 0
RUNNING = 1
BREAK = 2
class Pomodoro(IntervalModule):
"""
This plugin shows Pomodoro timer.
Left click starts/restarts timer.
Right click stops it.
Example color settings.
.. code-block:: python
color_map = {
'stopped': '#2ECCFA',
'running': '#FFFF00',
'break': '#37FF00'
}
"""
settings = (
('sound',
'Path to sound file to play as alarm. Played by "aplay" utility'),
('pomodoro_duration',
'Working (pomodoro) interval duration in seconds'),
('break_duration', 'Short break duration in seconds'),
('long_break_duration', 'Long break duration in seconds'),
('short_break_count', 'Short break count before first long break'),
('format', 'format string, available formatters: current_pomodoro, '
'total_pomodoro, time'),
('inactive_format', 'format string to display when no timer is running'),
('color', 'dictionary containing a mapping of statuses to colours')
)
inactive_format = 'Start Pomodoro'
color_map = {
'stopped': '#2ECCFA',
'running': '#FFFF00',
'break': '#37FF00'
}
color = None
sound = None
interval = 1
short_break_count = 3
format = '☯ {current_pomodoro}/{total_pomodoro} {time}'
pomodoro_duration = 25 * 60
break_duration = 5 * 60
long_break_duration = 15 * 60
on_rightclick = "stop"
on_leftclick = "start"
def init(self):
# state could be either running/break or stopped
self.state = STOPPED
self.current_pomodoro = 0
self.total_pomodoro = self.short_break_count + 1 # and 1 long break
self.time = None
if self.color is not None and type(self.color) == dict:
self.color_map.update(self.color)
def run(self):
if self.time and datetime.utcnow() >= self.time:
if self.state == RUNNING:
self.state = BREAK
if self.current_pomodoro == self.short_break_count:
self.time = datetime.utcnow() + \
timedelta(seconds=self.long_break_duration)
else:
self.time = datetime.utcnow() + \
timedelta(seconds=self.break_duration)
text = 'Go for a break!'
else:
self.state = RUNNING
self.time = datetime.utcnow() + \
timedelta(seconds=self.pomodoro_duration)
text = 'Back to work!'
self.current_pomodoro = (self.current_pomodoro + 1) % self.total_pomodoro
self._alarm(text)
if self.state == RUNNING or self.state == BREAK:
min, sec = divmod((self.time - datetime.utcnow()).total_seconds(), 60)
text = '{:02}:{:02}'.format(int(min), int(sec))
sdict = {
'time': text,
'current_pomodoro': self.current_pomodoro + 1,
'total_pomodoro': self.total_pomodoro
}
color = self.color_map['running'] if self.state == RUNNING else self.color_map['break']
text = self.format.format(**sdict)
else:
text = self.inactive_format
color = self.color_map['stopped']
self.output = {
'full_text': text,
'color': color
}
def start(self):
self.state = RUNNING
self.time = datetime.utcnow() + timedelta(seconds=self.pomodoro_duration)
self.current_pomodoro = 0
def stop(self):
self.state = STOPPED
self.time = None
def _alarm(self, text):
notification = DesktopNotification(title='Alarm!', body=text)
notification.display()
if self.sound is not None:
subprocess.Popen(['aplay',
self.sound,
'-q'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pulseaudio/ 0000775 0000000 0000000 00000000000 13567273623 0022655 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pulseaudio/__init__.py 0000664 0000000 0000000 00000023322 13567273623 0024770 0 ustar 00root root 0000000 0000000 import os
import re
import subprocess
from i3pystatus import Module
from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.util import make_vertical_bar, make_bar
from .pulse import *
class PulseAudio(Module, ColorRangeModule):
"""
Shows volume of default PulseAudio sink (output).
- Requires amixer for toggling mute and incrementing/decrementing volume on scroll.
- Depends on the PyPI colour module - https://pypi.python.org/pypi/colour/0.0.5
.. rubric:: Example configuration
The example configuration below uses only unicode to display the volume (tested with otf-font-awesome)
.. code-block:: python
status.register(
"pulseaudio",
color_unmuted='#aa3300,
color_muted='#aa0500',
format_muted='\uf6a9',
format='{volume_bar}',
vertical_bar_width=1,
vertical_bar_glyphs=['\uf026 ', '\uf027 ', '\uf028']
)
.. rubric:: Available formatters
* `{volume}` — volume in percent (0...100)
* `{db}` — volume in decibels relative to 100 %, i.e. 100 % = 0 dB, 50 % = -18 dB, 0 % = -infinity dB
(the literal value for -infinity is `-∞`)
* `{muted}` — the value of one of the `muted` or `unmuted` settings
* `{volume_bar}` — unicode bar showing volume
* `{selected}` — show the format_selected string if selected sink is the configured one
"""
settings = (
"format",
("format_muted", "optional format string to use when muted"),
("format_selected", "string used to mark this sink if selected"),
"muted", "unmuted",
"color_muted", "color_unmuted",
("step", "percentage to increment volume on scroll"),
("sink", "sink name to use, None means pulseaudio default"),
("move_sink_inputs", "Move all sink inputs when we change the default sink"),
("bar_type", "type of volume bar. Allowed values are 'vertical' or 'horizontal'"),
("multi_colors", "whether or not to change the color from "
"'color_muted' to 'color_unmuted' based on volume percentage"),
("vertical_bar_width", "how many characters wide the vertical volume_bar should be"),
('vertical_bar_glyphs', 'custom array output as vertical bar instead of unicode bars')
)
muted = "M"
unmuted = ""
format = "♪: {volume}"
format_muted = None
format_selected = " 🗸"
currently_muted = False
has_amixer = False
color_muted = "#FF0000"
color_unmuted = "#FFFFFF"
vertical_bar_glyphs = None
sink = None
move_sink_inputs = True
step = 5
multi_colors = False
bar_type = 'vertical'
vertical_bar_width = 2
on_rightclick = "switch_mute"
on_doubleleftclick = "change_sink"
on_leftclick = "pavucontrol"
on_upscroll = "increase_volume"
on_downscroll = "decrease_volume"
def init(self):
"""Creates context, when context is ready context_notify_cb is called"""
# Wrap callback methods in appropriate ctypefunc instances so
# that the Pulseaudio C API can call them
self._context_notify_cb = pa_context_notify_cb_t(
self.context_notify_cb)
self._sink_info_cb = pa_sink_info_cb_t(self.sink_info_cb)
self._update_cb = pa_context_subscribe_cb_t(self.update_cb)
self._success_cb = pa_context_success_cb_t(self.success_cb)
self._server_info_cb = pa_server_info_cb_t(self.server_info_cb)
# Create the mainloop thread and set our context_notify_cb
# method to be called when there's updates relating to the
# connection to Pulseaudio
_mainloop = pa_threaded_mainloop_new()
_mainloop_api = pa_threaded_mainloop_get_api(_mainloop)
context = pa_context_new(_mainloop_api, "i3pystatus_pulseaudio".encode("ascii"))
pa_context_set_state_callback(context, self._context_notify_cb, None)
pa_context_connect(context, None, 0, None)
pa_threaded_mainloop_start(_mainloop)
self.colors = self.get_hex_color_range(self.color_muted, self.color_unmuted, 100)
self.sinks = []
def request_update(self, context):
"""Requests a sink info update (sink_info_cb is called)"""
pa_operation_unref(pa_context_get_sink_info_by_name(
context, self.current_sink.encode(), self._sink_info_cb, None))
def success_cb(self, context, success, userdata):
pass
@property
def current_sink(self):
if self.sink is not None:
return self.sink
self.sinks = subprocess.check_output(['pactl', 'list', 'short', 'sinks'],
universal_newlines=True).splitlines()
bestsink = None
state = 'DEFAULT'
for sink in self.sinks:
attribs = sink.split()
sink_state = attribs[-1]
if sink_state == 'RUNNING':
bestsink = attribs[1]
state = 'RUNNING'
elif sink_state in ('IDLE', 'SUSPENDED') and state == 'DEFAULT':
bestsink = attribs[1]
return bestsink
def server_info_cb(self, context, server_info_p, userdata):
"""Retrieves the default sink and calls request_update"""
server_info = server_info_p.contents
self.request_update(context)
def context_notify_cb(self, context, _):
"""Checks wether the context is ready
-Queries server information (server_info_cb is called)
-Subscribes to property changes on all sinks (update_cb is called)
"""
state = pa_context_get_state(context)
if state == PA_CONTEXT_READY:
pa_operation_unref(
pa_context_get_server_info(context, self._server_info_cb, None))
pa_context_set_subscribe_callback(context, self._update_cb, None)
pa_operation_unref(pa_context_subscribe(
context, PA_SUBSCRIPTION_EVENT_CHANGE | PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER, self._success_cb, None))
def update_cb(self, context, t, idx, userdata):
"""A sink property changed, calls request_update"""
if t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK == PA_SUBSCRIPTION_EVENT_SERVER:
pa_operation_unref(
pa_context_get_server_info(context, self._server_info_cb, None))
self.request_update(context)
def sink_info_cb(self, context, sink_info_p, eol, _):
"""Updates self.output"""
if sink_info_p:
sink_info = sink_info_p.contents
volume_percent = round(100 * sink_info.volume.values[0] / 0x10000)
volume_db = pa_sw_volume_to_dB(sink_info.volume.values[0])
self.currently_muted = sink_info.mute
if volume_db == float('-Infinity'):
volume_db = "-∞"
else:
volume_db = int(volume_db)
muted = self.muted if sink_info.mute else self.unmuted
if self.multi_colors and not sink_info.mute:
color = self.get_gradient(volume_percent, self.colors)
else:
color = self.color_muted if sink_info.mute else self.color_unmuted
if muted and self.format_muted is not None:
output_format = self.format_muted
else:
output_format = self.format
if self.bar_type == 'vertical':
volume_bar = make_vertical_bar(volume_percent, self.vertical_bar_width, glyphs=self.vertical_bar_glyphs)
elif self.bar_type == 'horizontal':
volume_bar = make_bar(volume_percent)
else:
raise Exception("bar_type must be 'vertical' or 'horizontal'")
selected = ""
dump = subprocess.check_output("pacmd dump".split(), universal_newlines=True)
for line in dump.split("\n"):
if line.startswith("set-default-sink"):
default_sink = line.split()[1]
if default_sink == self.current_sink:
selected = self.format_selected
self.output = {
"color": color,
"full_text": output_format.format(
muted=muted,
volume=volume_percent,
db=volume_db,
volume_bar=volume_bar,
selected=selected),
}
self.send_output()
elif eol < 0:
self.output = None
self.send_output()
def change_sink(self):
sinks = list(s.split()[1] for s in self.sinks)
if self.sink is None:
next_sink = sinks[(sinks.index(self.current_sink) + 1) %
len(sinks)]
else:
next_sink = self.current_sink
if self.move_sink_inputs:
sink_inputs = subprocess.check_output("pacmd list-sink-inputs".split(),
universal_newlines=True)
for input_index in re.findall(r'index:\s+(\d+)', sink_inputs):
command = "pacmd move-sink-input {} {}".format(input_index, next_sink)
# Not all applications can be moved and pulseaudio, and when
# this fail pacmd print error messaging
with open(os.devnull, 'w') as devnull:
subprocess.call(command.split(), stdout=devnull)
subprocess.call("pacmd set-default-sink {}".format(next_sink).split())
def switch_mute(self):
subprocess.call(['pactl', '--', 'set-sink-mute', self.current_sink, "toggle"])
def increase_volume(self):
subprocess.call(['pactl', '--', 'set-sink-volume', self.current_sink, "+%s%%" % self.step])
def decrease_volume(self):
subprocess.call(['pactl', '--', 'set-sink-volume', self.current_sink, "-%s%%" % self.step])
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pulseaudio/pulse.py 0000664 0000000 0000000 00000024331 13567273623 0024362 0 ustar 00root root 0000000 0000000 # generation commands
# h2xml.py -I $PWD -c -o pa.xml pulse/mainloop-api.h pulse/sample.h pulse/def.h pulse/operation.h pulse/context.h pulse/channelmap.h pulse/volume.h pulse/stream.h pulse/introspect.h pulse/subscribe.h pulse/scache.h pulse/version.h pulse/error.h pulse/xmalloc.h pulse/utf8.h pulse/thread-mainloop.h pulse/mainloop.h pulse/mainloop-signal.h pulse/util.h pulse/timeval.h
# xml2py.py -k efstd -o lib_pulseaudio.py -l 'pulse' -r '(pa|PA)_.+' pa.xml
from ctypes import *
_libraries = {}
_libraries['libpulse.so.0'] = CDLL('libpulse.so.0')
STRING = c_char_p
pa_volume_t = c_uint32
pa_channel_position_t = c_int
pa_usec_t = c_uint64
pa_channel_position_mask_t = c_uint64
PA_CONTEXT_READY = 4
PA_OK = 0
PA_OPERATION_CANCELLED = 2
PA_OPERATION_DONE = 1
PA_OPERATION_RUNNING = 0
PA_SUBSCRIPTION_EVENT_CHANGE = 16
PA_SUBSCRIPTION_EVENT_FACILITY_MASK = 15
PA_SUBSCRIPTION_EVENT_SERVER = 7
PA_SUBSCRIPTION_MASK_SINK = 1
PA_SUBSCRIPTION_MASK_SERVER = 0x80
class pa_sink_port_info(Structure):
pass
class pa_format_info(Structure):
pass
class pa_context(Structure):
pass
pa_context._fields_ = [
]
pa_context_notify_cb_t = CFUNCTYPE(None, POINTER(pa_context), c_void_p)
pa_context_success_cb_t = CFUNCTYPE(None, POINTER(pa_context), c_int, c_void_p)
class pa_proplist(Structure):
pass
pa_context_event_cb_t = CFUNCTYPE(
None, POINTER(pa_context), STRING, POINTER(pa_proplist), c_void_p)
class pa_mainloop_api(Structure):
pass
pa_context_new = _libraries['libpulse.so.0'].pa_context_new
pa_context_new.restype = POINTER(pa_context)
pa_context_new.argtypes = [POINTER(pa_mainloop_api), STRING]
pa_context_new_with_proplist = _libraries[
'libpulse.so.0'].pa_context_new_with_proplist
pa_context_new_with_proplist.restype = POINTER(pa_context)
pa_context_new_with_proplist.argtypes = [
POINTER(pa_mainloop_api), STRING, POINTER(pa_proplist)]
pa_context_unref = _libraries['libpulse.so.0'].pa_context_unref
pa_context_unref.restype = None
pa_context_unref.argtypes = [POINTER(pa_context)]
pa_context_ref = _libraries['libpulse.so.0'].pa_context_ref
pa_context_ref.restype = POINTER(pa_context)
pa_context_ref.argtypes = [POINTER(pa_context)]
pa_context_set_state_callback = _libraries[
'libpulse.so.0'].pa_context_set_state_callback
pa_context_set_state_callback.restype = None
pa_context_set_state_callback.argtypes = [
POINTER(pa_context), pa_context_notify_cb_t, c_void_p]
# values for enumeration 'pa_context_state'
pa_context_state = c_int # enum
pa_context_state_t = pa_context_state
pa_context_get_state = _libraries['libpulse.so.0'].pa_context_get_state
pa_context_get_state.restype = pa_context_state_t
pa_context_get_state.argtypes = [POINTER(pa_context)]
# values for enumeration 'pa_context_flags'
pa_context_flags = c_int # enum
pa_context_flags_t = pa_context_flags
class pa_spawn_api(Structure):
_fields_ = [
('prefork', CFUNCTYPE(None)),
('postfork', CFUNCTYPE(None)),
('atfork', CFUNCTYPE(None)),
]
pa_context_connect = _libraries['libpulse.so.0'].pa_context_connect
pa_context_connect.restype = c_int
pa_context_connect.argtypes = [
POINTER(pa_context), STRING, pa_context_flags_t, POINTER(pa_spawn_api)]
pa_context_disconnect = _libraries['libpulse.so.0'].pa_context_disconnect
pa_context_disconnect.restype = None
pa_context_disconnect.argtypes = [POINTER(pa_context)]
class pa_operation(Structure):
pass
class pa_sample_spec(Structure):
_fields_ = [
('format', c_int),
('rate', c_uint32),
('channels', c_uint8),
]
# values for enumeration 'pa_subscription_mask'
pa_subscription_mask = c_int # enum
pa_subscription_mask_t = pa_subscription_mask
# values for enumeration 'pa_subscription_event_type'
pa_subscription_event_type = c_int # enum
pa_subscription_event_type_t = pa_subscription_event_type
pa_context_subscribe_cb_t = CFUNCTYPE(
None, POINTER(pa_context), pa_subscription_event_type_t, c_uint32, c_void_p)
pa_context_subscribe = _libraries['libpulse.so.0'].pa_context_subscribe
pa_context_subscribe.restype = POINTER(pa_operation)
pa_context_subscribe.argtypes = [
POINTER(pa_context), pa_subscription_mask_t, pa_context_success_cb_t, c_void_p]
pa_context_set_subscribe_callback = _libraries[
'libpulse.so.0'].pa_context_set_subscribe_callback
pa_context_set_subscribe_callback.restype = None
pa_context_set_subscribe_callback.argtypes = [
POINTER(pa_context), pa_context_subscribe_cb_t, c_void_p]
# values for enumeration 'pa_sink_flags'
pa_sink_flags = c_int # enum
pa_sink_flags_t = pa_sink_flags
# values for enumeration 'pa_sink_state'
pa_sink_state = c_int # enum
pa_sink_state_t = pa_sink_state
pa_free_cb_t = CFUNCTYPE(None, c_void_p)
pa_strerror = _libraries['libpulse.so.0'].pa_strerror
pa_strerror.restype = STRING
pa_strerror.argtypes = [c_int]
class pa_sink_info(Structure):
pass
class pa_cvolume(Structure):
_fields_ = [
('channels', c_uint8),
('values', pa_volume_t * 32),
]
class pa_channel_map(Structure):
_fields_ = [
('channels', c_uint8),
('map', pa_channel_position_t * 32),
]
pa_sink_info._fields_ = [
('name', STRING),
('index', c_uint32),
('description', STRING),
('sample_spec', pa_sample_spec),
('channel_map', pa_channel_map),
('owner_module', c_uint32),
('volume', pa_cvolume),
('mute', c_int),
('monitor_source', c_uint32),
('monitor_source_name', STRING),
('latency', pa_usec_t),
('driver', STRING),
('flags', pa_sink_flags_t),
('proplist', POINTER(pa_proplist)),
('configured_latency', pa_usec_t),
('base_volume', pa_volume_t),
('state', pa_sink_state_t),
('n_volume_steps', c_uint32),
('card', c_uint32),
('n_ports', c_uint32),
('ports', POINTER(POINTER(pa_sink_port_info))),
('active_port', POINTER(pa_sink_port_info)),
('n_formats', c_uint8),
('formats', POINTER(POINTER(pa_format_info))),
]
pa_sink_info_cb_t = CFUNCTYPE(
None, POINTER(pa_context), POINTER(pa_sink_info), c_int, c_void_p)
pa_context_get_sink_info_by_name = _libraries[
'libpulse.so.0'].pa_context_get_sink_info_by_name
pa_context_get_sink_info_by_name.restype = POINTER(pa_operation)
pa_context_get_sink_info_by_name.argtypes = [
POINTER(pa_context), STRING, pa_sink_info_cb_t, c_void_p]
pa_context_get_sink_info_by_index = _libraries[
'libpulse.so.0'].pa_context_get_sink_info_by_index
pa_context_get_sink_info_by_index.restype = POINTER(pa_operation)
pa_context_get_sink_info_by_index.argtypes = [
POINTER(pa_context), c_uint32, pa_sink_info_cb_t, c_void_p]
pa_context_get_sink_info_list = _libraries[
'libpulse.so.0'].pa_context_get_sink_info_list
pa_context_get_sink_info_list.restype = POINTER(pa_operation)
pa_context_get_sink_info_list.argtypes = [
POINTER(pa_context), pa_sink_info_cb_t, c_void_p]
class pa_server_info(Structure):
pass
pa_server_info._fields_ = [
('user_name', STRING),
('host_name', STRING),
('server_version', STRING),
('server_name', STRING),
('sample_spec', pa_sample_spec),
('default_sink_name', STRING),
('default_source_name', STRING),
('cookie', c_uint32),
('channel_map', pa_channel_map),
]
pa_server_info_cb_t = CFUNCTYPE(
None, POINTER(pa_context), POINTER(pa_server_info), c_void_p)
pa_context_get_server_info = _libraries[
'libpulse.so.0'].pa_context_get_server_info
pa_context_get_server_info.restype = POINTER(pa_operation)
pa_context_get_server_info.argtypes = [
POINTER(pa_context), pa_server_info_cb_t, c_void_p]
class pa_threaded_mainloop(Structure):
pass
pa_threaded_mainloop._fields_ = [
]
pa_threaded_mainloop_new = _libraries['libpulse.so.0'].pa_threaded_mainloop_new
pa_threaded_mainloop_new.restype = POINTER(pa_threaded_mainloop)
pa_threaded_mainloop_new.argtypes = []
pa_threaded_mainloop_free = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_free
pa_threaded_mainloop_free.restype = None
pa_threaded_mainloop_free.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_start = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_start
pa_threaded_mainloop_start.restype = c_int
pa_threaded_mainloop_start.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_stop = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_stop
pa_threaded_mainloop_stop.restype = None
pa_threaded_mainloop_stop.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_lock = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_lock
pa_threaded_mainloop_lock.restype = None
pa_threaded_mainloop_lock.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_unlock = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_unlock
pa_threaded_mainloop_unlock.restype = None
pa_threaded_mainloop_unlock.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_wait = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_wait
pa_threaded_mainloop_wait.restype = None
pa_threaded_mainloop_wait.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_signal = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_signal
pa_threaded_mainloop_signal.restype = None
pa_threaded_mainloop_signal.argtypes = [POINTER(pa_threaded_mainloop), c_int]
pa_threaded_mainloop_accept = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_accept
pa_threaded_mainloop_accept.restype = None
pa_threaded_mainloop_accept.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_get_retval = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_get_retval
pa_threaded_mainloop_get_retval.restype = c_int
pa_threaded_mainloop_get_retval.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_get_api = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_get_api
pa_threaded_mainloop_get_api.restype = POINTER(pa_mainloop_api)
pa_threaded_mainloop_get_api.argtypes = [POINTER(pa_threaded_mainloop)]
pa_threaded_mainloop_in_thread = _libraries[
'libpulse.so.0'].pa_threaded_mainloop_in_thread
pa_threaded_mainloop_in_thread.restype = c_int
pa_threaded_mainloop_in_thread.argtypes = [POINTER(pa_threaded_mainloop)]
pa_sw_volume_to_dB = _libraries['libpulse.so.0'].pa_sw_volume_to_dB
pa_sw_volume_to_dB.restype = c_double
pa_sw_volume_to_dB.argtypes = [pa_volume_t]
pa_operation_unref = _libraries['libpulse.so.0'].pa_operation_unref
pa_operation_unref.restype = None
pa_operation_unref.argtypes = [POINTER(pa_operation)]
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pyload.py 0000664 0000000 0000000 00000006137 13567273623 0022354 0 ustar 00root root 0000000 0000000 import urllib.request
import urllib.parse
import urllib.error
import http.cookiejar
import webbrowser
import json
from i3pystatus import IntervalModule
class pyLoad(IntervalModule):
"""
Shows pyLoad status
.. rubric:: Available formatters
* `{captcha}` — see captcha_true and captcha_false, which are the values filled in for this formatter
* `{progress}` — average over all running downloads
* `{progress_all}` — percentage of completed files/links in queue
* `{speed}` — kilobytes/s
* `{download}` — downloads enabled, also see download_true and download_false
* `{total}` — number of downloads
* `{free_space}` — free space in download directory in gigabytes
"""
interval = 5
settings = (
("address", "Address of pyLoad webinterface"),
"format",
"captcha_true", "captcha_false",
"download_true", "download_false",
"username", "password",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
)
required = ("username", "password")
keyring_backend = None
address = "http://127.0.0.1:8000"
format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s"
captcha_true = "Captcha waiting"
captcha_false = ""
download_true = "Downloads enabled"
download_false = "Downloads disabled"
on_leftclick = "open_webbrowser"
def _rpc_call(self, method, data=None):
if not data:
data = {}
urlencoded = urllib.parse.urlencode(data).encode("ascii")
return json.loads(self.opener.open("{address}/api/{method}/".format(address=self.address, method=method),
urlencoded).read().decode("utf-8"))
def init(self):
self.cj = http.cookiejar.CookieJar()
self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(self.cj))
def login(self):
return self._rpc_call("login", {
"username": self.username,
"password": self.password,
})
def run(self):
self.login()
server_status = self._rpc_call("statusServer")
downloads_status = self._rpc_call("statusDownloads")
if downloads_status:
progress = sum(dl["percent"]
for dl in downloads_status) / len(downloads_status) * 100
else:
progress = 100.0
fdict = {
"download": self.download_true if server_status["download"] else self.download_false,
"speed": server_status["speed"] / 1024,
"progress": progress,
"progress_all": sum(pkg["linksdone"] for pkg in self._rpc_call("getQueue")) / server_status["total"] * 100,
"captcha": self.captcha_true if self._rpc_call("isCaptchaWaiting") else self.captcha_false,
"free_space": self._rpc_call("freeSpace") / (1024 ** 3),
}
self.data = fdict
self.output = {
"full_text": self.format.format(**fdict).strip(),
"instance": self.address,
}
def open_webbrowser(self):
webbrowser.open_new_tab(self.address)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/random_password.py 0000664 0000000 0000000 00000005636 13567273623 0024271 0 ustar 00root root 0000000 0000000 from i3pystatus import Module
import random
import string
import subprocess
class RandomPassword(Module):
"""
Generates a random password and copies it to the clipboard. Useful if you use any password manager and you want to generate a password in the moment and save it later in you manager's database.
Uses `SystemRandom` class as a cryptographically secure pseudo-number generator -
- Requires `xsel` or `xclip` for copying to the clipboard.
- Generates a new password with a left click by default.
- Generates a password with a default length of 12 and with lowercase, uppercase, digits and special symbols.
.. rubric:: Available formatters
* `{length}` — length of generated password
"""
settings = (
("format", "Format string to be displayed in the status bar"),
("length", "Length of the generated password"),
("charset", "Dictionary containing character types to be included in the password"),
("cliptool", "Currently supports xsel and xclip"),
("color", "HTML color hex code #RRGGBB"),
)
format = ''
length = 12
charset = ['lowercase', 'uppercase', 'digits', 'special']
cliptool = None
color = None
on_doubleleftclick = 'generate_password'
def init(self):
# Finds out if either xsel or xclip exist
self._find_cliptool()
cdict = {
'length': self.length
}
self.output = {
"full_text": self.format.format(**cdict)
}
if self.color:
self.output["color"] = self.color
def _find_cliptool(self):
if subprocess.call(['which', 'xsel'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
self.cliptool = 'xsel'
self._clip_params = ['-b', '-i']
elif subprocess.call(['which', 'xclip'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
self.cliptool = 'xclip'
self._clip_params = ['-selection', 'c']
# Asserts that either xsel or xclip was found
assert self.cliptool and self._clip_params, 'It was no possible to find xsel or xclip installed in your system.'
def generate_password(self):
# If a blank list is provided for the charset, it will generate an empty password
chars = ''
if 'lowercase' in self.charset:
chars = string.ascii_lowercase
if 'uppercase' in self.charset:
chars += string.ascii_uppercase
if 'digits' in self.charset:
chars += string.digits
if 'special' in self.charset:
chars += string.punctuation
passwd = ''.join(random.SystemRandom().choice(chars) for x in range(self.length))
p = subprocess.Popen([self.cliptool, self._clip_params[0], self._clip_params[1]], stdin=subprocess.PIPE, close_fds=True)
p.communicate(input=passwd.encode('utf-8'))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/reddit.py 0000664 0000000 0000000 00000014517 13567273623 0022340 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import re
import praw
from i3pystatus import IntervalModule
from i3pystatus.core.util import internet, require, user_open
class Reddit(IntervalModule):
"""
This module fetches and displays posts and/or user mail/messages from
reddit.com. Left-clicking on the display text opens the permalink/comments
page using webbrowser.open() while right-clicking opens the URL of the
submission directly. Depends on the Python Reddit API Wrapper (PRAW)
.
PRAW must be configured for this module to work.
https://praw.readthedocs.io/en/latest/
.. rubric:: Available formatters
* {submission_title}
* {submission_author}
* {submission_points}
* {submission_comments}
* {submission_permalink}
* {submission_url}
* {submission_domain}
* {submission_subreddit}
* {message_unread}
* {message_author}
* {message_subject}
* {message_body}
* {link_karma}
* {comment_karma}
"""
settings = (
("format", "Format string used for output."),
("username", "Reddit username."),
('keyring_backend', 'alternative keyring backend for retrieving \
credentials'),
("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."),
("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."),
("time_filter", "'all', 'day','hour', 'month', 'week', 'year'"),
("color", "Standard color."),
("colorize", "Enable color change on new message."),
("color_orangered", "Color for new messages."),
("mail_brackets", "Display unread message count in square-brackets."),
("title_maxlen", "Maximum number of characters to display in title."),
("interval", "Update interval."),
("status", "New message indicator."),
)
format = "[{submission_subreddit}] {submission_title} ({submission_domain})"
username = ""
keyring_backend = None
subreddit = ""
sort_by = "hot"
time_filter = "all"
color = "#FFFFFF"
colorize = True
color_orangered = "#FF4500"
mail_brackets = False
title_maxlen = 80
interval = 300
status = {
"new_mail": "✉",
"no_mail": "",
}
on_leftclick = "open_permalink"
_permalink = ""
_url = ""
subreddit_pattern = re.compile(r"{submission_\w+}")
message_pattern = re.compile(r"{message_\w+\}")
user_pattern = re.compile(r"{comment_karma}|{link_karma}")
reddit_session = None
@require(internet)
def run(self):
reddit = self.connect()
fdict = {}
if self.message_pattern.search(self.format):
fdict.update(self.get_messages(reddit))
if self.subreddit_pattern.search(self.format):
fdict.update(self.get_subreddit(reddit))
if self.user_pattern.search(self.format):
fdict.update(self.get_redditor(reddit))
if self.colorize and fdict.get("message_unread", False):
color = self.color_orangered
if self.mail_brackets:
fdict["message_unread"] = "[{}]".format(fdict["message_unread"])
else:
color = self.color
self.data = fdict
full_text = self.format.format(**fdict)
self.output = {
"full_text": full_text,
"color": color,
}
def connect(self):
if not self.reddit_session:
self.reddit_session = praw.Reddit(user_agent='i3pystatus',
disable_update_check=True)
return self.reddit_session
def get_redditor(self, reddit):
redditor_info = {}
u = reddit.redditor(self.username)
redditor_info["link_karma"] = u.link_karma
redditor_info["comment_karma"] = u.comment_karma
return redditor_info
def get_messages(self, reddit):
message_info = {
"message_unread": "",
"status": self.status["no_mail"],
"message_author": "",
"message_subject": "",
"message_body": ""
}
unread_messages = sum(1 for i in reddit.inbox.unread())
if unread_messages:
d = vars(next(reddit.inbox.unread()))
message_info = {
"message_unread": unread_messages,
"message_author": d["author"],
"message_subject": d["subject"],
"message_body": d["body"].replace("\n", " "),
"status": self.status["new_mail"]
}
return message_info
def get_subreddit(self, reddit):
fdict = {}
subreddit_dict = {}
if self.subreddit:
s = reddit.subreddit(self.subreddit)
else:
s = reddit.front
if self.sort_by == 'hot':
subreddit_dict = vars(next(s.hot(limit=1)))
elif self.sort_by == 'new':
subreddit_dict = vars(next(s.new(limit=1)))
elif self.sort_by == 'rising':
try:
subreddit_dict = vars(next(s.rising(limit=1)))
except StopIteration:
return
elif self.sort_by == 'controversial':
subreddit_dict = vars(next(s.controversial(
time_filter=self.time_filter,
limit=1))
)
elif self.sort_by == 'top':
subreddit_dict = vars(next(s.top(limit=1)))
fdict["submission_title"] = subreddit_dict["title"]
fdict["submission_author"] = subreddit_dict["author"]
fdict["submission_points"] = subreddit_dict["ups"]
fdict["submission_comments"] = subreddit_dict["num_comments"]
fdict["submission_permalink"] = subreddit_dict["permalink"]
fdict["submission_url"] = subreddit_dict["url"]
fdict["submission_domain"] = subreddit_dict["domain"]
fdict["submission_subreddit"] = subreddit_dict["subreddit"]
if len(fdict["submission_title"]) > self.title_maxlen:
title = fdict["submission_title"][:(self.title_maxlen - 3)] + "..."
fdict["submission_title"] = title
self._permalink = fdict["submission_permalink"]
self._url = fdict["submission_url"]
return fdict
def open_mail(self):
user_open('https://www.reddit.com/message/unread/')
def open_permalink(self):
user_open(self._permalink)
def open_link(self):
user_open(self._url)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/redshift.py 0000664 0000000 0000000 00000012744 13567273623 0022675 0 ustar 00root root 0000000 0000000 import os
import signal
import threading
from subprocess import Popen, PIPE
from i3pystatus import IntervalModule, formatp
class RedshiftController(threading.Thread):
def __init__(self, args=[]):
"""Initialize controller and start child process
The parameter args is a list of command line arguments to pass on to
the child process. The "-v" argument is automatically added."""
threading.Thread.__init__(self)
# Initialize state variables
self._inhibited = False
self._temperature = 0
self._period = 'Unknown'
self._location = (0.0, 0.0)
self._brightness = 0.0
self._pid = None
cmd = ["redshift"] + args
if "-v" not in cmd:
cmd += ["-v"]
env = os.environ.copy()
env['LANG'] = env['LANGUAGE'] = env['LC_ALL'] = env['LC_MESSAGES'] = 'C'
self._params = {
"args": cmd,
"env": env,
"bufsize": 1,
"stdout": PIPE,
"universal_newlines": True,
}
def parse_output(self, line):
"""Convert output to key value pairs"""
try:
key, value = line.split(":")
self.update_value(key.strip(), value.strip())
except ValueError:
pass
def update_value(self, key, value):
"""Parse key value pairs to update their values"""
if key == "Status":
self._inhibited = value != "Enabled"
elif key == "Color temperature":
self._temperature = int(value.rstrip("K"), 10)
elif key == "Period":
self._period = value
elif key == "Brightness":
self._brightness = value
elif key == "Location":
location = []
for x in value.split(", "):
v, d = x.split(" ")
location.append(float(v) * (1 if d in "NE" else -1))
self._location = (location)
@property
def inhibited(self):
"""Current inhibition state"""
return self._inhibited
@property
def temperature(self):
"""Current screen temperature"""
return self._temperature
@property
def period(self):
"""Current period of day"""
return self._period
@property
def location(self):
"""Current location"""
return self._location
@property
def brightness(self):
"""Current brightness"""
return self._brightness
def set_inhibit(self, inhibit):
"""Set inhibition state"""
if self._pid and inhibit != self._inhibited:
os.kill(self._pid, signal.SIGUSR1)
self._inhibited = inhibit
def run(self):
with Popen(**self._params) as proc:
self._pid = proc.pid
for line in proc.stdout:
self.parse_output(line)
proc.wait(10)
class Redshift(IntervalModule):
"""
Show status and control redshift - http://jonls.dk/redshift/.
This module runs an instance of redshift by itself, since it needs to parse
its output, so you should remove redshift/redshift-gtk from your i3 config
before using this module.
Requires `redshift` installed.
.. rubric:: Available formatters
* `{inhibit}` — show if redshift is currently On or Off (using `toggle_inhibit` callback)
* `{latitude}` — location latitude
* `{longitude}` — location longitude
* `{period}` — current period (Day or Night)
* `{temperature}` — current screen temperature in Kelvin scale (K)
"""
settings = (
("color", "Text color"),
("error_color", "Text color when an error occurs"),
"format",
("format_inhibit",
"List of 2 strings for `{inhibit}`, the first is shown when Redshift is On and the second is shown when Off"),
("redshift_parameters", "List of parameters to pass to redshift binary"),
)
color = "#ffffff"
error_color = "#ff0000"
format = "{inhibit} {temperature}K"
format_inhibit = ["On", "Off"]
on_leftclick = "toggle_inhibit"
redshift_parameters = []
def init(self):
self._controller = RedshiftController(self.redshift_parameters)
self._controller.daemon = True
self._controller.start()
self.update_values()
def update_values(self):
self.inhibit = self._controller.inhibited
self.period = self._controller.period
self.temperature = self._controller.temperature
self.latitude, self.longitude = self._controller.location
self.brightness = self._controller.brightness
def toggle_inhibit(self):
"""Enable/disable redshift"""
if self.inhibit:
self._controller.set_inhibit(False)
self.inhibit = False
else:
self._controller.set_inhibit(True)
self.inhibit = True
def run(self):
if self._controller.is_alive():
self.update_values()
fdict = {
"inhibit": self.format_inhibit[int(self.inhibit)],
"period": self.period,
"temperature": self.temperature,
"latitude": self.latitude,
"longitude": self.longitude,
"brightness": self.brightness,
}
output = formatp(self.format, **fdict)
color = self.color
else:
output = "redshift exited unexpectedly"
color = self.error_color
self.output = {
"full_text": output,
"color": color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/regex.py 0000664 0000000 0000000 00000001357 13567273623 0022175 0 ustar 00root root 0000000 0000000 import re
from i3pystatus import IntervalModule
class Regex(IntervalModule):
"""
Simple regex file watcher
The groups of the regex are passed to the format string as positional arguments.
"""
flags = 0
format = "{0}"
settings = (
("format", "format string used for output"),
"regex",
("file", "file to search for regex matches"),
("flags", "Python.re flags"),
)
required = ("regex", "file")
def init(self):
self.re = re.compile(self.regex, self.flags)
def run(self):
with open(self.file, "r") as f:
match = self.re.search(f.read())
self.output = {
"full_text": self.format.format(*match.groups()),
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/runwatch.py 0000664 0000000 0000000 00000002571 13567273623 0022715 0 ustar 00root root 0000000 0000000 import glob
import os.path
from i3pystatus import IntervalModule
class RunWatch(IntervalModule):
"""
Expands the given path using glob to a pidfile and checks
if the process ID found inside is valid
(that is, if the process is running).
You can use this to check if a specific application,
such as a VPN client or your DHCP client is running.
.. rubric:: Available formatters
* {pid}
* {name}
"""
format_up = "{name}"
format_down = "{name}"
color_up = "#00FF00"
color_down = "#FF0000"
settings = (
"format_up", "format_down",
"color_up", "color_down",
"path", "name",
)
required = ("path", "name")
@staticmethod
def is_process_alive(pid):
return os.path.exists("/proc/{pid}/".format(pid=pid))
def run(self):
alive = False
pid = 0
try:
with open(glob.glob(self.path)[0], "r") as f:
pid = int(f.read().strip())
alive = self.is_process_alive(pid)
except Exception:
pass
if alive:
fmt = self.format_up
color = self.color_up
else:
fmt = self.format_down
color = self.color_down
self.output = {
"full_text": fmt.format(name=self.name, pid=pid),
"color": color,
"instance": self.name
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/sabnzbd.py 0000664 0000000 0000000 00000006434 13567273623 0022507 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
import json
import webbrowser
class sabnzbd(IntervalModule):
"""
Displays the current status of SABnzbd.
A leftclick pauses/resumes downloading.
A rightclick opens SABnzbd inside a browser.
.. rubric:: Available formatters
* All the first-level parameters from
https://sabnzbd.org/wiki/advanced/api#queue
(e.g. status, speed, timeleft, spaceleft, eta ...)
"""
format = "{speed} - {timeleft}"
format_paused = "{status}"
host = "127.0.0.1"
port = 8080
api_key = ""
url = "http://{host}:{port}/sabnzbd/api?output=json&apikey={api_key}"
color = "#FFFFFF"
color_paused = "#FF0000"
color_downloading = "#00FF00"
settings = (
("format", "format string used for output"),
("format_paused", "format string used if SABnzbd is paused"),
("host", "address of the server running SABnzbd"),
("port", "port that SABnzbd is running on"),
("api_key", "api key of SABnzbd"),
("color", "default color"),
("color_paused", "color if SABnzbd is paused"),
("color_downloading", "color if downloading"),
)
on_leftclick = "pause_resume"
on_rightclick = "open_browser"
def init(self):
"""Initialize the URL used to connect to SABnzbd."""
self.url = self.url.format(host=self.host, port=self.port,
api_key=self.api_key)
def run(self):
"""Connect to SABnzbd and get the data."""
try:
answer = urlopen(self.url + "&mode=queue").read().decode()
except (HTTPError, URLError) as error:
self.output = {
"full_text": str(error.reason),
"color": "#FF0000"
}
return
answer = json.loads(answer)
# if answer["status"] exists and is False, an error occured
if not answer.get("status", True):
self.output = {
"full_text": answer["error"],
"color": "#FF0000"
}
return
queue = answer["queue"]
self.status = queue["status"]
if self.is_paused():
color = self.color_paused
elif self.is_downloading():
color = self.color_downloading
else:
color = self.color
if self.is_downloading():
full_text = self.format.format(**queue)
else:
full_text = self.format_paused.format(**queue)
self.output = {
"full_text": full_text,
"color": color
}
def pause_resume(self):
"""Toggle between pausing or resuming downloading."""
if self.is_paused():
urlopen(self.url + "&mode=resume")
else:
urlopen(self.url + "&mode=pause")
def is_paused(self):
"""Return True if downloads are currently paused."""
return self.status == "Paused"
def is_downloading(self):
"""Return True if downloads are running."""
return self.status == "Downloading"
def open_browser(self):
"""Open the URL of SABnzbd inside a browser."""
webbrowser.open(
"http://{host}:{port}/".format(host=self.host, port=self.port))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/ 0000775 0000000 0000000 00000000000 13567273623 0022001 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/__init__.py 0000664 0000000 0000000 00000064716 13567273623 0024130 0 ustar 00root root 0000000 0000000 import copy
import json
import operator
import pytz
import re
import threading
import time
from datetime import datetime, timedelta
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from i3pystatus import SettingsBase, Module, formatp
from i3pystatus.core.util import user_open, internet, require
class ScoresBackend(SettingsBase):
settings = ()
favorite_teams = []
all_games = True
date = None
games = {}
scroll_order = []
last_update = 0
def init(self):
# Merge the passed team colors with the global ones. A simple length
# check is sufficient here because i3pystatus.scores.Scores instance
# will already have checked to see if any invalid teams were specified
# in team_colors.
if len(self.team_colors) != len(self._default_colors):
self.logger.debug(
'Overriding %s team colors with: %s',
self.__class__.__name__,
self.team_colors
)
new_colors = copy.copy(self._default_colors)
new_colors.update(self.team_colors)
self.team_colors = new_colors
self.logger.debug('%s team colors: %s',
self.__class__.__name__, self.team_colors)
def api_request(self, url):
self.logger.debug('Making %s API request to %s',
self.__class__.__name__, url)
try:
with urlopen(url) as content:
try:
if content.url != url:
self.logger.debug('Request to %s was redirected to %s',
url, content.url)
content_type = dict(content.getheaders())['Content-Type']
mime_type = content_type.split(';')[0].lower()
if 'json' not in mime_type:
self.logger.debug('Response from %s is not JSON',
content.url)
return {}
charset = re.search(r'charset=(.*)', content_type).group(1)
except AttributeError:
charset = 'utf-8'
response_json = content.read().decode(charset).strip()
if not response_json:
self.logger.debug('JSON response from %s was blank', url)
return {}
try:
response = json.loads(response_json)
except json.decoder.JSONDecodeError as exc:
self.logger.error('Error loading JSON: %s', exc)
self.logger.debug('JSON text that failed to load: %s',
response_json)
return {}
self.logger.log(5, 'API response: %s', response)
return response
except HTTPError as exc:
self.logger.critical(
'Error %s (%s) making request to %s',
exc.code, exc.reason, exc.url,
)
return {}
except (ConnectionResetError, URLError) as exc:
self.logger.critical('Error making request to %s: %s', url, exc)
return {}
def get_api_date(self):
'''
Figure out the date to use for API requests. Assumes yesterday's date
if between midnight and 10am Eastern time. Override this function in a
subclass to change how the API date is calculated.
'''
# NOTE: If you are writing your own function to get the date, make sure
# to include the first if block below to allow for the ``date``
# parameter to hard-code a date.
api_date = None
if self.date is not None and not isinstance(self.date, datetime):
try:
api_date = datetime.strptime(self.date, '%Y-%m-%d')
except (TypeError, ValueError):
self.logger.warning('Invalid date \'%s\'', self.date)
if api_date is None:
utc_time = pytz.utc.localize(datetime.utcnow())
eastern = pytz.timezone('US/Eastern')
api_date = eastern.normalize(utc_time.astimezone(eastern))
if api_date.hour < 10:
# The scores on NHL.com change at 10am Eastern, if it's before
# that time of day then we will use yesterday's date.
api_date -= timedelta(days=1)
self.date = api_date
@staticmethod
def add_ordinal(number):
try:
number = int(number)
except ValueError:
return number
if 4 <= number <= 20:
return '%d%s' % (number, 'th')
else:
ord_map = {1: 'st', 2: 'nd', 3: 'rd'}
return '%d%s' % (number, ord_map.get(number % 10, 'th'))
@staticmethod
def force_int(value):
try:
return int(value)
except (TypeError, ValueError):
return 0
def get_nested(self, data, expr, callback=None, default=''):
if callback is None:
def callback(x):
return x
try:
for key in expr.split(':'):
if key.isdigit() and isinstance(data, list):
key = int(key)
data = data[key]
except (KeyError, IndexError, TypeError):
self.logger.debug('No %s data found at %s, falling back to %s',
self.__class__.__name__, expr, repr(default))
return default
return callback(data)
def interpret_api_return(self, data, team_game_map):
favorite_games = []
# Cycle through the followed teams to ensure that games show up in the
# order of teams being followed.
for team in self.favorite_teams:
for id_ in team_game_map.get(team, []):
if id_ not in favorite_games:
favorite_games.append(id_)
# If all games are being tracked, add any games not from
# explicitly-followed teams.
if self.all_games:
additional_games = [x for x in data if x not in favorite_games]
else:
additional_games = []
# Process the API return data for each tracked game
self.games = {}
for game_id in favorite_games + additional_games:
self.games[game_id] = self.process_game(data[game_id])
# Favorite games come first
self.scroll_order = [self.games[x]['id'] for x in favorite_games]
# For any remaining games being tracked, sort each group by start time
# and add them to the list
for status in self.display_order:
time_map = {
x: self.games[x]['start_time'] for x in self.games
if x not in favorite_games and self.games[x]['status'] == status
}
sorted_games = sorted(time_map.items(), key=operator.itemgetter(1))
self.scroll_order.extend([x[0] for x in sorted_games])
# Reverse map so that we can know the scroll position for a given game
# by just its ID. This will help us to place the game in its new order
# when that order changes due to the game changing from one status to
# another.
self.scroll_order_revmap = {y: x for x, y in enumerate(self.scroll_order)}
class Scores(Module):
'''
This is a generic score checker, which must use at least one configured
:ref:`score backend `.
Followed games can be scrolled through with the mouse/trackpad.
Left-clicking on the module will refresh the scores, while right-clicking
it will cycle through the configured backends. Double-clicking the module
with the left button will launch the league-specific (MLB Gameday / NHL
GameCenter / etc.) URL for the game. If there is not an active game,
double-clicking will launch the league-specific scoreboard URL containing
all games for the current day.
Double-clicking with the right button will reset the current backend to the
first game in the scroll list. This is useful for quickly switching back to
a followed team's game after looking at other game scores.
Scores for the previous day's games will be shown until 10am Eastern Time
(US), after which time the current day's games will be shown.
.. rubric:: Available formatters
Formatters are set in the backend instances, see the :ref:`scorebackends`
for more information.
This module supports the :ref:`formatp ` extended string format
syntax. This allows for values to be hidden when they evaluate as False
(e.g. when a formatter is blank (an empty string). The default values for
the format strings set in the :ref:`score backends `
(``format_pregame``, ``format_in_progress``, etc.) make heavy use of
formatp, hiding many formatters when they are blank.
.. rubric:: Usage example
.. code-block:: python
from i3pystatus import Status
from i3pystatus.scores import mlb, nhl
status = Status()
status.register(
'scores',
hints={'markup': 'pango'},
colorize_teams=True,
favorite_icon='★',
backends=[
mlb.MLB(
teams=['CWS', 'SF'],
format_no_games='No games today :(',
inning_top='⬆',
inning_bottom='⬇',
),
nhl.NHL(teams=['CHI']),
nba.NBA(
teams=['GSW'],
all_games=False,
),
epl.EPL(),
],
)
status.run()
To enable colorized team name/city/abbbreviation, ``colorize_teams`` must
be set to ``True``. This also requires that i3bar is configured to use
Pango, and that the :ref:`hints ` param is set for the module and
includes a ``markup`` key, as in the example above. To ensure that i3bar is
configured to use Pango, the `font param`__ in your i3 config file must
start with ``pango:``.
.. __: http://i3wm.org/docs/userguide.html#fonts
.. _scores-game-order:
If a ``teams`` param is not specified for the backend, then all games for
the current day will be tracked, and will be ordered by the start time of
the game. Otherwise, only games from explicitly-followed teams will be
tracked, and will be in the same order as listed. If ``ALL`` is part of the
list, then games from followed teams will be first in the scroll list,
followed by all remaining games in order of start time.
Therefore, in the above example, only White Sox and Giants games would be
tracked, while in the below example all games would be tracked, with
White Sox and Giants games appearing first in the scroll list and the
remaining games appearing after them, in order of start time.
.. code-block:: python
from i3pystatus import Status
from i3pystatus.scores import mlb
status = Status()
status.register(
'scores',
hints={'markup': 'pango'},
colorize_teams=True,
favorite_icon='★',
backends=[
mlb.MLB(
teams=['CWS', 'SF', 'ALL'],
team_colors={
'NYM': '#1D78CA',
},
),
],
)
status.run()
.. rubric:: Troubleshooting
If the module gets stuck during an update (i.e. the ``refresh_icon`` does
not go away), then the update thread probably encountered a traceback. This
traceback will (by default) be logged to ``~/.i3pystatus-`` where
```` is the PID of the thread. However, it may be more convenient to
manually set the logfile to make the location of the log data reliable and
avoid clutter in your home directory. For example:
.. code-block:: python
import logging
from i3pystatus import Status
from i3pystatus.scores import mlb, nhl
status = Status(
logfile='/home/username/var/i3pystatus.log',
)
status.register(
'scores',
log_level=logging.DEBUG,
backends=[
mlb.MLB(
teams=['CWS', 'SF'],
log_level=logging.DEBUG,
),
nhl.NHL(
teams=['CHI'],
log_level=logging.DEBUG,
),
nba.NBA(
teams=['CHI'],
log_level=logging.DEBUG,
),
],
)
status.run()
.. note::
The ``log_level`` must be set separately in both the module and the
backend instances (as shown above), otherwise the backends will
still use the default log level.
'''
interval = 300
settings = (
('backends', 'List of backend instances'),
('interval', 'Update interval (in seconds)'),
('favorite_icon', 'Value for the ``{away_favorite}`` and '
'``{home_favorite}`` formatter when the displayed game '
'is being played by a followed team'),
('color', 'Color to be used for non-colorized text (defaults to the '
'i3bar color)'),
('color_no_games', 'Color to use when no games are scheduled for the '
'currently-displayed backend (defaults to the '
'i3bar color)'),
('colorize_teams', 'Dislay team city, name, and abbreviation in the '
'team\'s color (as defined in the '
':ref:`backend `\'s ``team_colors`` '
'attribute)'),
('scroll_arrow', 'Value used for the ``{scroll}`` formatter to '
'indicate that more than one game is being tracked '
'for the currently-displayed backend'),
('refresh_icon', 'Text to display (in addition to any text currently '
'shown by the module) when refreshing scores. '
'**NOTE:** Depending on how quickly the update is '
'performed, the icon may not be displayed.'),
)
backends = []
favorite_icon = '★'
color = None
color_no_games = None
colorize_teams = False
scroll_arrow = '⬍'
refresh_icon = '⟳'
output = {'full_text': ''}
game_map = {}
backend_id = 0
on_upscroll = ['scroll_game', 1]
on_downscroll = ['scroll_game', -1]
on_leftclick = ['check_scores', 'click event']
on_rightclick = ['cycle_backend', 1]
on_doubleleftclick = ['launch_web']
on_doublerightclick = ['reset_backend']
def init(self):
if not isinstance(self.backends, list):
self.backends = [self.backends]
if not self.backends:
raise ValueError('At least one backend is required')
# Initialize each backend's game index
for index in range(len(self.backends)):
self.game_map[index] = None
for backend in self.backends:
if hasattr(backend, '_valid_teams'):
for index in range(len(backend.favorite_teams)):
# Force team abbreviation to uppercase
team_uc = str(backend.favorite_teams[index]).upper()
# Check to make sure the team abbreviation is valid
if team_uc not in backend._valid_teams:
raise ValueError(
'Invalid %s team \'%s\'' % (
backend.__class__.__name__,
backend.favorite_teams[index]
)
)
backend.favorite_teams[index] = team_uc
for index in range(len(backend.display_order)):
order_lc = str(backend.display_order[index]).lower()
# Check to make sure the display order item is valid
if order_lc not in backend._valid_display_order:
raise ValueError(
'Invalid %s display_order \'%s\'' % (
backend.__class__.__name__,
backend.display_order[index]
)
)
backend.display_order[index] = order_lc
self.condition = threading.Condition()
self.thread = threading.Thread(target=self.update_thread, daemon=True)
self.thread.start()
def update_thread(self):
try:
self.check_scores(force='scheduled')
while True:
with self.condition:
self.condition.wait(self.interval)
self.check_scores(force='scheduled')
except Exception:
msg = 'Exception in {thread} at {time}, module {name}'.format(
thread=threading.current_thread().name,
time=time.strftime('%c'),
name=self.__class__.__name__,
)
self.logger.error(msg, exc_info=True)
@property
def current_backend(self):
return self.backends[self.backend_id]
@property
def current_scroll_index(self):
return self.game_map[self.backend_id]
@property
def current_game_id(self):
try:
return self.current_backend.scroll_order[self.current_scroll_index]
except (AttributeError, TypeError):
return None
@property
def current_game(self):
try:
return self.current_backend.games[self.current_game_id]
except KeyError:
return None
def scroll_game(self, step=1):
cur_index = self.current_scroll_index
if cur_index is None:
self.logger.debug(
'Cannot scroll, no tracked {backend} games for '
'{date:%Y-%m-%d}'.format(
backend=self.current_backend.__class__.__name__,
date=self.current_backend.date,
)
)
else:
new_index = (cur_index + step) % len(self.current_backend.scroll_order)
if new_index != cur_index:
cur_id = self.current_game_id
# Don't reference self.current_scroll_index here, we're setting
# a new value for the data point for which
# self.current_scroll_index serves as a shorthand.
self.game_map[self.backend_id] = new_index
self.logger.debug(
'Scrolled from %s game %d (ID: %s) to %d (ID: %s)',
self.current_backend.__class__.__name__,
cur_index,
cur_id,
new_index,
self.current_backend.scroll_order[new_index],
)
self.refresh_display()
else:
self.logger.debug(
'Cannot scroll, only one tracked {backend} game '
'(ID: {id_}) for {date:%Y-%m-%d}'.format(
backend=self.current_backend.__class__.__name__,
id_=self.current_game_id,
date=self.current_backend.date,
)
)
def cycle_backend(self, step=1):
if len(self.backends) < 2:
self.logger.debug(
'Only one backend (%s) configured, backend cannot be changed',
self.current_backend.__class__.__name__,
)
return
old = self.backend_id
# Set the new backend
self.backend_id = (self.backend_id + step) % len(self.backends)
self.logger.debug(
'Changed scores backend from %s to %s',
self.backends[old].__class__.__name__,
self.current_backend.__class__.__name__,
)
# Display the score for the new backend. This gets rid of lag between
# when the mouse is clicked and when the new backend is shown, caused
# by any network latency encountered when updating scores.
self.refresh_display()
# Update scores (if necessary) and display them
self.check_scores()
def reset_backend(self):
if self.current_backend.games:
self.game_map[self.backend_id] = 0
self.logger.debug(
'Resetting to first game in %s scroll list (ID: %s)',
self.current_backend.__class__.__name__,
self.current_game_id,
)
self.refresh_display()
else:
self.logger.debug(
'No %s games, cannot reset to first game in scroll list',
self.current_backend.__class__.__name__,
)
def launch_web(self):
game = self.current_game
if game is None:
live_url = self.current_backend.scoreboard_url
else:
live_url = game['live_url']
self.logger.debug('Launching %s in browser', live_url)
user_open(live_url)
@require(internet)
def check_scores(self, force=False):
update_needed = False
if not self.current_backend.last_update:
update_needed = True
self.logger.debug(
'Performing initial %s score check',
self.current_backend.__class__.__name__,
)
elif force:
update_needed = True
self.logger.debug(
'%s score check triggered (%s)',
self.current_backend.__class__.__name__,
force
)
else:
update_diff = time.time() - self.current_backend.last_update
msg = ('Seconds since last %s update (%f) ' %
(self.current_backend.__class__.__name__, update_diff))
if update_diff >= self.interval:
update_needed = True
msg += ('meets or exceeds update interval (%d), update '
'triggered' % self.interval)
else:
msg += ('does not exceed update interval (%d), update '
'skipped' % self.interval)
self.logger.debug(msg)
if update_needed:
self.show_refresh_icon()
cur_id = self.current_game_id
cur_games = self.current_backend.games.keys()
self.current_backend.check_scores()
if cur_games == self.current_backend.games.keys():
# Set the index to the scroll position of the current game (it
# may have changed due to this game or other games changing
# status.
if cur_id is None:
self.logger.debug(
'No tracked {backend} games for {date:%Y-%m-%d}'.format(
backend=self.current_backend.__class__.__name__,
date=self.current_backend.date,
)
)
else:
cur_pos = self.game_map[self.backend_id]
new_pos = self.current_backend.scroll_order_revmap[cur_id]
if cur_pos != new_pos:
self.game_map[self.backend_id] = new_pos
self.logger.debug(
'Scroll position for current %s game (%s) updated '
'from %d to %d',
self.current_backend.__class__.__name__,
cur_id,
cur_pos,
new_pos,
)
else:
self.logger.debug(
'Scroll position (%d) for current %s game (ID: %s) '
'unchanged',
cur_pos,
self.current_backend.__class__.__name__,
cur_id,
)
else:
# Reset the index to 0 if there are any tracked games,
# otherwise set it to None to signify no tracked games for the
# backend.
if self.current_backend.games:
self.game_map[self.backend_id] = 0
self.logger.debug(
'Tracked %s games updated, setting scroll position to '
'0 (ID: %s)',
self.current_backend.__class__.__name__,
self.current_game_id
)
else:
self.game_map[self.backend_id] = None
self.logger.debug(
'No tracked {backend} games for {date:%Y-%m-%d}'.format(
backend=self.current_backend.__class__.__name__,
date=self.current_backend.date,
)
)
self.current_backend.last_update = time.time()
self.refresh_display()
def show_refresh_icon(self):
self.output['full_text'] = \
self.refresh_icon + self.output.get('full_text', '')
def refresh_display(self):
if self.current_scroll_index is None:
output = self.current_backend.format_no_games
color = self.color_no_games
else:
game = copy.copy(self.current_game)
fstr = str(getattr(
self.current_backend,
'format_%s' % game['status']
))
for team in ('home', 'away'):
abbrev_key = '%s_abbrev' % team
# Set favorite icon, if applicable
game['%s_favorite' % team] = self.favorite_icon \
if game[abbrev_key] in self.current_backend.favorite_teams \
else ''
if self.colorize_teams:
# Wrap in Pango markup
color = self.current_backend.team_colors.get(
game.get(abbrev_key)
)
if color is not None:
for item in ('abbrev', 'city', 'name', 'name_short'):
key = '%s_%s' % (team, item)
if key in game:
val = '%s' % (color, game[key])
game[key] = val
game['scroll'] = self.scroll_arrow \
if len(self.current_backend.games) > 1 \
else ''
output = formatp(fstr, **game).strip()
self.output = {'full_text': output, 'color': self.color}
def run(self):
pass
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/epl.py 0000664 0000000 0000000 00000040106 13567273623 0023134 0 ustar 00root root 0000000 0000000 from i3pystatus.core.util import internet, require
from i3pystatus.scores import ScoresBackend
import copy
import pytz
import time
from collections import namedtuple
from datetime import datetime
LIVE_URL = 'http://live.premierleague.com/#/gameweek/%s/matchday/%s/match/%s'
CONTEXT_URL = 'http://live.premierleague.com/syndicationdata/context.json'
SCOREBOARD_URL = 'http://live.premierleague.com/'
API_URL = 'http://live.premierleague.com/syndicationdata/competitionId=%s/seasonId=%s/gameWeekId=%s/scores.json'
STATS_URL = 'http://live.premierleague.com/syndicationdata/competitionId=%s/seasonId=%s/matchDayId=%s/league-table.json'
MATCH_DETAILS_URL = 'http://live.premierleague.com/syndicationdata/competitionId=%s/seasonId=%s/matchDayId=%s/matchId=%s/match-details.json'
MATCH_STATUS_PREGAME = 1
MATCH_STATUS_IN_PROGRESS = 2
MATCH_STATUS_FINAL = 3
MATCH_STATUS_HALFTIME = 4
class EPL(ScoresBackend):
'''
Backend to retrieve scores from the English Premier League. For usage
examples, see :py:mod:`here <.scores>`.
.. rubric:: Promotion / Relegation
Due to promotion/relegation, the **team_colors** configuration will
eventuall become out of date. When this happens, it will be necessary to
manually set the colors for the newly-promoted teams until the source for
this module is updated. An example of setting colors for newly promoted
teams can be seen below:
.. code-block:: python
from i3pystatus import Status
from i3pystatus.scores import epl
status = Status()
status.register(
'scores',
hints={'markup': 'pango'},
colorize_teams=True,
backends=[
epl.EPL(
teams=['LIV'],
team_colors={
'ABC': '#1D78CA',
'DEF': '#8AFEC3',
'GHI': '#33FA6D',
},
),
],
)
status.run()
.. rubric:: Available formatters
* `{home_name}` — Name of home team (e.g. **Tottenham Hotspur**)
* `{home_name_short}` — Shortened team name (e.g. **Spurs**)
* `{home_abbrev}` — 2 or 3-letter abbreviation for home team's city (e.g.
**TOT**)
* `{home_score}` — Home team's current score
* `{home_wins}` — Home team's number of wins
* `{home_losses}` — Home team's number of losses
* `{home_draws}` — Home team's number of draws
* `{home_points}` — Home team's number of standings points
* `{home_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the home team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{away_name}` — Name of away team (e.g. **Manchester United**)
* `{away_name_short}` — Name of away team's city (e.g. **Man Utd**)
* `{away_abbrev}` — 2 or 3-letter abbreviation for away team's name (e.g.
**MUN**)
* `{away_score}` — Away team's current score
* `{away_wins}` — Away team's number of wins
* `{away_losses}` — Away team's number of losses
* `{away_draws}` — Away team's number of draws
* `{away_points}` — Away team's number of standings points
* `{away_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the away team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{minute}` — Current minute of game when in progress
* `{start_time}` — Start time of game in system's localtime (supports
strftime formatting, e.g. `{start_time:%I:%M %p}`)
.. rubric:: Team abbreviations
* **ARS** — Arsenal
* **AVL** — Aston Villa
* **BOU** — Bournemouth
* **CHE** — Chelsea
* **CRY** — Crystal Palace
* **EVE** — Everton
* **LEI** — Leicester City
* **LIV** — Liverpool
* **MCI** — Manchester City
* **MUN** — Manchester United
* **NEW** — Newcastle United
* **NOR** — Norwich City
* **SOU** — Southampton
* **STK** — Stoke City
* **SUN** — Sunderland Association
* **SWA** — Swansea City
* **TOT** — Tottenham Hotspur
* **WAT** — Watford
* **WBA** — West Bromwich Albion
* **WHU** — West Ham United
'''
interval = 300
settings = (
('favorite_teams', 'List of abbreviations of favorite teams. Games '
'for these teams will appear first in the scroll '
'list. A detailed description of how games are '
'ordered can be found '
':ref:`here `.'),
('all_games', 'If set to ``True``, all games will be present in '
'the scroll list. If set to ``False``, then only '
'games from **favorite_teams** will be present in '
'the scroll list.'),
('display_order', 'When **all_games** is set to ``True``, this '
'option will dictate the order in which games from '
'teams not in **favorite_teams** are displayed'),
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('team_colors', 'Dictionary mapping team abbreviations to hex color '
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the date will be determined by '
'the return value of an API call to the **context_url**. '
'Due to API limitations, the date can presently only be '
'overridden to another date in the current week. This '
'option exists primarily for troubleshooting purposes.'),
('live_url', 'URL string to launch EPL Live Match Centre. This value '
'should not need to be changed.'),
('scoreboard_url', 'Link to the EPL scoreboard page. Like '
'**live_url**, this value should not need to be '
'changed.'),
('api_url', 'Alternate URL string from which to retrieve score data. '
'Like **live_url**, this value should not need to be '
'changed.'),
('stats_url', 'Alternate URL string from which to retrieve team '
'statistics. Like **live_url**, this value should not '
'need to be changed.'),
('match_details_url', 'Alternate URL string from which to retrieve '
'match details. Like **live_url**, this value '
'should not need to be changed.'),
)
required = ()
_default_colors = {
'ARS': '#ED1B22',
'AVL': '#94BEE5',
'BOU': '#CB0B0F',
'CHE': '#195FAF',
'CRY': '#195FAF',
'EVE': '#004F9E',
'LEI': '#304FB6',
'LIV': '#D72129',
'MCI': '#74B2E0',
'MUN': '#DD1921',
'NEW': '#06B3EB',
'NOR': '#00A651',
'SOU': '#DB1C26',
'STK': '#D81732',
'SUN': '#BC0007',
'SWA': '#B28250',
'TOT': '#DADADA',
'WAT': '#E4D500',
'WBA': '#B43C51',
'WHU': '#9DE4FA',
}
_valid_display_order = ['in_progress', 'final', 'pregame']
display_order = _valid_display_order
format_no_games = 'EPL: No games'
format_pregame = '[{scroll} ]EPL: [{away_favorite} ]{away_abbrev} ({away_points}, {away_wins}-{away_losses}-{away_draws}) at [{home_favorite} ]{home_abbrev} ({home_points}, {home_wins}-{home_losses}-{home_draws}) {start_time:%H:%M %Z}'
format_in_progress = '[{scroll} ]EPL: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})] ({minute})'
format_final = '[{scroll} ]EPL: [{away_favorite} ]{away_abbrev} {away_score} ({away_points}, {away_wins}-{away_losses}-{away_draws}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_points}, {home_wins}-{home_losses}-{home_draws}) (Final)'
team_colors = _default_colors
context_url = CONTEXT_URL
live_url = LIVE_URL
scoreboard_url = SCOREBOARD_URL
api_url = API_URL
stats_url = STATS_URL
match_details_url = MATCH_DETAILS_URL
def get_api_date(self):
# NOTE: We're not really using this date for EPL API calls, but we do
# need it to allow for a 'date' param to override which date we use for
# scores.
if self.date is not None and not isinstance(self.date, datetime):
try:
self.date = datetime.strptime(self.date, '%Y-%m-%d')
except (TypeError, ValueError):
self.logger.warning('Invalid date \'%s\'', self.date)
if self.date is None:
self.date = datetime.strptime(self.context.date, '%Y%m%d')
def get_context(self):
response = self.api_request(self.context_url)
if not response:
# There is no context data, but we still need a date to use in
# __init__.py to log that there are no games for the given date.
# Fall back to the parent class' function to set a date.
super(EPL, self).get_api_date()
return False
context_tuple = namedtuple(
'Context',
('competition', 'date', 'game_week', 'match_day', 'season')
)
self.context = context_tuple(
*[
response.get(x, '')
for x in ('competitionId', 'currentDay', 'gameWeekId',
'matchDayId', 'seasonId')
]
)
return True
def get_team_stats(self):
ret = {}
url = self.stats_url % (self.context.competition,
self.context.season,
self.context.match_day)
for item in self.api_request(url).get('Data', []):
try:
key = item.pop('TeamCode')
except KeyError:
self.logger.debug('Error occurred obtaining %s team stats',
self.__class__.__name__,
exc_info=True)
continue
ret[key] = item
return ret
def get_minute(self, data, id_):
match_status = data[id_].get('StatusId', MATCH_STATUS_PREGAME)
if match_status == MATCH_STATUS_HALFTIME:
return 'Halftime'
if match_status == MATCH_STATUS_IN_PROGRESS:
url = self.match_details_url % (self.context.competition,
self.context.season,
data[id_].get('MatchDayId', ''),
id_)
try:
response = self.api_request(url)
return '%s\'' % response['Data']['Minute']
except (KeyError, TypeError):
return '?\''
else:
return '?\''
def check_scores(self):
if not self.get_context():
data = team_game_map = {}
else:
self.get_api_date()
url = self.api_url % (self.context.competition,
self.context.season,
self.context.game_week)
for item in self.api_request(url).get('Data', []):
if item.get('Key', '') == self.date.strftime('%Y%m%d'):
game_list = item.get('Scores', [])
break
else:
game_list = []
self.logger.debug('game_list = %s', game_list)
team_stats = self.get_team_stats()
# Convert list of games to dictionary for easy reference later on
data = {}
team_game_map = {}
for game in game_list:
try:
id_ = game['Id']
except KeyError:
continue
try:
for key in ('HomeTeam', 'AwayTeam'):
team = game[key]['Code'].upper()
if team in self.favorite_teams:
team_game_map.setdefault(team, []).append(id_)
except KeyError:
continue
data[id_] = game
# Merge in the team stats, because they are not returned in the
# initial API request.
for key in ('HomeTeam', 'AwayTeam'):
team = game[key]['Code'].upper()
data[id_][key]['Stats'] = team_stats.get(team, {})
# Add the minute, if applicable
data[id_]['Minute'] = self.get_minute(data, id_)
self.interpret_api_return(data, team_game_map)
def process_game(self, game):
ret = {}
def _update(ret_key, game_key=None, callback=None, default='?'):
ret[ret_key] = self.get_nested(game,
game_key or ret_key,
callback=callback,
default=default)
self.logger.debug('Processing %s game data: %s',
self.__class__.__name__, game)
_update('id', 'Id')
_update('minute', 'Minute')
ret['live_url'] = self.live_url % (self.context.game_week,
self.context.match_day,
ret['id'])
status_map = {
MATCH_STATUS_PREGAME: 'pregame',
MATCH_STATUS_IN_PROGRESS: 'in_progress',
MATCH_STATUS_FINAL: 'final',
MATCH_STATUS_HALFTIME: 'in_progress',
}
status_code = game.get('StatusId')
if status_code is None:
self.logger.debug('%s game %s is missing StatusId',
self.__class__.__name__, ret['id'])
status_code = 1
ret['status'] = status_map[status_code]
for ret_key, game_key in (('home', 'HomeTeam'), ('away', 'AwayTeam')):
_update('%s_score' % ret_key, '%s:Score' % game_key, default=0)
_update('%s_name' % ret_key, '%s:Name' % game_key)
_update('%s_name_short' % ret_key, '%s:ShortName' % game_key)
_update('%s_abbrev' % ret_key, '%s:Code' % game_key)
_update('%s_wins' % ret_key, '%s:Stats:Won' % game_key, default=0)
_update('%s_losses' % ret_key, '%s:Stats:Lost' % game_key)
_update('%s_draws' % ret_key, '%s:Stats:Drawn' % game_key)
_update('%s_points' % ret_key, '%s:Stats:Points' % game_key)
try:
game_time = datetime.strptime(
game.get('DateTime', ''),
'%Y-%m-%dT%H:%M:%S'
)
except ValueError as exc:
# Log when the date retrieved from the API return doesn't match the
# expected format (to help troubleshoot API changes), and set an
# actual datetime so format strings work as expected. The times
# will all be wrong, but the logging here will help us make the
# necessary changes to adapt to any API changes.
self.logger.error(
'Error encountered determining game time for %s game %s:',
self.__class__.__name__,
ret['id'],
exc_info=True
)
game_time = datetime.datetime(1970, 1, 1)
london = pytz.timezone('Europe/London')
ret['start_time'] = london.localize(game_time).astimezone()
self.logger.debug('Returned %s formatter data: %s',
self.__class__.__name__, ret)
return ret
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/mlb.py 0000664 0000000 0000000 00000034227 13567273623 0023135 0 ustar 00root root 0000000 0000000 from i3pystatus.core.util import internet, require
from i3pystatus.scores import ScoresBackend
import copy
import json
import pytz
import re
import time
from datetime import datetime
from urllib.request import urlopen
LIVE_URL = 'https://www.mlb.com/gameday/%s'
SCOREBOARD_URL = 'http://m.mlb.com/scoreboard'
API_URL = 'https://statsapi.mlb.com/api/v1/schedule?sportId=1,51&date=%04d-%02d-%02d&gameTypes=E,S,R,A,F,D,L,W&hydrate=team(),linescore(matchup,runners),stats,game(content(media(featured,epg),summary),tickets),seriesStatus(useOverride=true)&useLatestGames=false&language=en&leagueId=103,104,420'
class MLB(ScoresBackend):
'''
Backend to retrieve MLB scores. For usage examples, see :py:mod:`here
<.scores>`.
.. rubric:: Available formatters
* `{home_name}` — Name of home team
* `{home_city}` — Name of home team's city
* `{home_abbrev}` — 2 or 3-letter abbreviation for home team's city
* `{home_score}` — Home team's current score
* `{home_wins}` — Home team's number of wins
* `{home_losses}` — Home team's number of losses
* `{home_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the home team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{away_name}` — Name of away team
* `{away_city}` — Name of away team's city
* `{away_abbrev}` — 2 or 3-letter abbreviation for away team's city
* `{away_score}` — Away team's current score
* `{away_wins}` — Away team's number of wins
* `{away_losses}` — Away team's number of losses
* `{away_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the away team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{top_bottom}` — Displays the value of either ``inning_top`` or
``inning_bottom`` based on whether the game is in the top or bottom of an
inning.
* `{inning}` — Current inning
* `{outs}` — Number of outs in current inning
* `{venue}` — Name of ballpark where game is being played
* `{start_time}` — Start time of game in system's localtime (supports
strftime formatting, e.g. `{start_time:%I:%M %p}`)
* `{delay}` — Reason for delay, if game is currently delayed. Otherwise,
this formatter will be blank.
* `{postponed}` — Reason for postponement, if game has been postponed.
Otherwise, this formatter will be blank.
* `{extra_innings}` — When a game lasts longer than 9 innings, this
formatter will show that number of innings. Otherwise, it will blank.
.. rubric:: Team abbreviations
* **ARI** — Arizona Diamondbacks
* **ATL** — Atlanta Braves
* **BAL** — Baltimore Orioles
* **BOS** — Boston Red Sox
* **CHC** — Chicago Cubs
* **CIN** — Cincinnati Reds
* **CLE** — Cleveland Indians
* **COL** — Colorado Rockies
* **CWS** — Chicago White Sox
* **DET** — Detroit Tigers
* **HOU** — Houston Astros
* **KC** — Kansas City Royals
* **LAA** — Los Angeles Angels of Anaheim
* **LAD** — Los Angeles Dodgers
* **MIA** — Miami Marlins
* **MIL** — Milwaukee Brewers
* **MIN** — Minnesota Twins
* **NYY** — New York Yankees
* **NYM** — New York Mets
* **OAK** — Oakland Athletics
* **PHI** — Philadelphia Phillies
* **PIT** — Pittsburgh Pirates
* **SD** — San Diego Padres
* **SEA** — Seattle Mariners
* **SF** — San Francisco Giants
* **STL** — St. Louis Cardinals
* **TB** — Tampa Bay Rays
* **TEX** — Texas Rangers
* **TOR** — Toronto Blue Jays
* **WSH** — Washington Nationals
'''
interval = 300
settings = (
('favorite_teams', 'List of abbreviations of favorite teams. Games '
'for these teams will appear first in the scroll '
'list. A detailed description of how games are '
'ordered can be found '
':ref:`here `.'),
('all_games', 'If set to ``True``, all games will be present in '
'the scroll list. If set to ``False``, then only '
'games from **favorite_teams** will be present in '
'the scroll list.'),
('display_order', 'When **all_games** is set to ``True``, this '
'option will dictate the order in which games from '
'teams not in **favorite_teams** are displayed'),
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('format_postponed', 'Format used when the game has been postponed'),
('format_suspended', 'Format used when the game has been suspended'),
('inning_top', 'Value for the ``{top_bottom}`` formatter when game '
'is in the top half of an inning'),
('inning_bottom', 'Value for the ``{top_bottom}`` formatter when game '
'is in the bottom half of an inning'),
('team_colors', 'Dictionary mapping team abbreviations to hex color '
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the current day\'s games will be '
'displayed starting at 10am Eastern time, with last '
'evening\'s scores being shown before then. This option '
'exists primarily for troubleshooting purposes.'),
('live_url', 'Alternate URL string to launch MLB Gameday. This value '
'should not need to be changed'),
('scoreboard_url', 'Link to the MLB.com scoreboard page. Like '
'**live_url**, this value should not need to be '
'changed.'),
('api_url', 'Alternate URL string from which to retrieve score data. '
'Like **live_url*** this value should not need to be '
'changed.'),
)
required = ()
_default_colors = {
'ARI': '#A71930',
'ATL': '#CE1141',
'BAL': '#DF4601',
'BOS': '#BD3039',
'CHC': '#004EC1',
'CIN': '#C6011F',
'CLE': '#E31937',
'COL': '#5E5EB6',
'CWS': '#DADADA',
'DET': '#FF6600',
'HOU': '#EB6E1F',
'KC': '#0046DD',
'LAA': '#BA0021',
'LAD': '#005A9C',
'MIA': '#00A3E0',
'MIL': '#0747CC',
'MIN': '#D31145',
'NYY': '#0747CC',
'NYM': '#FF5910',
'OAK': '#006659',
'PHI': '#E81828',
'PIT': '#FFCC01',
'SD': '#285F9A',
'SEA': '#2E8B90',
'SF': '#FD5A1E',
'STL': '#B53B30',
'TB': '#8FBCE6',
'TEX': '#C0111F',
'TOR': '#0046DD',
'WSH': '#C70003',
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'suspended', 'final', 'postponed', 'pregame']
display_order = _valid_display_order
format_no_games = 'MLB: No games'
format_pregame = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) {start_time:%H:%M %Z}[ ({delay} Delay)]'
format_in_progress = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score}, [{home_favorite} ]{home_abbrev} {home_score} ({top_bottom} {inning}, {outs} Out)[ ({delay} Delay)]'
format_final = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Final[/{extra_innings}])'
format_postponed = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) (PPD: {postponed})'
format_suspended = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Suspended: {suspended})'
inning_top = 'Top'
inning_bottom = 'Bot'
team_colors = _default_colors
live_url = LIVE_URL
scoreboard_url = SCOREBOARD_URL
api_url = API_URL
@require(internet)
def check_scores(self):
self.get_api_date()
url = self.api_url % (self.date.year, self.date.month, self.date.day)
game_list = self.get_nested(
self.api_request(url),
'dates:0:games',
default=[])
if not isinstance(game_list, list):
# When only one game is taking place during a given day, the game
# data is just a single dict containing that game's data, rather
# than a list of dicts. Encapsulate the single game dict in a list
# to make it process correctly in the loop below.
game_list = [game_list]
# Convert list of games to dictionary for easy reference later on
data = {}
team_game_map = {}
for game in game_list:
try:
id_ = game['gamePk']
except (KeyError, TypeError):
continue
away_abbrev = self.get_nested(
game,
'teams:away:team:abbreviation').upper()
home_abbrev = self.get_nested(
game,
'teams:home:team:abbreviation').upper()
if away_abbrev and home_abbrev:
try:
for team in (home_abbrev, away_abbrev):
if team in self.favorite_teams:
team_game_map.setdefault(team, []).append(id_)
except KeyError:
continue
data[id_] = game
self.interpret_api_return(data, team_game_map)
def process_game(self, game):
ret = {}
self.logger.debug('Processing %s game data: %s',
self.__class__.__name__, game)
linescore = self.get_nested(game, 'linescore', default={})
ret['id'] = game['gamePk']
ret['inning'] = self.get_nested(linescore, 'currentInning', default=0)
ret['outs'] = self.get_nested(linescore, 'outs')
ret['live_url'] = self.live_url % ret['id']
for team in ('away', 'home'):
team_data = self.get_nested(game, 'teams:%s' % team, default={})
if team == 'home':
ret['venue'] = self.get_nested(team_data, 'venue:name')
ret['%s_city' % team] = self.get_nested(
team_data,
'team:locationName')
ret['%s_name' % team] = self.get_nested(
team_data,
'team:teamName')
ret['%s_abbrev' % team] = self.get_nested(
team_data,
'team:abbreviation')
ret['%s_wins' % team] = self.get_nested(
team_data,
'leagueRecord:wins',
default=0)
ret['%s_losses' % team] = self.get_nested(
team_data,
'leagueRecord:losses',
default=0)
ret['%s_score' % team] = self.get_nested(
linescore,
'teams:%s:runs' % team,
default=0)
for key in ('delay', 'postponed', 'suspended'):
ret[key] = ''
ret['status'] = self.get_nested(game, 'status:detailedState').replace(' ', '_').lower()
if ret['status'] == 'delayed_start':
ret['status'] = 'pregame'
ret['delay'] = self.get_nested(game, 'status:reason', default='Unknown')
elif ret['status'].startswith('delayed'):
ret['status'] = 'in_progress'
ret['delay'] = game['status']['detailedState'].split(':', 1)[-1].strip()
elif ret['status'] == 'postponed':
ret['postponed'] = self.get_nested(game, 'status:reason', default='Unknown Reason')
elif ret['status'] == 'suspended':
ret['suspended'] = self.get_nested(game, 'status:reason', default='Unknown Reason')
elif ret['status'].startswith('completed_early') or ret['status'] == 'game_over':
ret['status'] = 'final'
elif ret['status'] not in ('in_progress', 'final'):
ret['status'] = 'pregame'
try:
ret['extra_innings'] = ret['inning'] \
if ret['status'] == 'final' and ret['inning'] != 9 \
else ''
except ValueError:
ret['extra_innings'] = ''
top_bottom = self.get_nested(linescore, 'inningHalf').lower()
ret['top_bottom'] = self.inning_top if top_bottom == 'top' \
else self.inning_bottom if top_bottom == 'bottom' \
else ''
try:
game_time = datetime.strptime(
self.get_nested(game, 'gameDate'),
'%Y-%m-%dT%H:%M:%SZ')
except ValueError as exc:
# Log when the date retrieved from the API return doesn't match the
# expected format (to help troubleshoot API changes), and set an
# actual datetime so format strings work as expected. The times
# will all be wrong, but the logging here will help us make the
# necessary changes to adapt to any API changes.
self.logger.error(
'Error encountered determining %s game time for game %s:',
self.__class__.__name__,
game['gamePk'],
exc_info=True
)
game_time = datetime(1970, 1, 1)
ret['start_time'] = pytz.timezone('UTC').localize(game_time).astimezone()
self.logger.debug('Returned %s formatter data: %s',
self.__class__.__name__, ret)
return ret
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/nba.py 0000664 0000000 0000000 00000034022 13567273623 0023114 0 ustar 00root root 0000000 0000000 from i3pystatus.core.util import internet, require
from i3pystatus.scores import ScoresBackend
import copy
import pytz
import time
from datetime import datetime
LIVE_URL = 'http://www.nba.com/gametracker/#/%s/lp'
SCOREBOARD_URL = 'http://www.nba.com/scores'
API_URL = 'http://data.nba.com/data/10s/json/cms/noseason/scoreboard/%04d%02d%02d/games.json'
STANDINGS_URL = 'http://data.nba.com/data/json/cms/%s/league/standings.json'
class NBA(ScoresBackend):
'''
Backend to retrieve NBA scores. For usage examples, see :py:mod:`here
<.scores>`.
.. rubric:: Available formatters
* `{home_name}` — Name of home team
* `{home_city}` — Name of home team's city
* `{home_abbrev}` — 3-letter abbreviation for home team's city
* `{home_score}` — Home team's current score
* `{home_wins}` — Home team's number of wins
* `{home_losses}` — Home team's number of losses
* `{home_seed}` — During the playoffs, shows the home team's playoff seed.
When not in the playoffs, this formatter will be blank.
* `{home_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the home team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{away_name}` — Name of away team
* `{away_city}` — Name of away team's city
* `{away_abbrev}` — 2 or 3-letter abbreviation for away team's city
* `{away_score}` — Away team's current score
* `{away_wins}` — Away team's number of wins
* `{away_losses}` — Away team's number of losses
* `{away_seed}` — During the playoffs, shows the away team's playoff seed.
When not in the playoffs, this formatter will be blank.
* `{away_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the away team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{time_remaining}` — Time remaining in the current quarter/OT period
* `{quarter}` — Number of the current quarter
* `{venue}` — Name of arena where game is being played
* `{start_time}` — Start time of game in system's localtime (supports
strftime formatting, e.g. `{start_time:%I:%M %p}`)
* `{overtime}` — If the game ended in overtime, this formatter will show
``OT``. If the game ended in regulation, or has not yet completed, this
formatter will be blank.
.. rubric:: Team abbreviations
* **ATL** — Atlanta Hawks
* **BKN** — Brooklyn Nets
* **BOS** — Boston Celtics
* **CHA** — Charlotte Hornets
* **CHI** — Chicago Bulls
* **CLE** — Cleveland Cavaliers
* **DAL** — Dallas Mavericks
* **DEN** — Denver Nuggets
* **DET** — Detroit Pistons
* **GSW** — Golden State Warriors
* **HOU** — Houston Rockets
* **IND** — Indiana Pacers
* **MIA** — Miami Heat
* **MEM** — Memphis Grizzlies
* **MIL** — Milwaukee Bucks
* **LAC** — Los Angeles Clippers
* **LAL** — Los Angeles Lakers
* **MIN** — Minnesota Timberwolves
* **NOP** — New Orleans Pelicans
* **NYK** — New York Knicks
* **OKC** — Oklahoma City Thunder
* **ORL** — Orlando Magic
* **PHI** — Philadelphia 76ers
* **PHX** — Phoenix Suns
* **POR** — Portland Trailblazers
* **SAC** — Sacramento Kings
* **SAS** — San Antonio Spurs
* **TOR** — Toronto Raptors
* **UTA** — Utah Jazz
* **WAS** — Washington Wizards
'''
interval = 300
settings = (
('favorite_teams', 'List of abbreviations of favorite teams. Games '
'for these teams will appear first in the scroll '
'list. A detailed description of how games are '
'ordered can be found '
':ref:`here `.'),
('all_games', 'If set to ``True``, all games will be present in '
'the scroll list. If set to ``False``, then only '
'games from **favorite_teams** will be present in '
'the scroll list.'),
('display_order', 'When **all_games** is set to ``True``, this '
'option will dictate the order in which games from '
'teams not in **favorite_teams** are displayed'),
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('team_colors', 'Dictionary mapping team abbreviations to hex color '
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the current day\'s games will be '
'displayed starting at 10am Eastern time, with last '
'evening\'s scores being shown before then. This option '
'exists primarily for troubleshooting purposes.'),
('live_url', 'URL string to launch NBA Game Tracker. This value '
'should not need to be changed.'),
('scoreboard_url', 'Link to the NBA.com scoreboard page. Like '
'**live_url**, this value should not need to be '
'changed.'),
('api_url', 'Alternate URL string from which to retrieve score data. '
'Like, **live_url**, this value should not need to be '
'changed.'),
('standings_url', 'Alternate URL string from which to retrieve team '
'standings. Like **live_url**, this value should '
'not need to be changed.'),
)
required = ()
_default_colors = {
'ATL': '#E2383F',
'BKN': '#DADADA',
'BOS': '#178D58',
'CHA': '#00798D',
'CHI': '#CD1041',
'CLE': '#FDBA31',
'DAL': '#006BB7',
'DEN': '#5593C3',
'DET': '#207EC0',
'GSW': '#DEB934',
'HOU': '#CD1042',
'IND': '#FFBB33',
'MIA': '#A72249',
'MEM': '#628BBC',
'MIL': '#4C7B4B',
'LAC': '#ED174C',
'LAL': '#FDB827',
'MIN': '#35749F',
'NOP': '#A78F59',
'NYK': '#F68428',
'OKC': '#F05033',
'ORL': '#1980CB',
'PHI': '#006BB7',
'PHX': '#E76120',
'POR': '#B03037',
'SAC': '#7A58A1',
'SAS': '#DADADA',
'TOR': '#CD112C',
'UTA': '#4B7059',
'WAS': '#E51735',
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'final', 'pregame']
display_order = _valid_display_order
format_no_games = 'NBA: No games'
format_pregame = '[{scroll} ]NBA: [{away_favorite} ][{away_seed} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ][{home_seed} ]{home_abbrev} ({home_wins}-{home_losses}) {start_time:%H:%M %Z}'
format_in_progress = '[{scroll} ]NBA: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})] ({time_remaining} {quarter})'
format_final = '[{scroll} ]NBA: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Final[/{overtime}])'
team_colors = _default_colors
live_url = LIVE_URL
scoreboard_url = SCOREBOARD_URL
api_url = API_URL
standings_url = STANDINGS_URL
def check_scores(self):
self.get_api_date()
url = self.api_url % (self.date.year, self.date.month, self.date.day)
response = self.api_request(url)
game_list = self.get_nested(response,
'sports_content:games:game',
default=[])
standings_year = self.get_nested(
response,
'sports_content:sports_meta:season_meta:standings_season_year',
default=self.date.year,
)
stats_list = self.get_nested(
self.api_request(self.standings_url % standings_year),
'sports_content:standings:team',
default=[],
)
team_stats = {}
for item in stats_list:
try:
key = item.pop('abbreviation')
except KeyError:
self.logger.debug('Error occurred obtaining team stats',
exc_info=True)
continue
team_stats[key] = item.get('team_stats', {})
self.logger.debug('%s team stats: %s',
self.__class__.__name__, team_stats)
# Convert list of games to dictionary for easy reference later on
data = {}
team_game_map = {}
for game in game_list:
try:
id_ = game['game_url']
except KeyError:
continue
try:
for key in ('home', 'visitor'):
team = game[key]['abbreviation'].upper()
if team in self.favorite_teams:
team_game_map.setdefault(team, []).append(id_)
except KeyError:
continue
data[id_] = game
# Merge in the team stats, because they are not returned in the
# initial API request.
for key in ('home', 'visitor'):
team = data[id_][key]['abbreviation'].upper()
data[id_][key].update(team_stats.get(team, {}))
self.interpret_api_return(data, team_game_map)
def process_game(self, game):
ret = {}
def _update(ret_key, game_key=None, callback=None, default='?'):
ret[ret_key] = self.get_nested(game,
game_key or ret_key,
callback=callback,
default=default)
self.logger.debug('Processing %s game data: %s',
self.__class__.__name__, game)
_update('id', 'game_url')
ret['live_url'] = self.live_url % ret['id']
status_map = {
'1': 'pregame',
'2': 'in_progress',
'3': 'final',
}
period_data = game.get('period_time', {})
status_code = period_data.get('game_status', '1')
status = status_map.get(status_code)
if status is None:
self.logger.debug('Unknown %s game status code \'%s\'',
self.__class__.__name__, status_code)
status_code = '1'
ret['status'] = status_map[status_code]
if ret['status'] in ('in_progress', 'final'):
period_number = int(period_data.get('period_value', 1))
total_periods = int(period_data.get('total_periods', 0))
period_diff = period_number - total_periods
ret['quarter'] = 'OT' \
if period_diff == 1 \
else '%dOT' % period_diff if period_diff > 1 \
else self.add_ordinal(period_number)
else:
ret['quarter'] = ''
ret['time_remaining'] = period_data.get('game_clock')
if ret['time_remaining'] == '':
ret['time_remaining'] = 'End'
elif ret['time_remaining'] is None:
ret['time_remaining'] = ''
ret['overtime'] = ret['quarter'] if 'OT' in ret['quarter'] else ''
_update('venue', 'arena')
for ret_key, game_key in (('home', 'home'), ('away', 'visitor')):
_update('%s_score' % ret_key, '%s:score' % game_key,
callback=self.force_int, default=0)
_update('%s_city' % ret_key, '%s:city' % game_key)
_update('%s_name' % ret_key, '%s:nickname' % game_key)
_update('%s_abbrev' % ret_key, '%s:abbreviation' % game_key)
if 'playoffs' in game:
_update('%s_wins' % ret_key, 'playoffs:%s_wins' % game_key,
callback=self.force_int, default=0)
_update('%s_seed' % ret_key, 'playoffs:%s_seed' % game_key,
callback=self.force_int, default=0)
else:
_update('%s_wins' % ret_key, '%s:wins' % game_key,
callback=self.force_int, default=0)
_update('%s_losses' % ret_key, '%s:losses' % game_key,
callback=self.force_int, default=0)
ret['%s_seed' % ret_key] = ''
if 'playoffs' in game:
ret['home_losses'] = ret['away_wins']
ret['away_losses'] = ret['home_wins']
# From API data, date is YYYYMMDD, time is HHMM
game_time_str = '%s%s' % (game.get('date', ''), game.get('time', ''))
try:
game_time = datetime.strptime(game_time_str, '%Y%m%d%H%M')
except ValueError as exc:
# Log when the date retrieved from the API return doesn't match the
# expected format (to help troubleshoot API changes), and set an
# actual datetime so format strings work as expected. The times
# will all be wrong, but the logging here will help us make the
# necessary changes to adapt to any API changes.
self.logger.error(
'Error encountered determining game time for %s game %s:',
self.__class__.__name__,
game['id'],
exc_info=True
)
game_time = datetime.datetime(1970, 1, 1)
eastern = pytz.timezone('US/Eastern')
ret['start_time'] = eastern.localize(game_time).astimezone()
self.logger.debug('Returned %s formatter data: %s',
self.__class__.__name__, ret)
return ret
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/nhl.py 0000664 0000000 0000000 00000036357 13567273623 0023152 0 ustar 00root root 0000000 0000000 from i3pystatus.core.util import internet, require
from i3pystatus.scores import ScoresBackend
import copy
import json
import pytz
import re
import time
from datetime import datetime
from urllib.request import urlopen
LIVE_URL = 'https://www.nhl.com/gamecenter/%s'
SCOREBOARD_URL = 'https://www.nhl.com/scores'
API_URL = 'https://statsapi.web.nhl.com/api/v1/schedule?startDate=%04d-%02d-%02d&endDate=%04d-%02d-%02d&expand=schedule.teams,schedule.linescore,schedule.broadcasts.all&site=en_nhl&teamId='
class NHL(ScoresBackend):
'''
Backend to retrieve NHL scores. For usage examples, see :py:mod:`here
<.scores>`.
.. rubric:: Available formatters
* `{home_name}` — Name of home team
* `{home_city}` — Name of home team's city
* `{home_abbrev}` — 3-letter abbreviation for home team's city
* `{home_score}` — Home team's current score
* `{home_wins}` — Home team's number of wins
* `{home_losses}` — Home team's number of losses
* `{home_otl}` — Home team's number of overtime losses
* `{home_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the home team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{home_empty_net}` — Shows the value from the ``empty_net`` parameter
when the home team's net is empty.
* `{away_name}` — Name of away team
* `{away_city}` — Name of away team's city
* `{away_abbrev}` — 2 or 3-letter abbreviation for away team's city
* `{away_score}` — Away team's current score
* `{away_wins}` — Away team's number of wins
* `{away_losses}` — Away team's number of losses
* `{away_otl}` — Away team's number of overtime losses
* `{away_favorite}` — Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the away team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{away_empty_net}` — Shows the value from the ``empty_net`` parameter
when the away team's net is empty.
* `{period}` — Current period
* `{venue}` — Name of arena where game is being played
* `{start_time}` — Start time of game in system's localtime (supports
strftime formatting, e.g. `{start_time:%I:%M %p}`)
* `{overtime}` — If the game ended in overtime or a shootout, this
formatter will show ``OT`` kor ``SO``. If the game ended in regulation,
or has not yet completed, this formatter will be blank.
.. rubric:: Playoffs
In the playoffs, losses are not important (as the losses will be equal to
the other team's wins). Therefore, it is a good idea during the playoffs to
manually set format strings to exclude information on team losses. For
example:
.. code-block:: python
from i3pystatus import Status
from i3pystatus.scores import nhl
status = Status()
status.register(
'scores',
hints={'markup': 'pango'},
colorize_teams=True,
favorite_icon='★',
backends=[
nhl.NHL(
favorite_teams=['CHI'],
format_pregame = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} ({away_wins}) at [{home_favorite} ]{home_abbrev} ({home_wins}) {start_time:%H:%M %Z}',
format_final = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}) (Final[/{overtime}])',
),
],
)
.. rubric:: Team abbreviations
* **ANA** — Anaheim Ducks
* **ARI** — Arizona Coyotes
* **BOS** — Boston Bruins
* **BUF** — Buffalo Sabres
* **CAR** — Carolina Hurricanes
* **CBJ** — Columbus Blue Jackets
* **CGY** — Calgary Flames
* **CHI** — Chicago Blackhawks
* **COL** — Colorado Avalanche
* **DAL** — Dallas Stars
* **DET** — Detroit Red Wings
* **EDM** — Edmonton Oilers
* **FLA** — Florida Panthers
* **LAK** — Los Angeles Kings
* **MIN** — Minnesota Wild
* **MTL** — Montreal Canadiens
* **NJD** — New Jersey Devils
* **NSH** — Nashville Predators
* **NYI** — New York Islanders
* **NYR** — New York Rangers
* **OTT** — Ottawa Senators
* **PHI** — Philadelphia Flyers
* **PIT** — Pittsburgh Penguins
* **SJS** — San Jose Sharks
* **STL** — St. Louis Blues
* **TBL** — Tampa Bay Lightning
* **TOR** — Toronto Maple Leafs
* **VAN** — Vancouver Canucks
* **VGK** — Vegas Golden Knights
* **WPG** — Winnipeg Jets
* **WSH** — Washington Capitals
'''
interval = 300
settings = (
('favorite_teams', 'List of abbreviations of favorite teams. Games '
'for these teams will appear first in the scroll '
'list. A detailed description of how games are '
'ordered can be found '
':ref:`here `.'),
('all_games', 'If set to ``True``, all games will be present in '
'the scroll list. If set to ``False``, then only '
'games from **favorite_teams** will be present in '
'the scroll list.'),
('display_order', 'When **all_games** is set to ``True``, this '
'option will dictate the order in which games from '
'teams not in **favorite_teams** are displayed'),
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('empty_net', 'Value for the ``{away_empty_net}`` or '
'``{home_empty_net}`` formatter when the net is empty. '
'When the net is not empty, these formatters will be '
'empty strings.'),
('team_colors', 'Dictionary mapping team abbreviations to hex color '
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the current day\'s games will be '
'displayed starting at 10am Eastern time, with last '
'evening\'s scores being shown before then. This option '
'exists primarily for troubleshooting purposes.'),
('live_url', 'URL string to launch NHL GameCenter. This value should '
'not need to be changed.'),
('scoreboard_url', 'Link to the NHL.com scoreboard page. Like '
'**live_url**, this value should not need to be '
'changed.'),
('api_url', 'Alternate URL string from which to retrieve score data. '
'Like **live_url**, this value should not need to be '
'changed.'),
)
required = ()
_default_colors = {
'ANA': '#B4A277',
'ARI': '#AC313A',
'BOS': '#F6BD27',
'BUF': '#1568C5',
'CAR': '#FA272E',
'CBJ': '#1568C5',
'CGY': '#D23429',
'CHI': '#CD0E24',
'COL': '#9F415B',
'DAL': '#058158',
'DET': '#E51937',
'EDM': '#2F6093',
'FLA': '#E51837',
'LAK': '#DADADA',
'MIN': '#176B49',
'MTL': '#C8011D',
'NJD': '#CC0000',
'NSH': '#FDB71A',
'NYI': '#F8630D',
'NYR': '#1576CA',
'OTT': '#C50B2F',
'PHI': '#FF690B',
'PIT': '#FFB81C',
'SJS': '#007888',
'STL': '#1764AD',
'TBL': '#296AD5',
'TOR': '#296AD5',
'VAN': '#0454FA',
'VGK': '#B4975A',
'WPG': '#1568C5',
'WSH': '#E51937',
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'final', 'pregame']
display_order = _valid_display_order
format_no_games = 'NHL: No games'
format_pregame = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}-{home_otl}) {start_time:%H:%M %Z}'
format_in_progress = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})][ ({away_empty_net})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})][ ({home_empty_net})] ({time_remaining} {period})'
format_final = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}-{home_otl}) (Final[/{overtime}])'
empty_net = 'EN'
team_colors = _default_colors
live_url = LIVE_URL
scoreboard_url = SCOREBOARD_URL
api_url = API_URL
@require(internet)
def check_scores(self):
self.get_api_date()
url = self.api_url % (self.date.year, self.date.month, self.date.day,
self.date.year, self.date.month, self.date.day)
game_list = self.get_nested(self.api_request(url),
'dates:0:games',
default=[])
# Convert list of games to dictionary for easy reference later on
data = {}
team_game_map = {}
for game in game_list:
try:
id_ = game['gamePk']
except KeyError:
continue
try:
for key in ('home', 'away'):
team = game['teams'][key]['team']['abbreviation'].upper()
if team in self.favorite_teams:
team_game_map.setdefault(team, []).append(id_)
except KeyError:
continue
data[id_] = game
self.interpret_api_return(data, team_game_map)
def process_game(self, game):
ret = {}
self.logger.debug('Processing %s game data: %s',
self.__class__.__name__, game)
linescore = self.get_nested(game, 'linescore', default={})
ret['id'] = game['gamePk']
ret['live_url'] = self.live_url % ret['id']
ret['period'] = self.get_nested(
linescore,
'currentPeriodOrdinal')
ret['time_remaining'] = self.get_nested(
linescore,
'currentPeriodTimeRemaining',
callback=lambda x: x.capitalize())
ret['venue'] = self.get_nested(
game,
'venue:name')
pp_strength = self.get_nested(linescore, 'powerPlayStrength')
for team in ('away', 'home'):
team_data = self.get_nested(game, 'teams:%s' % team, default={})
if team == 'home':
ret['venue'] = self.get_nested(team_data, 'venue:name')
ret['%s_score' % team] = self.get_nested(
team_data,
'score',
callback=self.force_int,
default=0)
ret['%s_wins' % team] = self.get_nested(
team_data,
'leagueRecord:wins',
callback=self.force_int,
default=0)
ret['%s_losses' % team] = self.get_nested(
team_data,
'leagueRecord:losses',
callback=self.force_int,
default=0)
ret['%s_otl' % team] = self.get_nested(
team_data,
'leagueRecord:ot',
callback=self.force_int,
default=0)
ret['%s_city' % team] = self.get_nested(
team_data,
'team:shortName')
ret['%s_name' % team] = self.get_nested(
team_data,
'team:teamName')
ret['%s_abbrev' % team] = self.get_nested(
team_data,
'team:abbreviation')
ret['%s_power_play' % team] = self.get_nested(
linescore,
'teams:%s:powerPlay' % team,
callback=lambda x: pp_strength if x and pp_strength != 'Even' else '')
ret['%s_empty_net' % team] = self.get_nested(
linescore,
'teams:%s:goaliePulled' % team,
callback=lambda x: self.empty_net if x else '')
if game.get('gameType') == 'P':
# Calculate wins/losses in current playoff series
home_rem = ret['home_wins'] % 4
away_rem = ret['away_wins'] % 4
if ret['home_wins'] == ret['away_wins']:
if home_rem == 0:
# Both teams have multiples of 4 wins, so series has no
# completed games.
ret['home_wins'] = ret['away_wins'] = 0
else:
ret['home_wins'] = home_rem
ret['away_wins'] = away_rem
elif ret['home_wins'] > ret['away_wins']:
ret['home_wins'] = 4 if home_rem == 0 else home_rem
ret['away_wins'] = away_rem
else:
ret['away_wins'] = 4 if away_rem == 0 else away_rem
ret['home_wins'] = home_rem
# Series losses are the other team's wins
ret['home_losses'] = ret['away_wins']
ret['away_losses'] = ret['home_wins']
ret['status'] = self.get_nested(
game,
'status:abstractGameState',
callback=lambda x: x.lower().replace(' ', '_'))
if ret['status'] == 'live':
ret['status'] = 'in_progress'
elif ret['status'] == 'final':
ret['overtime'] = self.get_nested(
linescore,
'currentPeriodOrdinal',
callback=lambda x: x if 'OT' in x or x == 'SO' else '')
elif ret['status'] != 'in_progress':
ret['status'] = 'pregame'
# Game time is in UTC, ISO format, thank the FSM
# Ex. 2016-04-02T17:00:00Z
game_time_str = game.get('gameDate', '')
try:
game_time = datetime.strptime(game_time_str, '%Y-%m-%dT%H:%M:%SZ')
except ValueError as exc:
# Log when the date retrieved from the API return doesn't match the
# expected format (to help troubleshoot API changes), and set an
# actual datetime so format strings work as expected. The times
# will all be wrong, but the logging here will help us make the
# necessary changes to adapt to any API changes.
self.logger.error(
'Error encountered determining %s game time for game %s:',
self.__class__.__name__,
game['id'],
exc_info=True
)
game_time = datetime.datetime(1970, 1, 1)
ret['start_time'] = pytz.utc.localize(game_time).astimezone()
self.logger.debug('Returned %s formatter data: %s',
self.__class__.__name__, ret)
return ret
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scratchpad.py 0000664 0000000 0000000 00000003602 13567273623 0023172 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from threading import Thread
from i3pystatus import Module
import i3ipc
class Scratchpad(Module):
"""
Display the amount of windows and indicate urgency hints on scratchpad (async).
fork from scratchpad_async of py3status by cornerman
Requires the PyPI package `i3ipc`.
.. rubric:: Available formaters
* `{number}` — amount of windows on scratchpad
@author jok
@license BSD
"""
settings = (
("format", "format string."),
("always_show", "whether the indicator should be shown if there are"
" no scratchpad windows"),
("color_urgent", "color of urgent"),
("color", "text color"),
)
format = u"{number} ⌫"
always_show = True
color_urgent = "#900000"
color = "#FFFFFF"
def init(self):
self.count = 0
self.urgent = False
t = Thread(target=self._listen)
t.daemon = True
t.start()
def update_scratchpad_counter(self, conn, *args):
cons = conn.get_tree().scratchpad().leaves()
self.urgent = any(con for con in cons if con.urgent)
self.count = len(cons)
# output
if self.urgent:
color = self.color_urgent
else:
color = self.color
if self.always_show or self.count > 0:
full_text = self.format.format(number=self.count)
else:
full_text = ''
self.output = {
"full_text": full_text,
"color": color,
}
def _listen(self):
conn = i3ipc.Connection()
self.update_scratchpad_counter(conn)
conn.on('window::move', self.update_scratchpad_counter)
conn.on('window::urgent', self.update_scratchpad_counter)
conn.on('window::new', self.update_scratchpad_counter)
conn.on('window::close', self.update_scratchpad_counter)
conn.main()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/sensu.py 0000664 0000000 0000000 00000007035 13567273623 0022217 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule, formatp
from enum import IntEnum
import requests
from urllib.parse import urljoin
class SensuCheck(IntervalModule):
""" Pool sensu api events
.. rubric:: Available formatters
* {status} OK if not events else numbers of events
* {last_event} Display the output of the most recent event (with priority on error event)
"""
interval = 5
required = ("api_url",)
settings = (
("api_url", "URL of Sensu API. e.g: http://localhost/sensu/"),
"api_username",
"api_password",
"format",
"color_error",
"color_warn",
"color_ok",
("last_event_label", "Label to put before the last event output (default 'Last:')"),
("max_event_field", "Defines max length of the last_event message field "
"(default: 50)"),
)
api_url = None
api_username = None
api_password = None
format = "{status}"
color_error = "#ff0000"
color_warn = "#f9ba46"
color_ok = "#00ff00"
last_event_label = "Last:"
max_event_field = 50
def run(self):
try:
auth = ()
if self.api_username:
auth = (self.api_username, self.api_password or "")
response = requests.get(urljoin(self.api_url, "events"), auth=auth)
if response.status_code != requests.codes.OK:
self.error("could not query sensu api: {}".format(response.status_code))
else:
try:
events = response.json()
except ValueError:
self.error("could not decode json")
else:
try:
self.set_output(events)
except KeyError as exc:
self.error("could not find field {!s} in event".format(exc))
except Exception as exc:
self.output = {
"full_text": "FAILED: {!s}".format(str(exc)),
"color": self.color_error,
}
def set_output(self, events):
events = sorted(
[e for e in events if e["action"] != "resolve" and not e["silenced"]],
key=lambda x: x["last_ok"],
reverse=True
)
last_event_output = ""
if not events:
status = "OK"
color = self.color_ok
else:
error = None
try:
error = next(e for e in events if e["check"]["status"] == SensuStatus.critical)
except StopIteration:
last_event = events[0]
else:
last_event = error
status = "{} event(s)".format(len(events))
color = self.color_error if error else self.color_warn
last_event_output = self.get_event_output(last_event)
self.output = {
"full_text": formatp(self.format, status=status, last_event=last_event_output),
"color": color,
}
def get_event_output(self, event):
output = (event["check"]["output"] or "").replace("\n", " ")
if self.last_event_label:
output = "{} {}".format(self.last_event_label, output)
if self.max_event_field and len(output) > self.max_event_field:
output = output[:self.max_event_field]
return output
def error(self, error_msg):
self.output = {
"full_text": error_msg,
"color": self.color_error,
}
class SensuStatus(IntEnum):
ok = 0
warn = 1
critical = 2
unknown = 3
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/sge.py 0000664 0000000 0000000 00000002777 13567273623 0021650 0 ustar 00root root 0000000 0000000 import subprocess
from lxml import etree
from i3pystatus import IntervalModule
class SGETracker(IntervalModule):
"""
Used to display status of Batch computing jobs on a cluster running Sun Grid Engine.
The data is collected via ssh, so a valid ssh address must be specified.
Requires lxml.
"""
interval = 60
settings = (
("ssh", "The SSH connection address. Can be user@host or user:password@host or user@host -p PORT etc."),
'color', 'format'
)
required = ("ssh",)
format = "SGE qw: {queued} / r: {running} / Eqw: {error}"
on_leftclick = None
color = "#ffffff"
def parse_qstat_xml(self):
xml = subprocess.check_output("ssh {0} \"qstat -xml\"".format(self.ssh),
stderr=subprocess.STDOUT,
shell=True)
root = etree.fromstring(xml)
job_dict = {'qw': 0, 'Eqw': 0, 'r': 0}
for j in root.xpath('//job_info/job_info/job_list'):
job_dict[j.find("state").text] += 1
for j in root.xpath('//job_info/queue_info/job_list'):
job_dict[j.find("state").text] += 1
return job_dict
def run(self):
jobs = self.parse_qstat_xml()
fdict = {
"queued": jobs['qw'],
"error": jobs['Eqw'],
"running": jobs['r']
}
self.data = fdict
self.output = {
"full_text": self.format.format(**fdict).strip(),
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/shell.py 0000664 0000000 0000000 00000002461 13567273623 0022167 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.command import run_through_shell
class Shell(IntervalModule):
"""
Shows output of shell command
.. rubric:: Available formatters
* `{output}` — just the striped command output without newlines
"""
color = "#FFFFFF"
error_color = "#FF0000"
ignore_empty_stdout = False
settings = (
("command", "command to be executed"),
("ignore_empty_stdout", "Let the block be empty"),
("color", "standard color"),
("error_color", "color to use when non zero exit code is returned"),
"format"
)
required = ("command",)
format = "{output}"
def run(self):
retvalue, out, stderr = run_through_shell(self.command, enable_shell=True)
if retvalue != 0:
self.logger.error(stderr if stderr else "Unknown error")
if out:
out = out.replace("\n", " ").strip()
elif stderr:
out = stderr
full_text = self.format.format(output=out).strip()
if not full_text and not self.ignore_empty_stdout:
full_text = "Command `%s` returned %d" % (self.command, retvalue)
self.output = {
"full_text": full_text,
"color": self.color if retvalue == 0 else self.error_color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/solaar.py 0000664 0000000 0000000 00000004352 13567273623 0022342 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.command import run_through_shell
class DeviceNotFound(Exception):
pass
class NoBatteryStatus(Exception):
message = None
def __init__(self, message):
self.message = message
class Solaar(IntervalModule):
"""
Shows status and load percentage of logitech's unifying device
.. rubric:: Available formatters
* `{output}` — percentage of battery and status
"""
color = "#FFFFFF"
error_color = "#FF0000"
interval = 30
settings = (
("nameOfDevice", "name of the logitech's unifying device"),
("color", "standard color"),
("error_color", "color to use when non zero exit code is returned"),
)
required = ("nameOfDevice",)
def findDeviceNumber(self):
command = 'solaar show'
retvalue, out, stderr = run_through_shell(command, enable_shell=True)
for line in out.split('\n'):
if line.count(self.nameOfDevice) > 0 and line.count(':') > 0:
numberOfDevice = line.split(':')[0]
return numberOfDevice
raise DeviceNotFound()
def findBatteryStatus(self, numberOfDevice):
command = 'solaar show %s' % (numberOfDevice)
retvalue, out, stderr = run_through_shell(command, enable_shell=True)
for line in out.split('\n'):
if line.count('Battery') > 0:
if line.count(':') > 0:
batterystatus = line.split(':')[1].strip().strip(",")
return batterystatus
elif line.count('offline'):
raise NoBatteryStatus('offline')
else:
raise NoBatteryStatus('unknown')
raise NoBatteryStatus('unknown/error')
def run(self):
self.output = {}
try:
device_number = self.findDeviceNumber()
output = self.findBatteryStatus(device_number)
self.output['color'] = self.color
except DeviceNotFound:
output = "device absent"
self.output['color'] = self.error_color
except NoBatteryStatus as e:
output = e.message
self.output['color'] = self.error_color
self.output['full_text'] = output
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/sonos.py 0000664 0000000 0000000 00000011456 13567273623 0022225 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
import soco
class Sonos(IntervalModule):
"""
Controls and displays information from Sonos devices.
A devices is found by IP, by name, or automatically if no IP or name is \
supplied.
.. rubric:: Available formatters
* `{player_name}` — player name
* `{volume}` — volume from 0 to 100
* `{muted}` — "M" if muted, else ""
* `{title}` — the title of the current song
* `{artist}` — the artist of the current song
* `{album}` — the album of the current song
* `{duration}` — duration of the current song (%M:%S)
* `{position}` — position in the current song (%M:%S)
* `{state}` — Playing, Paused, Stopped
Requires: soco (can be installed using pip)
"""
ip = None
name = None
format = "{state}: {artist} - {title} [{muted}{volume:.0f}%]"
color = "#FFFFFF"
format_no_music = "No music"
color_no_music = "#888888"
format_no_connection = "No connection"
color_no_connection = "#888888"
hide_no_connection = False
interval = 1
settings = (
("ip", "Speaker IP address."),
("name", "Speaker name (used if no IP is given)."),
("format", "Format used when playing or paused."),
("color", "Color used when playing or paused."),
("format_no_music", "Format used when stopped."),
("color_no_music", "Color used when stopped."),
("format_no_connection", "Format used if no player is connected."),
("color_no_connection", "Color used if no player is connected."),
("hide_no_connection", "Hide output if no player is connected."),
)
state_text_map = {
"PLAYING": "Playing",
"TRANSITIONING": "Playing",
"PAUSED_PLAYBACK": "Paused",
"STOPPED": "Stopped",
}
on_leftclick = "play_pause"
on_upscroll = "incr_vol"
on_middleclick = "toggle_mute"
on_downscroll = "decr_vol"
on_doubleleftclick = "next_song"
player = None
def run(self):
if not self.player:
if self.ip:
self.player = soco.SoCo(self.ip)
elif self.name:
self.player = soco.discovery.by_name(self.name)
else:
self.player = soco.discovery.any_soco()
if not self.player:
self.output = self.output_no_connection
return
try:
track_info = self.group_coordinator.get_current_track_info()
transp_info = self.group_coordinator.get_current_transport_info()
player_name = self.player.player_name
muted = self.player.mute
volume = self.player.volume
except:
self.output = self.output_no_connection
return
state = transp_info["current_transport_state"]
cdict = {
"player_name": player_name,
"volume": volume,
"muted": "M" if muted else "",
"title": track_info["title"],
"artist": track_info["artist"],
"album": track_info["album"],
"duration": track_info["duration"][2:],
"position": track_info["position"][2:],
"state": self.state_text_map[state],
}
self.data = cdict
if self.format_no_music is not None and state == "STOPPED":
self.output = {
"full_text": self.format_no_music.format(**cdict),
"color": self.color_no_music
}
else:
self.output = {
"full_text": self.format.format(**cdict),
"color": self.color
}
@property
def output_no_connection(self):
if self.hide_no_connection:
return None
else:
return {
"full_text": self.format_no_connection,
"color": self.color_no_connection
}
@property
def group_coordinator(self):
try:
return self.player.group.coordinator
except:
return None
def play_pause(self):
try:
transp_info = self.group_coordinator.get_current_transport_info()
state = transp_info["current_transport_state"]
if state in ["PLAYING", "TRANSITIONING"]:
self.group_coordinator.pause()
else:
self.group_coordinator.play()
except:
return
def incr_vol(self):
try:
self.player.volume += 1
except:
return
def decr_vol(self):
try:
self.player.volume -= 1
except:
return
def toggle_mute(self):
try:
self.player.mute = not self.player.mute
except:
return
def next_song(self):
try:
self.group_coordinator.next()
except:
return
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/spaceapi.py 0000664 0000000 0000000 00000002467 13567273623 0022653 0 ustar 00root root 0000000 0000000 import json
from urllib.request import urlopen
from datetime import datetime
from i3pystatus import IntervalModule
from i3pystatus.core.util import internet, require
class SpaceAPI(IntervalModule):
"""
Show if a hackerspace is open
.. rubric:: Available formatters
* {state}
* {message}
* {lastchange}
"""
data = {}
format = "S: {state}"
color_open = "#00FF00"
color_closed = "#FF0000"
interval = 10
settings = (
("url", "spaceapi endpoint"),
("format", "format string used for output."),
("color_open", "color if hackerspace is opened"),
("color_closed", "color if hackerspace is closed"),
("interval", "update interval")
)
required = ('url', )
url = None
@require(internet)
def run(self):
res = urlopen(self.url)
api = json.loads(res.read())
self.data['color'] = self.color_open if api['state']['open'] else self.color_closed
self.data['state'] = 'open' if api['state']['open'] else 'closed'
self.data['message'] = api['state'].get('message', '')
self.data['lastchange'] = datetime.fromtimestamp(int(api['state']['lastchange']))
self.output = {
"full_text": self.format.format(**self.data),
"color": self.data['color']
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/spotify.py 0000664 0000000 0000000 00000000302 13567273623 0022545 0 ustar 00root root 0000000 0000000 from i3pystatus.now_playing import NowPlaying
class Spotify(NowPlaying):
"""
Get Spotify info using dbus interface. Based on `now_playing`_ module.
"""
player_name = "spotify"
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/swap.py 0000664 0000000 0000000 00000005032 13567273623 0022027 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from psutil import swap_memory
from .core.util import round_dict
class Swap(IntervalModule):
"""
Shows swap load
.. rubric:: Available formatters
* {free}
* {percent_used}
* {used}
* {total}
Requires psutil (from PyPI)
"""
format = "{free} MiB"
format_no_swap = "No swap"
hide_if_empty = False
divisor = 1024 ** 2
color = "#00FF00"
warn_color = "#FFFF00"
alert_color = "#FF0000"
color_no_swap = "#FFFFFF"
warn_percentage = 50
alert_percentage = 80
round_size = 1
settings = (
("format", "format string used for output."),
("format_no_swap",
"format string used when no swap is enabled, "
"set to None to use default format"),
("hide_if_empty", "hide swap block when swap is not used"),
("divisor",
"divide all byte values by this value, default is 1024**2 (megabytes)"),
("warn_percentage", "minimal percentage for warn state"),
("alert_percentage", "minimal percentage for alert state"),
("color", "standard color"),
("warn_color",
"defines the color used when warn percentage is exceeded"),
("alert_color",
"defines the color used when alert percentage is exceeded"),
("color_no_swap",
"defines the color used when no swap is enabled, "
"set to None to use default color"),
("round_size", "defines number of digits in round"),
)
def run(self):
swap_usage = swap_memory()
if self.hide_if_empty and swap_usage.used == 0:
self.output = {}
return
elif swap_usage.total == 0:
format = self.format_no_swap if self.format_no_swap else self.format
color = self.color_no_swap if self.color_no_swap else self.color
else:
format = self.format
if swap_usage.percent >= self.alert_percentage:
color = self.alert_color
elif swap_usage.percent >= self.warn_percentage:
color = self.warn_color
else:
color = self.color
cdict = {
"free": swap_usage.free / self.divisor,
"percent_used": swap_usage.percent,
"used": swap_usage.used / self.divisor,
"total": swap_usage.total / self.divisor,
}
round_dict(cdict, self.round_size)
self.data = cdict
self.output = {
"full_text": format.format(**cdict),
"color": color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/syncthing.py 0000664 0000000 0000000 00000010302 13567273623 0023057 0 ustar 00root root 0000000 0000000 import json
import os.path
import requests
from subprocess import call
from urllib.parse import urljoin
import xml.etree.ElementTree as ET
from i3pystatus import IntervalModule
from i3pystatus.core.util import user_open
class Syncthing(IntervalModule):
"""
Check Syncthing's online status and start/stop Syncthing via
click events.
Requires `requests`.
"""
format_up = 'ST up'
color_up = '#00ff00'
format_down = 'ST down'
color_down = '#ff0000'
configfile = '~/.config/syncthing/config.xml'
url = 'auto'
apikey = 'auto'
verify_ssl = True
interval = 10
on_leftclick = 'st_open'
on_rightclick = 'st_toggle_systemd'
settings = (
('format_up', 'Text to show when Syncthing is running'),
('format_down', 'Text to show when Syncthing is not running'),
('color_up', 'Color when Syncthing is running'),
('color_down', 'Color when Syncthing is not running'),
('configfile', 'Path to Syncthing config'),
('url', 'Syncthing GUI URL; "auto" reads from local config'),
('apikey', 'Syncthing APIKEY; "auto" reads from local config'),
('verify_ssl', 'Verify SSL certificate'),
)
def st_get(self, endpoint):
# TODO: Maybe we can share a session across multiple GETs.
with requests.Session() as s:
r = s.get(self.url)
csrf_name, csfr_value = r.headers['Set-Cookie'].split('=')
s.headers.update({'X-' + csrf_name: csfr_value})
r = s.get(
urljoin(self.url, endpoint),
verify=self.verify_ssl,
)
return json.loads(r.text)
def st_post(self, endpoint, data=None):
headers = {'X-API-KEY': self.apikey}
requests.post(
urljoin(self.url, endpoint),
data=data,
headers=headers,
)
def read_config(self):
self.configfile = os.path.expanduser(self.configfile)
# Parse config only once!
if self.url == 'auto' or self.apikey == 'auto':
tree = ET.parse(self.configfile)
root = tree.getroot()
if self.url == 'auto':
tls = root.find('./gui').attrib['tls']
address = root.find('./gui/address').text
if tls == 'true':
self.url = 'https://' + address
else:
self.url = 'http://' + address
if self.apikey == 'auto':
self.apikey = root.find('./gui/apikey').text
def ping(self):
try:
ping_data = self.st_get('/rest/system/ping')
if ping_data['ping'] == 'pong':
return True
else:
return False
except requests.exceptions.ConnectionError:
return False
def run(self):
self.read_config()
self.online = True if self.ping() else False
if self.online:
self.output = {
'full_text': self.format_up,
'color': self.color_up
}
else:
self.output = {
'full_text': self.format_down,
'color': self.color_down
}
# Callbacks
def st_open(self):
"""Callback: Open Syncthing web UI"""
user_open(self.url)
def st_restart(self):
"""Callback: Restart Syncthing"""
self.st_post('/rest/system/restart')
def st_stop(self):
"""Callback: Stop Syncthing"""
self.st_post('/rest/system/shutdown')
def st_start_systemd(self):
"""Callback: systemctl --user start syncthing.service"""
call(['systemctl', '--user', 'start', 'syncthing.service'])
def st_restart_systemd(self):
"""Callback: systemctl --user restart syncthing.service"""
call(['systemctl', '--user', 'restart', 'syncthing.service'])
def st_stop_systemd(self):
"""Callback: systemctl --user stop syncthing.service"""
call(['systemctl', '--user', 'stop', 'syncthing.service'])
def st_toggle_systemd(self):
"""Callback: start Syncthing service if offline, or stop it when online"""
if self.online:
self.st_stop_systemd()
else:
self.st_start_systemd()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/taskwarrior.py 0000664 0000000 0000000 00000007247 13567273623 0023437 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from json import loads
import subprocess
class Taskwarrior(IntervalModule):
"""
Check Taskwarrior for pending tasks
Requires `json`
.. rubric:: Available formatters (uses :ref:`formatp`)
* `{ready}` — contains number of tasks returned by `ready_filter`
* `{urgent}` — contains number of tasks returned by `urgent_filter`
* `{next}` — contains the description of next task
* `{project}` — contains the projects the next task belongs to
.. rubric:: Available callbacks
* ``get_next_task`` — Display the next most urgent task.
* ``get_prev_task`` — Display the previous most urgent task.
* ``reset_next_task`` — Display the most urgent task, resetting any \
switching by other callbacks.
"""
format = 'Task: {next}'
ready_filter = '+READY'
urgent_filter = '+TODAY'
enable_mark_done = False
color_urgent = '#FF0000'
color_ready = '#78EAF2'
ready_tasks = []
urgent_tasks = []
current_tasks = []
next_id = 0
next_task = None
on_upscroll = "get_prev_task"
on_downscroll = "get_next_task"
on_rightclick = 'mark_task_as_done'
on_leftclick = "reset_next_task"
settings = (
('format', 'format string'),
('ready_filter', 'Filters to get ready tasks example: `+READY`'),
('urgent_filter', 'Filters to get urgent tasks example: `+TODAY`'),
('enable_mark_done', 'Enable right click mark task as done'),
('color_urgent', '#FF0000'),
('color_ready', '#78EAF2')
)
def reset_next_task(self):
self.next_id = 0
self.next_task = self.current_tasks[self.next_id]
def get_next_task(self):
self.next_id = (self.next_id + 1) % len(self.current_tasks)
self.next_task = self.current_tasks[self.next_id]
def get_prev_task(self):
self.next_id = (self.next_id - 1) % len(self.current_tasks)
self.next_task = self.current_tasks[self.next_id]
def mark_task_as_done(self):
if self.enable_mark_done and self.next_task is not None:
subprocess.check_output(['task', str(self.next_task['id']), 'done'])
self.get_next_task()
def run(self):
try:
urgent_params = ['task'] + self.urgent_filter.split(' ') + ['export']
urgent_tasks_json = subprocess.check_output(urgent_params)
self.urgent_tasks = loads(urgent_tasks_json.decode("utf-8"))
self.urgent_tasks = sorted(self.urgent_tasks, key=lambda x: x['urgency'], reverse=True)
ready_params = ['task'] + self.ready_filter.split(' ') + ['export']
ready_tasks = subprocess.check_output(ready_params)
self.ready_tasks = loads(ready_tasks.decode("utf-8"))
self.ready_tasks = sorted(self.ready_tasks, key=lambda x: x['urgency'], reverse=True)
self.current_tasks = self.urgent_tasks if len(self.urgent_tasks) > 0 else self.ready_tasks
if self.next_id < len(self.current_tasks):
self.next_task = self.current_tasks[self.next_id]
else:
self.next_id = 0
except ValueError:
self.logger.exception('Decoding JSON has failed')
raise
format_values = dict(urgent=len(self.urgent_tasks), ready=len(self.ready_tasks), next='')
if self.next_task is not None:
format_values['next'] = self.next_task['description']
format_values['project'] = self.next_task.get('project', '')
self.output = {
'full_text': self.format.format(**format_values),
'color': self.color_urgent if len(self.urgent_tasks) > 0 else self.color_ready
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/temp.py 0000664 0000000 0000000 00000022350 13567273623 0022024 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.util import make_vertical_bar
class Sensor:
"""
Simple class representing a CPU temperature sensor.
"""
def __init__(self, name, current, maximum, critical):
self.name = name.replace(' ', '_')
self.current = int(current)
self.maximum = int(maximum) if maximum else int(critical)
self.critical = int(critical)
def __repr__(self):
return "Sensor(name='{}', current={}, maximum={}, critical={})".format(
self.name,
self.current,
self.maximum,
self.critical
)
def is_warning(self):
return self.current > self.maximum
def is_critical(self):
return self.current > self.critical
def get_sensors():
""" Detect and return a list of Sensor objects """
import sensors
found_sensors = list()
def get_subfeature_value(feature, subfeature_type):
subfeature = chip.get_subfeature(feature, subfeature_type)
if subfeature:
return chip.get_value(subfeature.number)
for chip in sensors.get_detected_chips():
for feature in chip.get_features():
if feature.type == sensors.FEATURE_TEMP:
try:
name = chip.get_label(feature)
max = get_subfeature_value(feature, sensors.SUBFEATURE_TEMP_MAX)
current = get_subfeature_value(feature, sensors.SUBFEATURE_TEMP_INPUT)
critical = get_subfeature_value(feature, sensors.SUBFEATURE_TEMP_CRIT)
if critical:
found_sensors.append(Sensor(name=name, current=current, maximum=max, critical=critical))
except sensors.SensorsException:
continue
return found_sensors
class Temperature(IntervalModule, ColorRangeModule):
"""
Shows CPU temperature of Intel processors.
AMD is currently not supported as they can only report a relative temperature, which is pretty useless.
Requires `colour` module from PyPi
.. rubric:: Modes of operation
If lm_sensors_enabled is set to False, the module operates in default mode. This means that:
* only the {temp} formatter is available
* alert_temp is honored
If lm_sensors_enabled is set to True, the module operates in lm_sensors mode. This means that:
* pysensors must be installed (https://github.com/bastienleonard/pysensors)
* CPU sensors are discovered dynamically (supporting a sensor per core and multiple CPUs)
* alert_temp is ignored. The warning or critical values reported by the sensor are used instead (see urgent_on)
.. rubric:: lm_sensors installation
In order to take advantage of the lm_sensors library and tools, it must first be installed and configured.
On Arch this is as simple as:
.. code-block:: bash
pacman -S lm_sensors
On Ubuntu:
.. code-block:: bash
sudo apt-get update && sudo apt-get install lm-sensors libsensors4-dev
The Arch Wiki has a good page on the library - https://wiki.archlinux.org/index.php/lm_sensors
.. rubric:: lm_sensors_mode formatters
When ``lm_sensors_enabled`` is True the formatters are created dynamically. In order to discover the formatters that
are available, it is best to run the sensors command:
.. code-block:: bash
⇒ sensors
coretemp-isa-0000
Adapter: ISA adapter
Physical id 0: +48.0°C (high = +80.0°C, crit = +99.0°C)
Core 0: +48.0°C (high = +80.0°C, crit = +99.0°C)
Core 1: +46.0°C (high = +80.0°C, crit = +99.0°C)
Core 2: +43.0°C (high = +80.0°C, crit = +99.0°C)
Core 3: +47.0°C (high = +80.0°C, crit = +99.0°C)
The module replaces spaces in sensor names with underscores, therefore from the above output we can
identify the following sensor formatters:
* Physical_id_0
* Core_0
* Core_1
* Core_2
* Core_3
For each sensor a vertical bar is also generated. In this example we would also have the following bars:
* Physical_id_0_bar
* Core_0_bar
* Core_1_bar
* Core_2_bar
* Core_3_bar
Thus, this format string would be valid: "{Physical_id_0}°C {Core_0_bar}{Core_1_bar}{Core_2_bar}{Core_3_bar}"
.. rubric:: Pango Markup and lm_sensors_mode
When Pango Markup is enabled and ``dynamic_color`` is True, each sensor's formatter color is displayed independently.
The color is determined by the proximity of the sensors current value to it's critical value.
.. rubric:: Example Configuration
Here is an example configuration based on the sensor values discovered above:
.. code-block:: python
status.register("temp",
format="{Physical_id_0}°C {Core_0_bar}{Core_1_bar}{Core_2_bar}{Core_3_bar}",
hints={"markup": "pango"},
lm_sensors_enabled=True,
dynamic_color=True)
"""
settings = (
("format",
"format string used for output. {temp} is the temperature in degrees celsius"),
('display_if', 'snippet that gets evaluated. if true, displays the module output'),
('lm_sensors_enabled', 'whether or not lm_sensors should be used for obtaining CPU temperature information'),
('urgent_on', 'whether to flag as urgent when temperature exceeds urgent value or critical value '
'(requires lm_sensors_enabled)'),
('dynamic_color', 'whether to set the color dynamically (overrides alert_color)'),
"color",
"file",
"alert_temp",
"alert_color",
)
format = "{temp} °C"
color = "#FFFFFF"
file = "/sys/class/thermal/thermal_zone0/temp"
alert_temp = 90
alert_color = "#FF0000"
display_if = 'True'
lm_sensors_enabled = False
dynamic_color = False
urgent_on = 'warning'
def init(self):
self.pango_enabled = self.hints.get("markup", False) and self.hints["markup"] == "pango"
self.colors = self.get_hex_color_range(self.start_color, self.end_color, 100)
def run(self):
if eval(self.display_if):
if self.lm_sensors_enabled:
self.output = self.get_output_sensors()
else:
self.output = self.get_output_original()
def get_output_original(self):
"""
Build the output the original way. Requires no third party libraries.
"""
with open(self.file, "r") as f:
temp = float(f.read().strip()) / 1000
if self.dynamic_color:
perc = int(self.percentage(int(temp), self.alert_temp))
if (perc > 99):
perc = 99
color = self.colors[perc]
else:
color = self.color if temp < self.alert_temp else self.alert_color
return {
"full_text": self.format.format(temp=temp),
"color": color,
}
def get_output_sensors(self):
"""
Build the output using lm_sensors. Requires sensors Python module (see docs).
"""
data = dict()
found_sensors = get_sensors()
if len(found_sensors) == 0:
raise Exception("No sensors detected! "
"Ensure lm-sensors is installed and check the output of the `sensors` command.")
for sensor in found_sensors:
data[sensor.name] = self.format_sensor(sensor)
data["{}_bar".format(sensor.name)] = self.format_sensor_bar(sensor)
data['temp'] = max((s.current for s in found_sensors))
return {
'full_text': self.format.format(**data),
'urgent': self.get_urgent(found_sensors),
'color': self.color if not self.dynamic_color else None,
}
def get_urgent(self, sensors):
""" Determine if any sensors should set the urgent flag. """
if self.urgent_on not in ('warning', 'critical'):
raise Exception("urgent_on must be one of (warning, critical)")
for sensor in sensors:
if self.urgent_on == 'warning' and sensor.is_warning():
return True
elif self.urgent_on == 'critical' and sensor.is_critical():
return True
return False
def format_sensor(self, sensor):
""" Format a sensor value. If pango is enabled color is per sensor. """
current_val = sensor.current
if self.pango_enabled:
percentage = self.percentage(sensor.current, sensor.critical)
if self.dynamic_color:
color = self.colors[int(percentage)]
return self.format_pango(color, current_val)
return current_val
def format_sensor_bar(self, sensor):
""" Build and format a sensor bar. If pango is enabled bar color is per sensor."""
percentage = self.percentage(sensor.current, sensor.critical)
bar = make_vertical_bar(int(percentage))
if self.pango_enabled:
if self.dynamic_color:
color = self.colors[int(percentage)]
return self.format_pango(color, bar)
return bar
def format_pango(self, color, value):
return '{}'.format(color, value)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/text.py 0000664 0000000 0000000 00000000606 13567273623 0022043 0 ustar 00root root 0000000 0000000 from i3pystatus import Module
class Text(Module):
"""
Display static, colored text.
"""
settings = (
"text",
("color", "HTML color code #RRGGBB"),
)
required = ("text",)
color = None
def init(self):
self.output = {
"full_text": self.text
}
if self.color:
self.output["color"] = self.color
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/timer.py 0000664 0000000 0000000 00000013354 13567273623 0022203 0 ustar 00root root 0000000 0000000 import time
from i3pystatus import IntervalModule
from i3pystatus.core.command import execute
from i3pystatus.core.util import TimeWrapper
class TimerState:
stopped = 0
running = 1
overflow = 2
class Timer(IntervalModule):
"""
Timer module to remind yourself that there probably is something else you
should be doing right now.
Main features include:
- Set custom time interval with click events.
- Different output formats triggered when remaining time is less than `x`
seconds.
- Execute custom python function or external command when timer overflows
(or reaches zero depending on how you look at it).
.. rubric:: Available formatters
Time formatters are available to show the remaining time.
These include ``%h``, ``%m``, ``%s``, ``%H``, ``%M``, ``%S``.
See :py:class:`.TimeWrapper` for detailed description.
The ``format_custom`` setting allows you to display different formats when
certain amount of seconds is remaining.
This setting accepts list of tuples which contain time in seconds,
format string and color string each.
See the default settings for an example:
- ``(0, "+%M:%S", "#ffffff")`` - Use this format after overflow. White text
with red background set by the urgent flag.
- ``(60, "-%M:%S", "#ffa500")`` - Change color to orange in last minute.
- ``(3600, "-%M:%S", "#00ff00")`` - Hide hour digits when remaining time is
less than one hour.
Only first matching rule is applied (if any).
.. rubric:: Callbacks
Module contains three mouse event callback methods:
- :py:meth:`.start` - Default: Left click starts (or adds) 5 minute
countdown.
- :py:meth:`.increase` - Default: Upscroll/downscroll increase/decrease time
by 1 minute.
- :py:meth:`.reset` - Default: Right click resets timer.
Two new event settings were added:
- ``on_overflow`` - Executed when remaining time reaches zero.
- ``on_reset`` - Executed when timer is reset but only if overflow occured.
These settings accept either a python callable object or a string with shell
command.
Python callbacks should be non-blocking and without any arguments.
Here is an example that plays a short sound file in 'loop' every 60 seconds
until timer is reset.
(``play`` is part of ``SoX`` - the Swiss Army knife of audio manipulation)
::
on_overflow = "play -q /path/to/sound.mp3 pad 0 60 repeat -"
on_reset = "pkill -SIGTERM -f 'play -q /path/to/sound.mp3 pad 0 60 repeat -'"
"""
interval = 1
on_leftclick = ["start", 300]
on_rightclick = "reset"
on_upscroll = ["increase", 60]
on_downscroll = ["increase", -60]
settings = (
("format", "Default format that is showed if no ``format_custom`` "
"rules are matched."),
("format_stopped", "Format showed when timer is inactive."),
"color",
"color_stopped",
"format_custom",
("overflow_urgent", "Set urgent flag on overflow."),
"on_overflow",
"on_reset",
)
format = '-%h:%M:%S'
format_stopped = "T"
color = "#00ff00"
color_stopped = "#ffffff"
format_custom = [
(0, "+%M:%S", "#ffffff"),
(60, "-%M:%S", "#ffa500"),
(3600, "-%M:%S", "#00ff00"),
]
overflow_urgent = True
on_overflow = None
on_reset = None
def init(self):
self.compare = 0
self.state = TimerState.stopped
if not self.format_custom:
self.format_custom = []
def run(self):
if self.state is not TimerState.stopped:
diff = self.compare - time.time()
if diff < 0 and self.state is TimerState.running:
self.state = TimerState.overflow
if self.on_overflow:
if callable(self.on_overflow):
self.on_overflow()
else:
execute(self.on_overflow)
fmt = self.format
color = self.color
for rule in self.format_custom:
if diff < rule[0]:
fmt = rule[1]
color = rule[2]
break
urgent = self.overflow_urgent and self.state is TimerState.overflow
self.output = {
"full_text": format(TimeWrapper(abs(diff), fmt)),
"color": color,
"urgent": urgent,
}
else:
self.output = {
"full_text": self.format_stopped,
"color": self.color_stopped,
}
def start(self, seconds=300):
"""
Starts timer.
If timer is already running it will increase remaining time instead.
:param int seconds: Initial time.
"""
if self.state is TimerState.stopped:
self.compare = time.time() + abs(seconds)
self.state = TimerState.running
elif self.state is TimerState.running:
self.increase(seconds)
def increase(self, seconds):
"""
Change remainig time value.
:param int seconds: Seconds to add. Negative value substracts from
remaining time.
"""
if self.state is TimerState.running:
new_compare = self.compare + seconds
if new_compare > time.time():
self.compare = new_compare
def reset(self):
"""
Stop timer and execute ``on_reset`` if overflow occured.
"""
if self.state is not TimerState.stopped:
if self.on_reset and self.state is TimerState.overflow:
if callable(self.on_reset):
self.on_reset()
else:
execute(self.on_reset)
self.state = TimerState.stopped
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/timewarrior.py 0000664 0000000 0000000 00000004650 13567273623 0023426 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
from json import loads
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from datetime import datetime, timezone
import subprocess
class Timewarrior(IntervalModule):
"""
Show current Timewarrior tracking
Requires `json` `dateutil`
Formaters:
* `{tags}` — contains tags of current track
* `{start}` - contains start of track
* `{duration}` — contains time of current track
"""
format = '{duration}'
duration_format = '{years}y{months}m{days}d{hours}h{minutes}m{seconds}s'
enable_stop = True
enable_continue = True
color_running = '#00FF00'
color_stopped = '#F00000'
on_rightclick = 'stop_or_continue'
track = None
settings = (
('format', 'format string'),
('enable_stop', 'Allow right click to stop tracking'),
('enable_continue', 'ALlow right click to continue tracking'),
('color_running', '#00FF00'),
('color_stopped', '#F00000'),
)
def loadTrack(self):
try:
tracks_json = subprocess.check_output(['timew', 'export'])
tracks = loads(tracks_json.decode("utf-8"))
self.track = tracks[-1]
except ValueError as error:
self.logger.exception('Decoding JSON has failed')
raise error
def stop_or_continue(self):
self.loadTrack()
if 'end' in self.track and self.enable_continue:
subprocess.check_output(['timew', 'continue'])
elif self.enable_stop:
subprocess.check_output(['timew', 'stop'])
def run(self):
self.loadTrack()
start = parse(self.track['start'])
end = parse(self.track['end']) if 'end' in self.track else datetime.now(timezone.utc)
duration = relativedelta(end, start)
format_values = dict(
tags=", ".join(self.track['tags'] if 'tags' in self.track else []),
start=start,
duration=self.duration_format.format(
years=duration.years,
months=duration.months,
days=duration.days,
hours=duration.hours,
minutes=duration.minutes,
seconds=duration.seconds,
)
)
self.output = {
'full_text': self.format.format(**format_values),
'color': self.color_stopped if 'end' in self.track else self.color_running
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/tlp.py 0000664 0000000 0000000 00000003071 13567273623 0021655 0 ustar 00root root 0000000 0000000 from i3pystatus import IntervalModule
class Tlp(IntervalModule):
"""
Shows the current mode of TLP (Linux power management tool), either
battery, AC or unknown.
.. rubric:: Available formatters
* `{output}` - one of the strings configured through the `*_text` settings
"""
last_pwr_file = "/run/tlp/last_pwr"
bat_color = "#00FF00"
ac_color = "#FFAA00"
na_color = "#FF0000"
bat_text = "BAT"
ac_text = "AC"
na_text = "N/A"
settings = (
("last_pwr_file", "path to the TLP 'last pwr' file, default is `/run/tlp/last_pwr`"),
("bat_color", "color of text when TLP is in battery mode"),
("ac_color", "color of text when TLP is in AC mode"),
("na_color", "color of text when TLP is in unknown mode"),
("bat_text", "text to show when TLP is in battery mode"),
("ac_text", "text to show when TLP is in AC mode"),
("na_text", "text to show when TLP is in unknown mode"),
"format",
)
format = "{output}"
def run(self):
try:
with open(self.last_pwr_file) as f:
content = "".join(f.readlines()).strip()
except Exception as e:
content = None
if content == "0": # AC
text = self.ac_text
color = self.ac_color
elif content == "1":
text = self.bat_text
color = self.bat_color
else:
text = self.na_text
color = self.na_color
self.output = {
"full_text": text,
"color": color,
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/tools/ 0000775 0000000 0000000 00000000000 13567273623 0021643 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/tools/__init__.py 0000664 0000000 0000000 00000000000 13567273623 0023742 0 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/tools/setting_util.py 0000775 0000000 0000000 00000010436 13567273623 0024736 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import glob
import inspect
import os
import getpass
import sys
import signal
import pkgutil
from collections import defaultdict, OrderedDict
import keyring
import i3pystatus
from i3pystatus import Module, SettingsBase
from i3pystatus.core import ClassFinder
from i3pystatus.core.exceptions import ConfigInvalidModuleError
def signal_handler(signal, frame):
sys.exit(0)
def get_int_in_range(prompt, _range):
while True:
try:
answer = input(prompt)
except EOFError:
print()
sys.exit(0)
try:
n = int(answer.strip())
if n in _range:
return n
else:
print("Value out of range!")
except ValueError:
print("Invalid input!")
def enumerate_choices(choices):
lines = []
for index, choice in enumerate(choices, start=1):
lines.append(" %d - %s\n" % (index, choice))
return "".join(lines)
def get_modules():
for importer, modname, ispkg in pkgutil.iter_modules(i3pystatus.__path__):
if modname not in ["core", "tools"]:
yield modname
def get_credential_modules():
verbose = "-v" in sys.argv
protected_settings = SettingsBase._SettingsBase__PROTECTED_SETTINGS
class_finder = ClassFinder(Module)
credential_modules = defaultdict(dict)
for module_name in get_modules():
try:
module = class_finder.get_module(module_name)
clazz = class_finder.get_class(module)
except (ImportError, ConfigInvalidModuleError):
if verbose:
print("ImportError while importing", module_name)
continue
members = [m[0] for m in inspect.getmembers(clazz) if not m[0].startswith('_')]
if any([hasattr(clazz, setting) for setting in protected_settings]):
credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members))
credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__)
elif hasattr(clazz, 'required'):
protected = []
required = getattr(clazz, 'required')
for setting in protected_settings:
if setting in required:
protected.append(setting)
if protected:
credential_modules[clazz.__name__]['credentials'] = protected
credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__)
return credential_modules
def main():
signal.signal(signal.SIGINT, signal_handler)
print("""%s - part of i3pystatus
This allows you to edit keyring-protected settings of
i3pystatus modules, which are stored globally (independent
of your i3pystatus configuration) in your keyring.
Options:
-l: list all stored settings (no values are printed)
-v: print informational messages
""" % os.path.basename(sys.argv[0]))
credential_modules = get_credential_modules()
if "-l" in sys.argv:
for name, module in credential_modules.items():
print(name)
for credential in module['credentials']:
if keyring.get_password("%s.%s" % (module['key'], credential), getpass.getuser()):
print(" - %s: set" % credential)
else:
print(" - %s: unset" % credential)
return
choices = list(credential_modules.keys())
prompt = "Choose a module to edit:\n"
prompt += enumerate_choices(choices)
prompt += "> "
index = get_int_in_range(prompt, range(1, len(choices) + 1))
module_name = choices[index - 1]
module = credential_modules[module_name]
prompt = "Choose setting of %s to edit:\n" % module_name
prompt += enumerate_choices(module["credentials"])
prompt += "> "
choices = module['credentials']
index = get_int_in_range(prompt, range(1, len(choices) + 1))
setting = choices[index - 1]
answer = getpass.getpass("Enter value for %s:\n> " % setting)
answer2 = getpass.getpass("Re-enter value\n> ")
if answer == answer2:
key = "%s.%s" % (module['key'], setting)
keyring.set_password(key, getpass.getuser(), answer)
print("%s set!" % setting)
else:
print("Values don't match - nothing set.")
if __name__ == "__main__":
main()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/travisci.py 0000664 0000000 0000000 00000007344 13567273623 0022711 0 ustar 00root root 0000000 0000000 import os
import dateutil.parser
from travispy import TravisPy
from i3pystatus import IntervalModule
from i3pystatus.core.util import TimeWrapper, formatp, internet, require
__author__ = 'chestm007'
class TravisCI(IntervalModule):
"""
Get current status of travis builds
Requires `travispy` `dateutil.parser`
Formatters:
* `{repo_slug}` - repository owner/repository name
* `{repo_status}` - repository status
* `{repo_name}` - repository name
* `{repo_owner}` - repository owner
* `{last_build_finished}` - date of the last finished build
* `{last_build_duration}` - duration of the last build
Examples
.. code-block:: python
status_color_map = {
'passed': '#00FF00',
'failed': '#FF0000',
'errored': '#FFAA00',
'cancelled': '#EEEEEE',
'started': '#0000AA',
}
.. code-block:: python
repo_status_map={
'passed': 'passed',
'started': 'started',
'failed': 'failed',
}
"""
settings = (
'format',
('github_token', 'github personal access token'),
('repo_slug', 'repository identifier eg. "enkore/i3pystatus"'),
('time_format', 'passed directly to .strftime() for `last_build_finished`'),
('repo_status_map', 'map representing how to display status'),
('duration_format', '`last_build_duration` format string'),
('status_color_map', 'color for all text based on status'),
('color', 'color for all text not otherwise colored'))
required = ('github_token', 'repo_slug')
format = '{repo_owner}/{repo_name}-{repo_status} [({last_build_finished}({last_build_duration}))]'
short_format = '{repo_name}-{repo_status}'
time_format = '%m/%d'
duration_format = '%m:%S'
status_color_map = None
repo_status_map = None
color = '#DDDDDD'
travis = None
on_leftclick = 'open_build_webpage'
def init(self):
self.repo_status = None
self.last_build_duration = None
self.last_build_finished = None
self.repo_owner, self.repo_name = self.repo_slug.split('/')
def _format_time(self, time):
_datetime = dateutil.parser.parse(time)
return _datetime.strftime(self.time_format)
@require(internet)
def run(self):
if self.travis is None:
self.travis = TravisPy.github_auth(self.github_token)
repo = self.travis.repo(self.repo_slug)
self.repo_status = self.repo_status_map.get(repo.last_build_state, repo.last_build_state)
self.last_build_id = repo.last_build_id
if repo.last_build_state == 'started':
self.last_build_finished = None
self.last_build_duration = None
elif repo.last_build_state in ('failed', 'errored', 'cancelled', 'passed'):
self.last_build_finished = self._format_time(repo.last_build_finished_at)
self.last_build_duration = TimeWrapper(repo.last_build_duration, default_format=self.duration_format)
self.output = dict(
full_text=formatp(self.format, **vars(self)),
short_text=self.short_format.format(**vars(self)),
)
if self.status_color_map:
self.output['color'] = self.status_color_map.get(repo.last_build_state, self.color)
else:
self.output['color'] = self.color
def open_build_webpage(self):
os.popen('xdg-open https://travis-ci.org/{owner}/{repository_name}/builds/{build_id} > /dev/null'
.format(owner=self.repo_owner,
repository_name=self.repo_name,
build_id=self.last_build_id))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/uname.py 0000664 0000000 0000000 00000001650 13567273623 0022164 0 ustar 00root root 0000000 0000000 import os
from i3pystatus import Module
class Uname(Module):
"""
uname(1) like module.
.. rubric:: Available formatters
* `{sysname}` — operating system name
* `{nodename}` — name of machine on network (implementation-defined)
* `{release}` — operating system release
* `{version}` — operating system version
* `{machine}` — hardware identifier
"""
format = "{sysname} {release}"
settings = (
("format", "format string used for output"),
)
def init(self):
uname_result = os.uname()
fdict = {
"sysname": uname_result.sysname,
"nodename": uname_result.nodename,
"release": uname_result.release,
"version": uname_result.version,
"machine": uname_result.machine,
}
self.data = fdict
self.output = {
"full_text": self.format.format(**fdict),
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/ 0000775 0000000 0000000 00000000000 13567273623 0022150 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/__init__.py 0000664 0000000 0000000 00000012217 13567273623 0024264 0 ustar 00root root 0000000 0000000 import threading
from i3pystatus import SettingsBase, Module, formatp
from i3pystatus.core.util import internet, require
from i3pystatus.core.desktop import DesktopNotification
class Backend(SettingsBase):
settings = ()
updates = 0
class Updates(Module):
"""
Generic update checker.
To use select appropriate backend(s) for your system.
For list of all available backends see :ref:`updatebackends`.
Left clicking on the module will refresh the count of upgradeable packages.
This may be used to dismiss the notification after updating your system.
Right clicking shows a desktop notification with a summary count and a list
of available updates.
.. rubric:: Available formatters
* `{count}` — Sum of all available updates from all backends.
* For each backend registered there is one formatter named after the
backend, multiple identical backends do not accumulate, but overwrite
each other.
* For example, `{Cower}` (note capital C) is the number of updates
reported by the cower backend, assuming it has been registered.
.. rubric:: Usage example
::
from i3pystatus import Status
from i3pystatus.updates import pacman, cower
status = Status()
status.register("updates",
format = "Updates: {count}",
format_no_updates = "No updates",
backends = [pacman.Pacman(), cower.Cower()])
status.run()
"""
interval = 3600
settings = (
("backends", "Required list of backends used to check for updates."),
("format", "Format used when updates are available. "
"May contain formatters."),
("format_no_updates", "String that is shown if no updates are "
"available. If not set the module will be hidden if no updates "
"are available."),
("format_working", "Format used while update queries are run. By "
"default the same as ``format``."),
("format_summary", "Format for the summary line of notifications. By "
"default the same as ``format``."),
("notification_icon", "Icon shown when reporting the list of updates. "
"Default is ``software-update-available``, and can be "
"None for no icon."),
"color",
"color_no_updates",
"color_working",
("interval", "Default interval is set to one hour."),
)
required = ("backends",)
backends = None
format = "Updates: {count}"
format_no_updates = None
format_working = None
format_summary = None
notification_icon = "software-update-available"
color = "#00DD00"
color_no_updates = None
color_working = None
on_leftclick = "run"
on_rightclick = "report"
def init(self):
if not isinstance(self.backends, list):
self.backends = [self.backends]
if self.format_working is None: # we want to allow an empty format
self.format_working = self.format
if self.format_summary is None: # we want to allow an empty format
self.format_summary = self.format
self.color_working = self.color_working or self.color
self.data = {
"count": 0
}
self.notif_body = {}
self.condition = threading.Condition()
self.thread = threading.Thread(target=self.update_thread, daemon=True)
self.thread.start()
def update_thread(self):
self.check_updates()
while True:
with self.condition:
self.condition.wait(self.interval)
self.check_updates()
@require(internet)
def check_updates(self):
for backend in self.backends:
key = backend.__class__.__name__
if key not in self.data:
self.data[key] = "?"
if key not in self.notif_body:
self.notif_body[key] = ""
self.output = {
"full_text": formatp(self.format_working, **self.data).strip(),
"color": self.color_working,
}
updates_count = 0
for backend in self.backends:
name = backend.__class__.__name__
updates, notif_body = backend.updates
try:
updates_count += updates
except TypeError:
pass
self.data[name] = updates
self.notif_body[name] = notif_body or ""
if updates_count == 0:
self.output = {} if not self.format_no_updates else {
"full_text": self.format_no_updates,
"color": self.color_no_updates,
}
return
self.data["count"] = updates_count
self.output = {
"full_text": formatp(self.format, **self.data).strip(),
"color": self.color,
}
def run(self):
with self.condition:
self.condition.notify()
def report(self):
DesktopNotification(
title=formatp(self.format_summary, **self.data).strip(),
body="\n".join(self.notif_body.values()),
icon=self.notification_icon,
urgency=1,
timeout=0,
).display()
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/aptget.py 0000664 0000000 0000000 00000002040 13567273623 0024002 0 ustar 00root root 0000000 0000000 import os
from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class AptGet(Backend):
"""
Gets update count for Debian based distributions.
This mimics the Arch Linux `checkupdates` script
but with apt-get and written in python.
"""
@property
def updates(self):
cache_dir = "/tmp/update-cache-" + os.getenv("USER")
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
command = "apt-get update -o Dir::State::Lists=" + cache_dir
run_through_shell(command.split())
command = "apt-get upgrade -s -o Dir::State::Lists=" + cache_dir
apt = run_through_shell(command.split())
out = apt.out.splitlines(True)
out = "".join([line[5:] for line in out if line.startswith("Inst ")])
return out.count("\n"), out
Backend = AptGet
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/auracle.py 0000664 0000000 0000000 00000001225 13567273623 0024136 0 ustar 00root root 0000000 0000000 from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class Auracle(Backend):
"""
Checks for updates in Arch User Repositories using the `auracle` AUR helper.
Depends on auracle AUR agent - https://github.com/falconindy/auracle
"""
@property
def updates(self):
command = ["auracle", "sync"]
auracle = run_through_shell(command)
return auracle.out.count('\n'), auracle.out
Backend = Auracle
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/cower.py 0000664 0000000 0000000 00000001201 13567273623 0023633 0 ustar 00root root 0000000 0000000 from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class Cower(Backend):
"""
Checks for updates in Arch User Repositories using the `cower` AUR helper.
Depends on cower AUR agent - https://github.com/falconindy/cower
"""
@property
def updates(self):
command = ["cower", "-u"]
cower = run_through_shell(command)
return cower.out.count('\n'), cower.out
Backend = Cower
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/dnf.py 0000664 0000000 0000000 00000004737 13567273623 0023304 0 ustar 00root root 0000000 0000000 from i3pystatus.updates import Backend
import sys
# Remove first dir from sys.path to avoid shadowing dnf module from
# site-packages dir when this module executed directly on the CLI.
__module_dir = sys.path.pop(0)
try:
import dnf
HAS_DNF_BINDINGS = True
except ImportError:
HAS_DNF_BINDINGS = False
finally:
# Replace the directory we popped earlier
sys.path.insert(0, __module_dir)
class Dnf(Backend):
"""
Gets updates for RPM-based distributions using the `DNF API`_
The notification body consists of the package name and version for each
available update.
.. _`DNF API`: http://dnf.readthedocs.io/en/latest/api.html
.. note::
Users running i3pystatus from a virtualenv may see the updates display
as ``?`` due to an inability to import the ``dnf`` module. To ensure
that i3pystatus can access the DNF Python bindings, the virtualenv
should be created with ``--system-site-packages``.
If using `pyenv-virtualenv`_, the virtualenv must additionally be
created to use the system Python binary:
.. code-block:: bash
$ pyenv virtualenv --system-site-packages --python=/usr/bin/python3 pyenv_name
To invoke i3pystatus with this virtualenv, your ``bar`` section in
``~/.config/i3/config`` would look like this:
.. code-block:: bash
bar {
position top
status_command PYENV_VERSION=pyenv_name python /path/to/i3pystatus/script.py
}
.. _`pyenv-virtualenv`: https://github.com/yyuu/pyenv-virtualenv
"""
@property
def updates(self):
if HAS_DNF_BINDINGS:
try:
with dnf.Base() as base:
base.read_all_repos()
base.fill_sack()
upgrades = base.sack.query().upgrades().run()
notif_body = ''.join([
'%s: %s-%s\n' % (pkg.name, pkg.version, pkg.release)
for pkg in upgrades
])
return len(upgrades), notif_body
except Exception as exc:
self.logger.error('DNF update check failed', exc_info=True)
return '?', exc.__str__()
else:
return '?', 'Failed to import DNF Python bindings'
Backend = Dnf
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/packagekit.py 0000664 0000000 0000000 00000001544 13567273623 0024631 0 ustar 00root root 0000000 0000000 import os
from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class PackageKit(Backend):
"""
Gets update count for distributions using PackageKit.
At the moment, it works with english localization, only.
"""
@property
def updates(self):
command = "pkcon get-updates -p"
pk = run_through_shell(command.split())
out = pk.out.splitlines(True)
resultStrings = ("Security", "Bug fix", "Enhancement")
out = "".join([line for line in out[out.index("Results:\n") + 1:]
if line.startswith(resultStrings)])
return out.count("\n"), out
Backend = PackageKit
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/pacman.py 0000664 0000000 0000000 00000001211 13567273623 0023754 0 ustar 00root root 0000000 0000000 from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class Pacman(Backend):
"""
Checks for updates in Arch Linux repositories using the
`checkupdates` script which is part of the `pacman-contrib` package.
"""
@property
def updates(self):
command = ["checkupdates"]
checkupdates = run_through_shell(command)
return checkupdates.out.count("\n"), checkupdates.out
Backend = Pacman
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/yaourt.py 0000664 0000000 0000000 00000002420 13567273623 0024043 0 ustar 00root root 0000000 0000000 from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class Yaourt(Backend):
"""
This module counts the available updates using yaourt.
By default it will only count aur packages. Thus it can be used with the
pacman backend like this:
.. code-block:: python
from i3pystatus.updates import pacman, yaourt
status.register("updates", backends = \
[pacman.Pacman(), yaourt.Yaourt()])
To count both pacman and aur packages, pass False in the constructor:
.. code-block:: python
from i3pystatus.updates import yaourt
status.register("updates", backends = [yaourt.Yaourt(False)])
"""
def __init__(self, aur_only=True):
self.aur_only = aur_only
@property
def updates(self):
command = ["yaourt", "-Qua"]
checkupdates = run_through_shell(command)
out = checkupdates.out
if(self.aur_only):
out = "".join([line for line in out.splitlines(True)
if line.startswith("aur")])
return out.count("\n"), out
Backend = Yaourt
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/yay.py 0000664 0000000 0000000 00000002261 13567273623 0023325 0 ustar 00root root 0000000 0000000 from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class Yay(Backend):
"""
This module counts the available updates using yay.
By default it will only count aur packages. Thus it can be used with the
pacman backend like this:
.. code-block:: python
from i3pystatus.updates import pacman, yay
status.register("updates", backends = \
[pacman.Pacman(), yay.Yay()])
To count both pacman and aur packages, pass False in the constructor:
.. code-block:: python
from i3pystatus.updates import yay
status.register("updates", backends = [yay.Yay(False)])
"""
def __init__(self, aur_only=True):
self.aur_only = aur_only
@property
def updates(self):
if(self.aur_only):
command = ["yay", "-Qua"]
else:
command = ["yay", "-Qu"]
checkupdates = run_through_shell(command)
out = checkupdates.out
return out.count("\n"), out
Backend = Yay
if __name__ == "__main__":
"""
Call this module directly; Print the update count and notification body.
"""
print("Updates: {}\n\n{}".format(*Backend().updates))
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/uptime.py 0000664 0000000 0000000 00000003624 13567273623 0022365 0 ustar 00root root 0000000 0000000
from i3pystatus import IntervalModule, formatp
class Uptime(IntervalModule):
"""
Outputs Uptime
.. rubric:: Available formatters
* `{days}` - uptime in days
* `{hours}` - rest of uptime in hours
* `{mins}` - rest of uptime in minutes
* `{secs}` - rest of uptime in seconds
* `{uptime}` - deprecated: equals '`{hours}:{mins}`'
"""
settings = (
("format", "Format string"),
("color", "String color"),
("alert", "If you want the string to change color"),
("seconds_alert", "How many seconds necessary to start the alert"),
("color_alert", "Alert color"),
)
file = "/proc/uptime"
format = "up {hours}:{mins}"
color = "#ffffff"
alert = False
seconds_alert = 60 * 60 * 24 * 30 # 30 days
color_alert = "#ff0000"
def run(self):
with open(self.file, "r") as f:
seconds = int(float(f.read().split()[0]))
raw_seconds = seconds
days = seconds // (60 * 60 * 24)
hours = seconds // (60 * 60)
minutes = seconds // 60
if "{days}" in self.format:
hours = (seconds % (60 * 60 * 24)) // (60 * 60)
minutes = (seconds % (60 * 60 * 24)) // 60
seconds = (seconds % (60 * 60 * 24))
if "{hours}" in self.format:
minutes = (seconds % (60 * 60)) // 60
seconds = (seconds % (60 * 60))
if "{mins}" in self.format:
seconds = seconds % 60
fdict = {
"days": days,
"hours": hours,
"mins": minutes,
"secs": seconds,
"uptime": "{}:{}".format(hours, minutes),
}
self.data = fdict
if self.alert:
if raw_seconds > self.seconds_alert:
self.color = self.color_alert
self.output = {
"full_text": formatp(self.format, **fdict),
"color": self.color
}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/utils/ 0000775 0000000 0000000 00000000000 13567273623 0021643 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/utils/__init__.py 0000664 0000000 0000000 00000000000 13567273623 0023742 0 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/utils/gpu.py 0000664 0000000 0000000 00000003434 13567273623 0023014 0 ustar 00root root 0000000 0000000 import subprocess
from collections import namedtuple
GPUUsageInfo = namedtuple('GPUUsageInfo', ['total_mem', 'avail_mem', 'used_mem',
'temp', 'percent_fan',
'usage_gpu', 'usage_mem'])
def query_nvidia_smi(gpu_number) -> GPUUsageInfo:
"""
:return:
all memory fields are in megabytes,
temperature in degrees celsius,
fan speed is integer percent from 0 to 100 inclusive,
usage_gpu and usage_mem are integer percents from 0 to 100 inclusive
(usage_mem != used_mem, usage_mem is about read/write access load)
read more in 'nvidia-smi --help-query-gpu'.
Any field can be None if such information is not supported by nvidia-smi for current GPU
Returns None if call failed (no nvidia-smi or query format was changed)
Raises exception with readable comment
"""
params = ["memory.total", "memory.free", "memory.used",
"temperature.gpu", "fan.speed",
"utilization.gpu", "utilization.memory"]
try:
output = subprocess.check_output(["nvidia-smi",
"--query-gpu={}".format(','.join(params)),
"--format=csv,noheader,nounits"])
except FileNotFoundError:
raise Exception("No nvidia-smi")
except subprocess.CalledProcessError:
raise Exception("nvidia-smi call failed")
output = output.decode('utf-8').split("\n")[gpu_number].strip()
values = output.split(", ")
# If value contains 'not' - it is not supported for this GPU (in fact, for now nvidia-smi returns '[Not Supported]')
values = [None if ("not" in value.lower()) else int(value) for value in values]
return GPUUsageInfo(*values)
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/vk.py 0000664 0000000 0000000 00000005217 13567273623 0021502 0 ustar 00root root 0000000 0000000 from i3pystatus import Status, IntervalModule
from i3pystatus.core.util import internet, require, user_open
import vk
class Vk(IntervalModule):
"""
Display amount of unread messages in VK social network.
Creating your own VK API app is highly recommended for your own privacy, though there is a default one provided.
Reference vk.com/dev for instructions on creating VK API app.
If access_token is not specified, the module will try to open a request page in browser.
You will need to manually copy obtained acess token to your config file.
Requires the PyPI package `vk`.
"""
API_LINK = "https://oauth.vk.com/authorize?client_id={id}&display=page&revoke=1&scope=messages,offline&response_type=token&v=5.40"
app_id = 5160484
access_token = None
session = None
token_error = "Vk: token error"
format = '{unread}/{total}'
interval = 1
color = "#ffffff"
color_unread = "#ffffff"
color_bad = "#ff0000"
settings = (
("app_id", "Id of your VK API app"),
("access_token", "Your access token. You must have `messages` and `offline` access permissions"),
("token_error", "Message to be shown if there's some problem with your token"),
("color", "General color of the output"),
("color_bad", "Color of the output in case of access token error"),
("color_unread", "Color of the output if there are unread messages"),
)
@require(internet)
def token_request(self, func):
user_open(self.API_LINK.format(id=self.app_id))
self.run = func
@require(internet)
def init(self):
if self.access_token:
self.session = vk.AuthSession(app_id=self.app_id, access_token=self.access_token)
self.api = vk.API(self.session, v='5.40', lang='en', timeout=10)
try:
permissions = int(self.api.account.getAppPermissions())
assert((permissions & 65536 == 65536) and (permissions & 4096 == 4096))
except:
self.token_request(self.error)
else:
self.token_request(lambda: None)
@require(internet)
def run(self):
total = self.api.messages.getDialogs()['count']
unread = self.api.messages.getDialogs(unread=1)['count']
if unread > 0:
color = self.color_unread
else:
color = self.color
self.output = {
"full_text": self.format.format(
total=total,
unread=unread
),
"color": color
}
def error(self):
self.output = {"full_text": self.token_error,
"color": self.color_bad}
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/weather/ 0000775 0000000 0000000 00000000000 13567273623 0022142 5 ustar 00root root 0000000 0000000 i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/weather/__init__.py 0000664 0000000 0000000 00000030676 13567273623 0024267 0 ustar 00root root 0000000 0000000 import json
import re
import threading
import time
from urllib.request import urlopen
from i3pystatus import SettingsBase, IntervalModule, formatp
from i3pystatus.core.util import user_open, internet, require
class WeatherBackend(SettingsBase):
settings = ()
@require(internet)
def api_request(self, url):
self.logger.debug('Making API request to %s', url)
try:
with urlopen(url) as content:
try:
content_type = dict(content.getheaders())['Content-Type']
charset = re.search(r'charset=(.*)', content_type).group(1)
except AttributeError:
charset = 'utf-8'
response_json = content.read().decode(charset).strip()
if not response_json:
self.logger.debug('JSON response from %s was blank', url)
return {}
try:
response = json.loads(response_json)
except json.decoder.JSONDecodeError as exc:
self.logger.error('Error loading JSON: %s', exc)
self.logger.debug('JSON text that failed to load: %s',
response_json)
return {}
self.logger.log(5, 'API response: %s', response)
error = self.check_response(response)
if error:
self.logger.error('Error in JSON response: %s', error)
return {}
return response
except Exception as exc:
self.logger.error(
'Failed to make API request to %s. Exception follows:', url,
exc_info=True
)
return {}
def check_response(response):
raise NotImplementedError
class Weather(IntervalModule):
'''
This is a generic weather-checker which must use a configured weather
backend. For list of all available backends see :ref:`weatherbackends`.
Double-clicking on the module will launch the forecast page for the
location being checked, and single-clicking will trigger an update.
.. _weather-formatters:
.. rubric:: Available formatters
* `{city}` — Location of weather observation
* `{condition}` — Current weather condition (Rain, Snow, Overcast, etc.)
* `{icon}` — Icon representing the current weather condition
* `{observation_time}` — Time of weather observation (supports strftime format flags)
* `{current_temp}` — Current temperature, excluding unit
* `{low_temp}` — Forecasted low temperature, excluding unit
* `{high_temp}` — Forecasted high temperature, excluding unit (may be
empty in the late afternoon)
* `{temp_unit}` — Either ``°C`` or ``°F``, depending on whether metric or
* `{feelslike}` — "Feels Like" temperature, excluding unit
* `{dewpoint}` — Dewpoint temperature, excluding unit
imperial units are being used
* `{wind_speed}` — Wind speed, excluding unit
* `{wind_unit}` — Either ``kph`` or ``mph``, depending on whether metric or
imperial units are being used
* `{wind_direction}` — Wind direction
* `{wind_gust}` — Speed of wind gusts in mph/kph, excluding unit
* `{pressure}` — Barometric pressure, excluding unit
* `{pressure_unit}` — ``mb`` or ``in``, depending on whether metric or
imperial units are being used
* `{pressure_trend}` — ``+`` if rising, ``-`` if falling, or an empty
string if the pressure is steady (neither rising nor falling)
* `{visibility}` — Visibility distance, excluding unit
* `{visibility_unit}` — Either ``km`` or ``mi``, depending on whether
metric or imperial units are being used
* `{humidity}` — Current humidity, excluding percentage symbol
* `{uv_index}` — UV Index
* `{update_error}` — When the configured weather backend encounters an
error during an update, this formatter will be set to the value of the
backend's **update_error** config value. Otherwise, this formatter will
be an empty string.
This module supports the :ref:`formatp ` extended string format
syntax. This allows for values to be hidden when they evaluate as False.
The default **format** string value for this module makes use of this
syntax to conditionally show the value of the **update_error** config value
when the backend encounters an error during an update.
The extended string format syntax also comes in handy for the
:py:mod:`weathercom <.weather.weathercom>` backend, which at a certain
point in the afternoon will have a blank ``{high_temp}`` value. Using the
following snippet in your format string will only display the high
temperature information if it is not blank:
::
{current_temp}{temp_unit}[ Hi: {high_temp}] Lo: {low_temp}[ {update_error}]
Brackets are evaluated from the outside-in, so the fact that the only
formatter in the outer block (``{high_temp}``) is empty would keep the
inner block from being evaluated at all, and entire block would not be
displayed.
See the following links for usage examples for the available weather
backends:
- :ref:`Weather.com `
- :ref:`Weather Underground `
.. rubric:: Troubleshooting
If an error is encountered while updating, the ``{update_error}`` formatter
will be set, and (provided it is in your ``format`` string) will show up
next to the forecast to alert you to the error. The error message will (by
default be logged to ``~/.i3pystatus-`` where ```` is the PID of
the update thread. However, it may be more convenient to manually set the
logfile to make the location of the log data predictable and avoid clutter
in your home directory. Additionally, using the ``DEBUG`` log level can
be helpful in revealing why the module is not working as expected. For
example:
.. code-block:: python
import logging
from i3pystatus import Status
from i3pystatus.weather import weathercom
status = Status(logfile='/home/username/var/i3pystatus.log')
status.register(
'weather',
format='{condition} {current_temp}{temp_unit}[ {icon}][ Hi: {high_temp}][ Lo: {low_temp}][ {update_error}]',
colorize=True,
hints={'markup': 'pango'},
update_error='!',
log_level=logging.DEBUG,
backend=weathercom.Weathercom(
location_code='94107:4:US',
units='imperial',
log_level=logging.DEBUG,
),
)
.. note::
The log level must be set separately in both the module and backend
contexts.
'''
settings = (
('colorize', 'Vary the color depending on the current conditions.'),
('color_icons', 'Dictionary mapping weather conditions to tuples '
'containing a UTF-8 code for the icon, and the color '
'to be used.'),
('color', 'Display color (or fallback color if ``colorize`` is True). '
'If not specified, falls back to default i3bar color.'),
('backend', 'Weather backend instance'),
('refresh_icon', 'Text to display (in addition to any text currently '
'shown by the module) when refreshing weather data. '
'**NOTE:** Depending on how quickly the update is '
'performed, the icon may not be displayed.'),
('online_interval', 'seconds between updates when online (defaults to interval)'),
('offline_interval', 'seconds between updates when offline (default: 300)'),
'format',
)
required = ('backend',)
colorize = False
color_icons = {
'Fair': (u'\u263c', '#ffcc00'),
'Fog': (u'', '#949494'),
'Cloudy': (u'\u2601', '#f8f8ff'),
'Partly Cloudy': (u'\u2601', '#f8f8ff'), # \u26c5 is not in many fonts
'Rainy': (u'\u26c8', '#cbd2c0'),
'Thunderstorm': (u'\u26a1', '#cbd2c0'),
'Sunny': (u'\u2600', '#ffff00'),
'Snow': (u'\u2603', '#ffffff'),
'default': ('', None),
}
color = None
backend = None
interval = 1800
offline_interval = 300
online_interval = None
refresh_icon = '⟳'
format = '{current_temp}{temp_unit}[ {update_error}]'
output = {'full_text': ''}
on_doubleleftclick = ['launch_web']
on_leftclick = ['check_weather']
def launch_web(self):
if self.backend.forecast_url and self.backend.forecast_url != 'N/A':
self.logger.debug('Launching %s in browser', self.backend.forecast_url)
user_open(self.backend.forecast_url)
def init(self):
if self.online_interval is None:
self.online_interval = int(self.interval)
if self.backend is None:
raise RuntimeError('A backend is required')
self.backend.data = {
'city': '',
'condition': '',
'observation_time': '',
'current_temp': '',
'low_temp': '',
'high_temp': '',
'temp_unit': '',
'feelslike': '',
'dewpoint': '',
'wind_speed': '',
'wind_unit': '',
'wind_direction': '',
'wind_gust': '',
'pressure': '',
'pressure_unit': '',
'pressure_trend': '',
'visibility': '',
'visibility_unit': '',
'humidity': '',
'uv_index': '',
'update_error': '',
}
self.backend.init()
self.condition = threading.Condition()
self.thread = threading.Thread(target=self.update_thread, daemon=True)
self.thread.start()
def update_thread(self):
if internet():
self.interval = self.online_interval
else:
self.interval = self.offline_interval
try:
self.check_weather()
while True:
with self.condition:
self.condition.wait(self.interval)
self.check_weather()
except Exception:
msg = 'Exception in {thread} at {time}, module {name}'.format(
thread=threading.current_thread().name,
time=time.strftime('%c'),
name=self.__class__.__name__,
)
self.logger.error(msg, exc_info=True)
def check_weather(self):
'''
Check the weather using the configured backend
'''
self.output['full_text'] = \
self.refresh_icon + self.output.get('full_text', '')
self.backend.check_weather()
self.refresh_display()
def get_color_data(self, condition):
'''
Disambiguate similarly-named weather conditions, and return the icon
and color that match.
'''
if condition not in self.color_icons:
# Check for similarly-named conditions if no exact match found
condition_lc = condition.lower()
if 'cloudy' in condition_lc or 'clouds' in condition_lc:
if 'partly' in condition_lc:
condition = 'Partly Cloudy'
else:
condition = 'Cloudy'
elif condition_lc == 'overcast':
condition = 'Cloudy'
elif 'thunder' in condition_lc or 't-storm' in condition_lc:
condition = 'Thunderstorm'
elif 'snow' in condition_lc:
condition = 'Snow'
elif 'rain' in condition_lc or 'showers' in condition_lc:
condition = 'Rainy'
elif 'sunny' in condition_lc:
condition = 'Sunny'
elif 'clear' in condition_lc or 'fair' in condition_lc:
condition = 'Fair'
elif 'fog' in condition_lc:
condition = 'Fog'
return self.color_icons['default'] \
if condition not in self.color_icons \
else self.color_icons[condition]
def refresh_display(self):
self.logger.debug('Weather data: %s', self.backend.data)
self.backend.data['icon'], condition_color = \
self.get_color_data(self.backend.data['condition'])
color = condition_color if self.colorize else self.color
self.output = {
'full_text': formatp(self.format, **self.backend.data).strip(),
'color': color,
}
def run(self):
pass
i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/weather/weathercom.py 0000664 0000000 0000000 00000034004 13567273623 0024653 0 ustar 00root root 0000000 0000000 import json
import re
from datetime import datetime
from html.parser import HTMLParser
from urllib.request import Request, urlopen
from i3pystatus.core.util import internet, require
from i3pystatus.weather import WeatherBackend
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0'
class WeathercomHTMLParser(HTMLParser):
'''
Obtain data points required by the Weather.com API which are obtained
through some other source at runtime and added as