diff options
| -rw-r--r-- | CHANGELOG | 15 | ||||
| -rw-r--r-- | MANIFEST.in | 4 | ||||
| -rw-r--r-- | README.rst | 63 | ||||
| -rw-r--r-- | modules/__init__.py | 0 | ||||
| -rw-r--r-- | modules/battery-level.py | 68 | ||||
| -rw-r--r-- | modules/dpms.py (renamed from examples/dpms.py) | 0 | ||||
| -rw-r--r-- | modules/empty_class.py (renamed from examples/empty_class.py) | 0 | ||||
| -rw-r--r-- | modules/glpi.py (renamed from examples/glpi.py) | 0 | ||||
| -rw-r--r-- | modules/i3bar_click_events.py (renamed from examples/i3bar_click_events.py) | 0 | ||||
| -rw-r--r-- | modules/keyboard-layout.py | 73 | ||||
| -rw-r--r-- | modules/mpd_status.py | 117 | ||||
| -rw-r--r-- | modules/netdata.py (renamed from examples/netdata.py) | 2 | ||||
| -rw-r--r-- | modules/ns_checker.py (renamed from examples/ns_checker.py) | 0 | ||||
| -rw-r--r-- | modules/pingdom.py (renamed from examples/pingdom.py) | 0 | ||||
| -rw-r--r-- | modules/pomodoro.py (renamed from examples/pomodoro.py) | 0 | ||||
| -rw-r--r-- | modules/scratchpad-counter.py | 50 | ||||
| -rw-r--r-- | modules/sysdata.py (renamed from examples/sysdata.py) | 13 | ||||
| -rw-r--r-- | modules/weather_yahoo.py (renamed from examples/weather_yahoo.py) | 0 | ||||
| -rw-r--r-- | modules/whoami.py (renamed from examples/whoami.py) | 0 | ||||
| -rw-r--r-- | modules/window-title.py | 58 | ||||
| -rwxr-xr-x | py3status/__init__.py | 102 | ||||
| -rwxr-xr-x | setup.py | 3 |
22 files changed, 528 insertions, 40 deletions
@@ -1,3 +1,18 @@ +version 1.5 (2014-05-03) +* add --version parameter wrt issue #29 +* switch back to RST for the README wrt pypi and issue #30 thx to @Weishaupt +* include modules, changelog and license to sources on pypi +* rename the examples folder to modules and include it in the installed files + +version 1.4 (2014-04-16) +* new ordering mechanism with verbose logging. fixes rare cases where the modules methods were not always loaded in the same order and caused inconsistent ordering between reloads. thx to @guiniol for reporting/debugging and @IotaSpencer and @tasse for testing. +* debug: dont catch print() on debug mode +* debug: add position requested by modules +* Merge pull request #25 from nawadanp/master +* Add new module ns_checker.py, by @nawadanp +* move README to markdown, change ordering +* update the README with the new options from --help + version 1.3 (2014-02-23) * new standalone mode allowing to bypass i3status when you need a py3status-modules-only i3bar * debug mode add log info about py3status invocation config diff --git a/MANIFEST.in b/MANIFEST.in index 191f6ec..53676cb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include *.rst -recursive-include examples *.py +include CHANGELOG LICENSE README.rst +recursive-include modules *.py @@ -1,25 +1,33 @@ -========= +********* py3status -========= +********* +|version| + +.. |version| image:: https://pypip.in/version/py3status/badge.png + **py3status** is an extensible i3status wrapper written in python. Using py3status, you can take control of your i3bar easily by: - - writing your own modules and have their output displayed on your bar - - handling click events on your i3bar and play with them in no time - - seeing your clock tick every second whatever your i3status interval + +- writing your own modules and have their output displayed on your bar +- handling click events on your i3bar and play with them in no time +- seeing your clock tick every second whatever your i3status interval No extra configuration file needed, just install & enjoy ! Documentation ============= See the wiki for up to date documentation: - https://github.com/ultrabug/py3status/wiki + +- https://github.com/ultrabug/py3status/wiki Learn how to write your own modules: - https://github.com/ultrabug/py3status/wiki/Write-your-own-modules + +- https://github.com/ultrabug/py3status/wiki/Write-your-own-modules Get help or share your ideas on IRC: - channel **#py3status** on **FreeNode** + +- channel **#py3status** on **FreeNode** Requirements ============ @@ -31,33 +39,35 @@ You **must** set the `output_format` to `i3bar` in the general section of your i output_format = "i3bar" } -Installation (all users) -======================== -py3status is available on pypi: +Usage +===== +In your i3 config file, simply switch from `i3status` to `py3status` in your `status_command`: :: - $ pip install py3status + status_command py3status -Installation (Gentoo users) -=========================== -py3status is available on portage: +Usually you have your own i3status configuration, just point to it: :: - $ sudo emerge -a py3status + status_command py3status -c ~/.i3/i3status.conf -Installation (Arch users) -========================= -Thanks to @waaaaargh, py3status is present in the Arch User Repository using this URL: +Installation +============ +Pypi +---- +Using pip: :: - https://aur.archlinux.org/packages/py3status-git/ + $ pip install py3status -Usage -===== -In your i3 config file, simply change the `status_command` with: +Gentoo Linux +------------ +Using emerge: :: - status_command py3status + $ sudo emerge -a py3status -Usually you have your own i3status configuration, just point to it: +Arch Linux +---------- +Thanks to @waaaaargh, py3status is present in the Arch User Repository using this URL: :: - status_command py3status -c ~/.i3/i3status.conf + https://aur.archlinux.org/packages/py3status-git/ Options ======= @@ -71,6 +81,7 @@ You can see the help of py3status by issuing `py3status -h`: -s, --standalone standalone mode, do not use i3status -t CACHE_TIMEOUT default injection cache timeout in seconds (default 60 sec) + -v, --version show py3status version and exit Control ======= diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/__init__.py diff --git a/modules/battery-level.py b/modules/battery-level.py new file mode 100644 index 0000000..6c30556 --- /dev/null +++ b/modules/battery-level.py @@ -0,0 +1,68 @@ +# -*- coding: utf8 -*- + +from __future__ import division # python2 compatibility +from time import time + +import math +import re +import subprocess + +""" +Module for displaying information about battery. + +Requires: + - the 'acpi' command line + +@author shadowprince +@license Eclipse Public License +""" + +CACHE_TIMEOUT = 30 # time to update battery +HIDE_WHEN_FULL = False # hide any information when battery is fully charged + +MODE = "bar" # for primitive-one-char bar, or "text" for text percentage ouput + +BLOCKS = ["_", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] # block for bar +TEXT_FORMAT = "Battery: {}" # text with "text" mode. percentage with % replaces {} + +CHARGING_CHARACTER = "⚡" + +# None means - get it from i3 config +COLOR_BAD = None +COLOR_CHARGING = "#FCE94F" +COLOR_DEGRADED = None +COLOR_GOOD = None + + +class Py3status: + def battery_level(self, i3status_output_json, i3status_config): + response = {'name': 'battery-level'} + + acpi = subprocess.check_output(["acpi"]).decode('utf-8') + proc = int(re.search(r"(\d+)%", acpi).group(1)) + + charging = bool(re.match(r".*Charging.*", acpi)) + full = bool(re.match(r".*Unknown.*", acpi)) or bool(re.match(r".*Full.*", acpi)) + + if MODE == "bar": + character = BLOCKS[int(math.ceil(proc/100*(len(BLOCKS) - 1)))] + else: + character = TEXT_FORMAT.format(str(proc) + "%") + + if proc < 30: + response['color'] = COLOR_DEGRADED if COLOR_DEGRADED else i3status_config['color_degraded'] + if proc < 10: + response['color'] = COLOR_BAD if COLOR_BAD else i3status_config['color_bad'] + + if full: + response['color'] = COLOR_GOOD if COLOR_GOOD else i3status_config['color_good'] + response['full_text'] = "" if HIDE_WHEN_FULL else BLOCKS[-1] + elif charging: + response['color'] = COLOR_CHARGING + response['full_text'] = CHARGING_CHARACTER + else: + response['full_text'] = character + + response['cached_until'] = time() + CACHE_TIMEOUT + + return (0, response) diff --git a/examples/dpms.py b/modules/dpms.py index 783c53c..783c53c 100644 --- a/examples/dpms.py +++ b/modules/dpms.py diff --git a/examples/empty_class.py b/modules/empty_class.py index 077269c..077269c 100644 --- a/examples/empty_class.py +++ b/modules/empty_class.py diff --git a/examples/glpi.py b/modules/glpi.py index 929749d..929749d 100644 --- a/examples/glpi.py +++ b/modules/glpi.py diff --git a/examples/i3bar_click_events.py b/modules/i3bar_click_events.py index 792965c..792965c 100644 --- a/examples/i3bar_click_events.py +++ b/modules/i3bar_click_events.py diff --git a/modules/keyboard-layout.py b/modules/keyboard-layout.py new file mode 100644 index 0000000..1ed3cee --- /dev/null +++ b/modules/keyboard-layout.py @@ -0,0 +1,73 @@ +from subprocess import check_output +from time import time + +""" +Module for showing current keyboard layout. + +Requires: + - xkblayout-state + or + - setxkbmap + +@author shadowprince +@license Eclipse Public License +""" + +CACHE_TIMEOUT = 10 # refresh time to update indicator + +# colors of layouts, check your command's output to match keys +LANG_COLORS = { + 'fr': '#268BD2', # solarized blue + 'ru': '#F75252', # red + 'ua': '#FCE94F', # yellow + 'us': '#729FCF', # light blue +} + + +def xbklayout(): + """ + check using xkblayout-state (preferred method) + """ + return check_output( + ["xkblayout-state", "print", "%s"] + ).decode('utf-8') + + +def setxkbmap(): + """ + check using setxkbmap >= 1.3.0 + + Please read issue 33 for more information : + https://github.com/ultrabug/py3status/pull/33 + """ + q = check_output(['setxkbmap', '-query']).decode('utf-8') + return q.replace(' ', '').split(':')[-1] + + +class Py3status: + def __init__(self): + """ + find the best implementation to get the keyboard's layout + """ + try: + xbklayout() + except: + self.command = setxkbmap + else: + self.command = xbklayout + + def keyboard_layout(self, i3status_output_json, i3status_config): + response = { + 'full_text': '', + 'name': 'keyboard-layout', + 'cached_until': time() + CACHE_TIMEOUT + } + + lang = self.command().strip() + lang_color = LANG_COLORS.get(lang) + + response['full_text'] = lang or '??' + if lang_color: + response['color'] = lang_color + + return (0, response) diff --git a/modules/mpd_status.py b/modules/mpd_status.py new file mode 100644 index 0000000..28e99dc --- /dev/null +++ b/modules/mpd_status.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +import re + +from mpd import (MPDClient, CommandError) +from socket import error as SocketError +from time import time + +""" +Mpd - module to show information from mpd to your's bar! Settings listed above. +Reequires + - python-mpd2 (NOT python2-mpd2) + # pip install python-mpd2 + +@author shadowprince +@license Eclipse Public License +""" + +# mpd settings +HOST = "localhost" +PORT = "6600" +PASSWORD = None + +# time to update +CACHE_TIMEOUT = 1 + +# position +POSITION = 0 + +# if text length will be greater - it'll shrink it +MAX_WIDTH = 120 + +# hide any indicator, if +HIDE_WHEN_PAUSED = False +HIDE_WHEN_STOPPED = True + +# state characters (or strings). Actual of them replaces {state} placeholder in STRFORMAT +STATE_CHARACTERS = { + "pause": "[pause]", + "play": "[play]", + "stop": "[stop]", +} + +""" format of result string +can contain: + {state} - current state from STATE_CHARACTERS + Track information: + {track}, {artist}, {title}, {time}, {album}, {pos} + In additional, information about next track also comes in, in analogue with current, but with next_ prefix, + like {next_title} +""" +STRFORMAT = "{state} №{pos}. {artist} - {title} [{time}] | {next_title}" + + +class Py3status: + def __init__(self): + self.text = '' + + def currentTrack(self, i3status_output_json, i3status_config): + try: + c = MPDClient() + c.connect(host=HOST, port=PORT) + if PASSWORD: + c.password(PASSWORD) + + status = c.status() + song = int(status.get("song", 0)) + next_song = int(status.get("nextsong", 0)) + + if (status["state"] == "pause" and HIDE_WHEN_PAUSED) or (status["state"] == "stop" and HIDE_WHEN_STOPPED): + text = "" + else: + try: + song = c.playlistinfo()[song] + song["time"] = "{0:.2f}".format(int(song.get("time", 1)) / 60) + except IndexError: + song = {} + + try: + next_song = c.playlistinfo()[next_song] + except IndexError: + next_song = {} + + format_args = song + format_args["state"] = STATE_CHARACTERS.get(status.get("state", None)) + for k, v in next_song.items(): + format_args["next_{}".format(k)] = v + + text = STRFORMAT + for k, v in format_args.items(): + text = text.replace("{" + k + "}", v) + + for sub in re.findall(r"{\S+?}", text): + text = text.replace(sub, "") + except SocketError: + text = "Failed to connect to mpd!" + except CommandError: + text = "Failed to authenticate to mpd!" + c.disconnect() + + if len(text) > MAX_WIDTH: + text = text[-MAX_WIDTH-3:] + "..." + + if self.text != text: + transformed = True + self.text = text + else: + transformed = False + + response = { + 'cached_until': time() + CACHE_TIMEOUT, + 'full_text': self.text, + 'name': 'scratchpad-count', + 'transformed': transformed + } + + return (POSITION, response) diff --git a/examples/netdata.py b/modules/netdata.py index d923036..729983b 100644 --- a/examples/netdata.py +++ b/modules/netdata.py @@ -64,7 +64,7 @@ class GetData: 'eth0') and grab received/transmitted bytes. """ - net_data = self.execCMD('cat', '/proc/net/dev').split() + net_data = self.execCMD('cat', '/proc/net/dev').decode('utf-8').split() interface_index = net_data.index(self.net_interface + ':') received_bytes = int(net_data[interface_index + 1]) transmitted_bytes = int(net_data[interface_index + 9]) diff --git a/examples/ns_checker.py b/modules/ns_checker.py index 4a332b1..4a332b1 100644 --- a/examples/ns_checker.py +++ b/modules/ns_checker.py diff --git a/examples/pingdom.py b/modules/pingdom.py index cddf1a9..cddf1a9 100644 --- a/examples/pingdom.py +++ b/modules/pingdom.py diff --git a/examples/pomodoro.py b/modules/pomodoro.py index 28b5ea1..28b5ea1 100644 --- a/examples/pomodoro.py +++ b/modules/pomodoro.py diff --git a/modules/scratchpad-counter.py b/modules/scratchpad-counter.py new file mode 100644 index 0000000..390745f --- /dev/null +++ b/modules/scratchpad-counter.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +import i3 +from time import time + +""" +Module showing amount of windows at the scratchpad. + +@author shadowprince +@license Eclipse Public License +""" + +CACHE_TIMEOUT = 5 +HIDE_WHEN_NONE = False # hide indicator when there is no windows +POSITION = 0 +STRFORMAT = "{} ⌫" # format of indicator. {} replaces with count of windows + + +def find_scratch(tree): + if tree["name"] == "__i3_scratch": + return tree + else: + for x in tree["nodes"]: + result = find_scratch(x) + if result: + return result + return None + + +class Py3status: + def __init__(self): + self.count = -1 + + def scratchpad_counter(self, i3status_output_json, i3status_config): + count = len(find_scratch(i3.get_tree()).get("floating_nodes", [])) + + if self.count != count: + transformed = True + self.count = count + else: + transformed = False + + response = { + 'cached_until': time() + CACHE_TIMEOUT, + 'full_text': '' if HIDE_WHEN_NONE and count == 0 else STRFORMAT.format(count), + 'name': 'scratchpad-counter', + 'transformed': transformed + } + + return (POSITION, response) diff --git a/examples/sysdata.py b/modules/sysdata.py index 14aefb3..5354fce 100644 --- a/examples/sysdata.py +++ b/modules/sysdata.py @@ -8,6 +8,10 @@ # i3wm homepage: http://i3wm.org # py3status homepage: https://github.com/ultrabug/py3status +# NOTE: If you want py3status to show you your CPU temperature, change value of CPUTEMP into True +# in Py3status class - CPUInfo function +# and REMEMBER that you must install lm_sensors if you want CPU temp! + # Copyright (C) <2013> <Shahin Azad [ishahinism at Gmail]> # This program is free software: you can redistribute it and/or modify @@ -109,8 +113,15 @@ class Py3status: response['color'] = i3status_config['color_degraded'] else: response['color'] = i3status_config['color_bad'] + #cpu temp + CPUTEMP=False + if CPUTEMP: + cputemp=subprocess.check_output('sensors | grep "CPU Temp" | cut -f 2 -d "+" | cut -f 1 -d " "',shell=True) + cputemp=cputemp[:-1].decode('utf-8') + response['full_text'] = "CPU: %.2f%%" % (used_cpu_percent*100) +" "+cputemp + else: + response['full_text'] = "CPU: %.2f%%" % (used_cpu_percent*100) - response['full_text'] = "CPU: %.2f%%" % (used_cpu_percent*100) #cache the status for 10 seconds response['cached_until'] = time() + 10 diff --git a/examples/weather_yahoo.py b/modules/weather_yahoo.py index 6b91b04..6b91b04 100644 --- a/examples/weather_yahoo.py +++ b/modules/weather_yahoo.py diff --git a/examples/whoami.py b/modules/whoami.py index f930991..f930991 100644 --- a/examples/whoami.py +++ b/modules/whoami.py diff --git a/modules/window-title.py b/modules/window-title.py new file mode 100644 index 0000000..4f6f062 --- /dev/null +++ b/modules/window-title.py @@ -0,0 +1,58 @@ +import i3 +from time import time + +""" +Py3status plugin - shows current window title. + +Requires: + - i3-py (https://github.com/ziberna/i3-py) + # pip install i3-py + +If payload from server contains wierd utf-8 +(for example one window have something bad in title) - the plugin will +give empty output UNTIL this window is closed. +I can't fix or workaround that in PLUGIN, problem is in i3-py library. + +@author shadowprince +@license Eclipse Public License +""" + +CACHE_TIMEOUT = 0.5 # maximum time to update indicator +MAX_WIDTH = 120 # if width of title is greater, shrink it and add '...' +POSITION = 0 + + +def find_focused(tree): + if type(tree) == list: + for el in tree: + res = find_focused(el) + if res: + return res + + elif type(tree) == dict: + if tree['focused']: + return tree + else: + return find_focused(tree['nodes'] + tree['floating_nodes']) + + +class Py3status: + def __init__(self): + self.text = '' + + def window_title(self, i3_status_output_json, i3status_config): + window = find_focused(i3.get_tree()) + + transformed = False + if window and 'name' in window and window['name'] != self.text: + self.text = len(window['name']) > MAX_WIDTH and "..." + window['name'][-(MAX_WIDTH-3):] or window['name'] + transformed = True + + response = { + 'cached_until': time() + CACHE_TIMEOUT, + 'full_text': self.text, + 'name': 'window-title', + 'transformed': transformed + } + + return (POSITION, response) diff --git a/py3status/__init__.py b/py3status/__init__.py index 4bad6bd..e453cda 100755 --- a/py3status/__init__.py +++ b/py3status/__init__.py @@ -549,7 +549,8 @@ class Module(Thread): if self.config['debug']: syslog( LOG_INFO, - 'method {} returned {}'.format(meth, result) + 'method {} returned {} '.format(meth, result) + + 'for position {}'.format(position) ) except Exception: err = sys.exc_info()[1] @@ -597,6 +598,14 @@ class Py3statusWrapper(): 'interval': 1 } + # package version + try: + import pkg_resources + version = pkg_resources.get_distribution('py3status').version + except: + version = 'unknown' + config['version'] = version + # command line options parser = argparse.ArgumentParser( description='The agile, python-powered, i3status wrapper') @@ -625,8 +634,21 @@ class Py3statusWrapper(): default=config['cache_timeout'], help="""default injection cache timeout in seconds (default 60 sec)""") + parser.add_argument('-v', '--version', action="store_true", + help="""show py3status version and exit""") options = parser.parse_args() + # only asked for version + if options.version: + from platform import python_version + print( + 'py3status version {} (python {})'.format( + config['version'], + python_version() + ) + ) + sys.exit(0) + # override configuration and helper variables config['cache_timeout'] = options.cache_timeout config['debug'] = options.debug @@ -699,8 +721,9 @@ class Py3statusWrapper(): syslog(LOG_INFO, 'events thread started') # suppress modules' ouput wrt issue #20 - sys.stdout = open('/dev/null', 'w') - sys.stderr = open('/dev/null', 'w') + if not self.config['debug']: + sys.stdout = open('/dev/null', 'w') + sys.stderr = open('/dev/null', 'w') # load and spawn modules threads for include_path, f_name in self.list_modules(): @@ -777,27 +800,88 @@ class Py3statusWrapper(): """ # prepopulate the list so that every usable index exists, thx @Lujeni m_list = [ - '' for value in range(sum([len(x.methods) for x in self.modules])) + '' for value in range( + sum([len(x.methods) for x in self.modules]) + len(json_list) + ) ] - # append i3status json list to the modules' list - m_list += json_list + + # debug the ordering matrix + if self.config['debug']: + syslog( + LOG_INFO, + 'ordering matrix {}'.format(list(range(len(m_list)))) + ) + # run through modules/methods output and insert them in reverse order + debug_msg = '' for m in reversed(self.modules): for meth in m.methods: position = m.methods[meth]['position'] last_output = m.methods[meth]['last_output'] try: + assert position in range(len(m_list)) if m_list[position] == '': m_list[position] = last_output else: if '' in m_list: m_list.remove('') m_list.insert(position, last_output) - except IndexError: + except (AssertionError, IndexError): # out of range indexes get placed at the end of the output m_list.append(last_output) - # cleanup and return output list - m_list = list(filter(lambda a: a != '', m_list)) + finally: + # debug user module's index + if self.config['debug']: + debug_msg += '{}={} '.format( + meth, + m_list.index(last_output) + ) + + # debug the user modules ordering + if self.config['debug']: + syslog( + LOG_INFO, + 'ordering user modules positions {}'.format(debug_msg.strip()) + ) + + # append i3status json list to the modules' list in empty slots + debug_msg = '' + for i3s_json in json_list: + for i in range(len(m_list)): + if m_list[i] == '': + m_list[i] = i3s_json + break + else: + # this should not happen ! + m_list.append(i3s_json) + + # debug i3status module's index + if self.config['debug']: + debug_msg += '{}={} '.format( + i3s_json['name'], + m_list.index(i3s_json) + ) + + # debug i3status modules ordering + if self.config['debug']: + syslog( + LOG_INFO, + 'ordering i3status modules positions {}'.format( + debug_msg.strip() + ) + ) + + # cleanup and return output list, we also remove empty outputs + m_list = list(filter(lambda a: a != '' and a['full_text'], m_list)) + + # log the final ordering in debug mode + if self.config['debug']: + syslog( + LOG_INFO, + 'ordering result {}'.format([m['name'] for m in m_list]) + ) + + # return the ordered result return m_list def run(self): @@ -15,7 +15,7 @@ def read(fname): setup( name='py3status', - version='1.3', + version='1.5', author='Ultrabug', author_email='ultrabug@ultrabug.net', description='py3status is an extensible i3status wrapper written in python', @@ -42,6 +42,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) |
