pax_global_header00006660000000000000000000000064135672736230014530gustar00rootroot0000000000000052 comment=ee53f0d41142062981e3547859643214414428a7 i3pystatus-3.35+git20191126.5a8eaf4/000077500000000000000000000000001356727362300163535ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/.gitignore000066400000000000000000000002211356727362300203360ustar00rootroot00000000000000*__pycache__* *.pyc i3pystatus/__main__.py build/* dist/* *.egg-info/* *~ .i3pystatus-* ci-build docs/_build *.swp *.ropeproject \.idea/ venv/ i3pystatus-3.35+git20191126.5a8eaf4/.travis.yml000066400000000000000000000001721356727362300204640ustar00rootroot00000000000000language: python sudo: false python: - "3.4" install: - "pip install -r dev-requirements.txt" script: "./ci-build.sh" i3pystatus-3.35+git20191126.5a8eaf4/CONTRIBUTORS000066400000000000000000000016241356727362300202360ustar00rootroot00000000000000aaron-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-LICENSE000066400000000000000000000021511356727362300200060ustar00rootroot00000000000000Copyright (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.rst000066400000000000000000000035201356727362300200420ustar00rootroot00000000000000i3pystatus ========== .. 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.sh000077500000000000000000000014411356727362300204020ustar00rootroot00000000000000#!/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.txt000066400000000000000000000001251356727362300224110ustar00rootroot00000000000000pytest>=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/000077500000000000000000000000001356727362300173035ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/docs/Makefile000066400000000000000000000151721356727362300207510ustar00rootroot00000000000000# 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/000077500000000000000000000000001356727362300207315ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/docs/_static/i3pystatus.css000066400000000000000000000005551356727362300236000ustar00rootroot00000000000000 #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.rst000066400000000000000000000320501356727362300217640ustar00rootroot00000000000000 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.py000066400000000000000000000246001356727362300206040ustar00rootroot00000000000000#!/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.rst000066400000000000000000000527661356727362300227240ustar00rootroot00000000000000Configuration ============= 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.rst000066400000000000000000000030511356727362300231130ustar00rootroot00000000000000core 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.rst000066400000000000000000000056771356727362300222040ustar00rootroot00000000000000Module 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.rst000066400000000000000000000004441356727362300211460ustar00rootroot00000000000000 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.bat000066400000000000000000000150651356727362300207170ustar00rootroot00000000000000@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.rst000066400000000000000000000073501356727362300213270ustar00rootroot00000000000000 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.py000066400000000000000000000142461356727362300221610ustar00rootroot00000000000000 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 + ")" + "
", "", ] else: lines[0:0] = [ ".. raw:: html", "", "
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/000077500000000000000000000000001356727362300205035ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/__init__.py000066400000000000000000000023701356727362300226160ustar00rootroot00000000000000from 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.py000066400000000000000000000244471356727362300227730ustar00rootroot00000000000000import 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.py000066400000000000000000000120731356727362300220000ustar00rootroot00000000000000from 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.py000066400000000000000000000046641356727362300223440ustar00rootroot00000000000000import 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.py000066400000000000000000000034751356727362300223420ustar00rootroot00000000000000import 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.py000066400000000000000000000060551356727362300230130ustar00rootroot00000000000000from 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.py000066400000000000000000000356021356727362300225350ustar00rootroot00000000000000import 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.py000066400000000000000000000143211356727362300225050ustar00rootroot00000000000000import 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/000077500000000000000000000000001356727362300222545ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/calendar/__init__.py000066400000000000000000000201531356727362300243660ustar00rootroot00000000000000import 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.py000066400000000000000000000113221356727362300241010ustar00rootroot00000000000000import 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.py000066400000000000000000000035371356727362300254060ustar00rootroot00000000000000from 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.py000066400000000000000000000060471356727362300246200ustar00rootroot00000000000000import 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.py000066400000000000000000000125051356727362300226350ustar00rootroot00000000000000import 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.py000066400000000000000000000116761356727362300221630ustar00rootroot00000000000000import 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.py000066400000000000000000000100751356727362300220270ustar00rootroot00000000000000import 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.py000066400000000000000000000055201356727362300220070ustar00rootroot00000000000000import 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/000077500000000000000000000000001356727362300214335ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/core/__init__.py000066400000000000000000000125211356727362300235450ustar00rootroot00000000000000import 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.py000066400000000000000000000035071356727362300231300ustar00rootroot00000000000000from 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.py000066400000000000000000000056611356727362300234330ustar00rootroot00000000000000import 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.py000066400000000000000000000063321356727362300234620ustar00rootroot00000000000000import 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.py000066400000000000000000000015541356727362300241730ustar00rootroot00000000000000class 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.py000066400000000000000000000036641356727362300235010ustar00rootroot00000000000000import 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.py000066400000000000000000000142741356727362300224240ustar00rootroot00000000000000import 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.py000066400000000000000000000264061356727362300234650ustar00rootroot00000000000000import 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 `&amp;`). """ 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.py000066400000000000000000000134071356727362300236520ustar00rootroot00000000000000from 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.py000066400000000000000000000122321356727362300237520ustar00rootroot00000000000000import 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.py000066400000000000000000000520101356727362300227600ustar00rootroot00000000000000import 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.py000066400000000000000000000055361356727362300226720ustar00rootroot00000000000000from 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.py000066400000000000000000000107441356727362300230360ustar00rootroot00000000000000from 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.py000066400000000000000000000041661356727362300236630ustar00rootroot00000000000000from 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.py000066400000000000000000000046451356727362300242220ustar00rootroot00000000000000from 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.py000066400000000000000000000101441356727362300223220ustar00rootroot00000000000000import 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.py000066400000000000000000000054471356727362300220210ustar00rootroot00000000000000import 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.py000066400000000000000000000100441356727362300227660ustar00rootroot00000000000000from 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.py000066400000000000000000000024051356727362300220210ustar00rootroot00000000000000from 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.py000066400000000000000000000055141356727362300220320ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000050471356727362300233750ustar00rootroot00000000000000from 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.py000066400000000000000000000025621356727362300220010ustar00rootroot00000000000000from 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.py000066400000000000000000000630031356727362300223410ustar00rootroot00000000000000import 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.py000066400000000000000000000041421356727362300225070ustar00rootroot00000000000000from 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.py000066400000000000000000000021761356727362300227030ustar00rootroot00000000000000from 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.py000066400000000000000000000030621356727362300230350ustar00rootroot00000000000000from 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.py000066400000000000000000000041131356727362300222100ustar00rootroot00000000000000from 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.py000066400000000000000000000065271356727362300221770ustar00rootroot00000000000000import 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.py000066400000000000000000000034201356727362300240470ustar00rootroot00000000000000import 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.py000066400000000000000000000034161356727362300223470ustar00rootroot00000000000000from 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.py000066400000000000000000000024611356727362300217770ustar00rootroot00000000000000from 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/000077500000000000000000000000001356727362300214255ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/mail/__init__.py000066400000000000000000000060651356727362300235450ustar00rootroot00000000000000from 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.py000066400000000000000000000045271356727362300226050ustar00rootroot00000000000000import 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.py000066400000000000000000000052771356727362300227400ustar00rootroot00000000000000from 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.py000066400000000000000000000005651356727362300234260ustar00rootroot00000000000000import 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.py000066400000000000000000000011121356727362300227370ustar00rootroot00000000000000import 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.py000066400000000000000000000024431356727362300243220ustar00rootroot00000000000000# 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.py000066400000000000000000000031251356727362300243120ustar00rootroot00000000000000# 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.py000066400000000000000000000023321356727362300230210ustar00rootroot00000000000000from 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.py000066400000000000000000000035121356727362300216340ustar00rootroot00000000000000from 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.py000066400000000000000000000033541356727362300224640ustar00rootroot00000000000000from 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.py000066400000000000000000000060651356727362300216420ustar00rootroot00000000000000import 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.py000066400000000000000000000055741356727362300223430ustar00rootroot00000000000000#!/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.py000066400000000000000000000061641356727362300220340ustar00rootroot00000000000000from 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.py000066400000000000000000000174001356727362300216370ustar00rootroot00000000000000from 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.py000066400000000000000000000066061356727362300230330ustar00rootroot00000000000000from 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.py000066400000000000000000000504531356727362300225550ustar00rootroot00000000000000from 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.py000066400000000000000000000204711356727362300234070ustar00rootroot00000000000000from 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.py000066400000000000000000000015631356727362300223460ustar00rootroot00000000000000from 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.py000066400000000000000000000013751356727362300230470ustar00rootroot00000000000000from 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.py000066400000000000000000000043461356727362300237400ustar00rootroot00000000000000from 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.py000066400000000000000000000063521356727362300225500ustar00rootroot00000000000000from 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.py000066400000000000000000000044551356727362300230710ustar00rootroot00000000000000from 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.py000066400000000000000000000133051356727362300223250ustar00rootroot00000000000000from 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.py000066400000000000000000000030011356727362300226420ustar00rootroot00000000000000from 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.py000066400000000000000000000051331356727362300220140ustar00rootroot00000000000000import 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.py000066400000000000000000000057721356727362300233040ustar00rootroot00000000000000import 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.py000066400000000000000000000101531356727362300227130ustar00rootroot00000000000000import 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/000077500000000000000000000000001356727362300226555ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/pulseaudio/__init__.py000066400000000000000000000233221356727362300247700ustar00rootroot00000000000000import 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.py000066400000000000000000000243311356727362300243620ustar00rootroot00000000000000# 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.py000066400000000000000000000061371356727362300223540ustar00rootroot00000000000000import 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.py000066400000000000000000000056361356727362300242710ustar00rootroot00000000000000from 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.py000066400000000000000000000145171356727362300223400ustar00rootroot00000000000000#!/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.py000066400000000000000000000127441356727362300226750ustar00rootroot00000000000000import 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.py000066400000000000000000000013571356727362300221750ustar00rootroot00000000000000import 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.py000066400000000000000000000025711356727362300227150ustar00rootroot00000000000000import 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.py000066400000000000000000000064341356727362300225070ustar00rootroot00000000000000from 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/000077500000000000000000000000001356727362300220015ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/scores/__init__.py000066400000000000000000000647161356727362300241300ustar00rootroot00000000000000import 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.py000066400000000000000000000401061356727362300231340ustar00rootroot00000000000000from 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.py000066400000000000000000000342271356727362300231350ustar00rootroot00000000000000from 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.py000066400000000000000000000340221356727362300231140ustar00rootroot00000000000000from 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.py000066400000000000000000000363571356727362300231520ustar00rootroot00000000000000from 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.py000066400000000000000000000036021356727362300231720ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000070351356727362300222170ustar00rootroot00000000000000from 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.py000066400000000000000000000027771356727362300216500ustar00rootroot00000000000000import 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.py000066400000000000000000000024611356727362300221670ustar00rootroot00000000000000from 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.py000066400000000000000000000043521356727362300223420ustar00rootroot00000000000000from 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.py000066400000000000000000000114561356727362300222250ustar00rootroot00000000000000from 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.py000066400000000000000000000024671356727362300226530ustar00rootroot00000000000000import 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.py000066400000000000000000000003021356727362300225450ustar00rootroot00000000000000from 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.py000066400000000000000000000050321356727362300220270ustar00rootroot00000000000000from 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.py000066400000000000000000000103021356727362300230570ustar00rootroot00000000000000import 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.py000066400000000000000000000072471356727362300234370ustar00rootroot00000000000000from 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.py000066400000000000000000000223501356727362300220240ustar00rootroot00000000000000from 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.py000066400000000000000000000006061356727362300220430ustar00rootroot00000000000000from 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.py000066400000000000000000000133541356727362300222030ustar00rootroot00000000000000import 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.py000066400000000000000000000046501356727362300234260ustar00rootroot00000000000000from 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.py000066400000000000000000000030711356727362300216550ustar00rootroot00000000000000from 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/000077500000000000000000000000001356727362300216435ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/tools/__init__.py000066400000000000000000000000001356727362300237420ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/tools/setting_util.py000077500000000000000000000104361356727362300247360ustar00rootroot00000000000000#!/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.py000066400000000000000000000073441356727362300227110ustar00rootroot00000000000000import 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.py000066400000000000000000000016501356727362300221640ustar00rootroot00000000000000import 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/000077500000000000000000000000001356727362300221505ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/updates/__init__.py000066400000000000000000000122171356727362300242640ustar00rootroot00000000000000import 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.py000066400000000000000000000020401356727362300240020ustar00rootroot00000000000000import 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.py000066400000000000000000000012251356727362300241360ustar00rootroot00000000000000from 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.py000066400000000000000000000012011356727362300236330ustar00rootroot00000000000000from 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.py000066400000000000000000000047371356727362300233040ustar00rootroot00000000000000from 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.py000066400000000000000000000015441356727362300246310ustar00rootroot00000000000000import 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.py000066400000000000000000000012111356727362300237540ustar00rootroot00000000000000from 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.py000066400000000000000000000024201356727362300240430ustar00rootroot00000000000000from 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.py000066400000000000000000000022611356727362300233250ustar00rootroot00000000000000from 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.py000066400000000000000000000036241356727362300223650ustar00rootroot00000000000000 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/000077500000000000000000000000001356727362300216435ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/utils/__init__.py000066400000000000000000000000001356727362300237420ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/utils/gpu.py000066400000000000000000000034341356727362300230140ustar00rootroot00000000000000import 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.py000066400000000000000000000052171356727362300215020ustar00rootroot00000000000000from 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/000077500000000000000000000000001356727362300221425ustar00rootroot00000000000000i3pystatus-3.35+git20191126.5a8eaf4/i3pystatus/weather/__init__.py000066400000000000000000000306761356727362300242670ustar00rootroot00000000000000import 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.py000066400000000000000000000340041356727362300246530ustar00rootroot00000000000000import 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