diff options
Diffstat (limited to 'py3status')
| -rwxr-xr-x | py3status/__init__.py | 548 | ||||
| -rw-r--r-- | py3status/modules/__init__.py | 0 | ||||
| -rw-r--r-- | py3status/modules/battery-level.py | 68 | ||||
| -rw-r--r-- | py3status/modules/clementine.py | 65 | ||||
| -rw-r--r-- | py3status/modules/dpms.py | 44 | ||||
| -rw-r--r-- | py3status/modules/empty_class.py | 41 | ||||
| -rw-r--r-- | py3status/modules/glpi.py | 52 | ||||
| -rw-r--r-- | py3status/modules/i3bar_click_events.py | 127 | ||||
| -rw-r--r-- | py3status/modules/imap.py | 58 | ||||
| -rw-r--r-- | py3status/modules/keyboard-layout.py | 73 | ||||
| -rw-r--r-- | py3status/modules/mpd_status.py | 117 | ||||
| -rw-r--r-- | py3status/modules/net_rate.py | 163 | ||||
| -rw-r--r-- | py3status/modules/netdata.py | 132 | ||||
| -rw-r--r-- | py3status/modules/ns_checker.py | 57 | ||||
| -rw-r--r-- | py3status/modules/pingdom.py | 59 | ||||
| -rw-r--r-- | py3status/modules/pomodoro.py | 123 | ||||
| -rw-r--r-- | py3status/modules/scratchpad-counter.py | 50 | ||||
| -rw-r--r-- | py3status/modules/sysdata.py | 148 | ||||
| -rw-r--r-- | py3status/modules/vnstat.py | 138 | ||||
| -rw-r--r-- | py3status/modules/weather_yahoo.py | 109 | ||||
| -rw-r--r-- | py3status/modules/whoami.py | 29 | ||||
| -rw-r--r-- | py3status/modules/window-title.py | 60 |
22 files changed, 2131 insertions, 130 deletions
diff --git a/py3status/__init__.py b/py3status/__init__.py index 4182b7c..49bd97e 100755 --- a/py3status/__init__.py +++ b/py3status/__init__.py @@ -14,6 +14,7 @@ from signal import SIGUSR1 from subprocess import Popen from subprocess import PIPE from subprocess import call +from tempfile import NamedTemporaryFile from threading import Event, Thread from time import sleep, time from syslog import syslog, LOG_ERR, LOG_INFO, LOG_WARNING @@ -81,69 +82,147 @@ class I3status(Thread): Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) - self.i3status_config_path = i3status_config_path - self.standalone = standalone - self.config = self.i3status_config_reader() self.error = None + self.i3status_module_names = [ + 'battery', + 'cpu_temperature', + 'cpu_usage', + 'ddate', + 'disk', + 'ethernet', + 'ipv6', + 'load', + 'path_exists', + 'run_watch', + 'time', + 'tztime', + 'volume', + 'wireless' + ] + self.json_list = None + self.json_list_ts = None self.last_output = None self.last_output_ts = None self.last_prefix = None self.lock = lock - self.json_list = None - self.json_list_ts = None + self.standalone = standalone + self.time_format = '%Y-%m-%d %H:%M:%S' + # + self.config = self.i3status_config_reader(i3status_config_path) - def i3status_config_reader(self): + def valid_config_param(self, param_name): + """ + Check if a given section name is a valid parameter for i3status. + """ + valid_config_params = self.i3status_module_names + ['general', 'order'] + return param_name.split(' ')[0] in valid_config_params + + def i3status_config_reader(self, i3status_config_path): """ Parse i3status.conf so we can adapt our code to the i3status config. """ - in_time = False - in_general = False config = { - 'colors': False, - 'color_good': '#00FF00', - 'color_bad': '#FF0000', - 'color_degraded': '#FFFF00', - 'color_separator': '#333333', - 'interval': 5, - 'output_format': None, - 'time_format': '%Y-%m-%d %H:%M:%S' + 'general': { + 'color_bad': '#FF0000', + 'color_degraded': '#FFFF00', + 'color_good': '#00FF00', + 'color_separator': '#333333', + 'colors': False, + 'interval': 5, + 'output_format': 'i3bar' + }, + 'i3s_modules': [], + 'on_click': {}, + 'order': [], + 'py3_modules': [] } # some ugly parsing - if os.path.isfile(self.i3status_config_path): - for line in open(self.i3status_config_path, 'r'): - line = line.strip(' \t\n\r') - if line.startswith('general'): - in_general = True - elif line.startswith('time') or line.startswith('tztime'): - in_time = True - elif line.startswith('}'): - in_general = False - in_time = False - if in_general and '=' in line: - key = line.split('=')[0].strip() - value = line.split('=')[1].strip() - if key in config: - if value in ['true', 'false']: - value = 'True' if value == 'true' else 'False' + in_section = False + section_name = '' + + for line in open(i3status_config_path, 'r'): + line = line.strip(' \t\n\r') + + if not line or line.startswith('#'): + continue + + if line.startswith('order'): + in_section = True + section_name = 'order' + + if not in_section: + section_name = line.split('{')[0].strip() + if section_name not in config: + config[section_name] = {} + + if '{' in line: + in_section = True + + if section_name and '=' in line: + line = line.split('}')[0].strip() + + key = line.split('=')[0].strip() + value = line.split('=')[1].strip() + try: + e_value = eval(value) + if isinstance(e_value, str) or isinstance(e_value, int): + value = e_value + else: + raise ValueError() + except NameError: + pass + except ValueError: + pass + + if section_name == 'order': + config[section_name].append(value) + line = '}' + + # detect internal modules to be loaded dynamically + if not self.valid_config_param(value): + config['py3_modules'].append(value) + else: + config['i3s_modules'].append(value) + else: + if not key.startswith('on_click'): + config[section_name][key] = value + else: + # on_click special parameters try: - config[key] = eval(value) - except NameError: - config[key] = value - if in_time and '=' in line: - key = line.split('=')[0].strip() - value = line.split('=')[1].strip() - if 'time_' + key in config: - config['time_' + key] = eval(value) + button = int(key.split()[1]) + if button not in range(1, 6): + raise ValueError( + 'should be 1, 2, 3, 4 or 5' + ) + except IndexError as e: + raise IndexError( + 'missing "button id" for "on_click" ' + 'parameter in section {}'.format(section_name) + ) + except ValueError as e: + raise ValueError( + 'invalid "button id" ' + 'for "on_click" parameter ' + 'in section {} ({})'.format(section_name, e) + ) + on_c = config['on_click'] + on_c[section_name] = on_c.get(section_name, {}) + on_c[section_name][button] = value - # force output format on standalone mode - if self.standalone: - config['output_format'] = 'i3bar' + # override time format + if section_name in ['time', 'tztime'] and key == 'format': + self.time_format = value - # py3status uses only the i3bar protocol - assert config['output_format'] == 'i3bar', \ - 'i3status output_format should be set to "i3bar" on {}'.format( - self.i3status_config_path + if '}' in line: + in_section = False + section_name = '' + + # py3status only uses the i3bar protocol because it needs JSON output + if config['general']['output_format'] != 'i3bar': + raise RuntimeError( + 'i3status output_format should ' + 'be set to "i3bar" on {}'.format(i3status_config_path) ) return config @@ -154,7 +233,7 @@ class I3status(Thread): """ json_list = deepcopy(self.json_list) try: - time_format = self.config['time_format'] + time_format = self.time_format for item in json_list: if 'name' in item and item['name'] in ['time', 'tztime']: i3status_time = item['full_text'].encode('UTF-8', 'replace') @@ -172,9 +251,7 @@ class I3status(Thread): ) date = datetime.strptime(i3status_time, time_format) date += delta - item['full_text'] = date.strftime( - self.config['time_format'] - ) + item['full_text'] = date.strftime(self.time_format) item['transformed'] = True except Exception: err = sys.exc_info()[1] @@ -191,60 +268,106 @@ class I3status(Thread): self.json_list = deepcopy(self.last_output) self.json_list_ts = deepcopy(self.last_output_ts) + def get_modules_output(self, json_list, py3_modules): + """ + Return the final json list to be displayed on the i3bar by taking + into account every py3status configured module and i3status'. + Simply put, this method honors the initial 'order' configured by + the user in his i3status.conf. + """ + ordered = [] + for module in self.config['order']: + if module in py3_modules: + for method in py3_modules[module].methods.values(): + ordered.append(method['last_output']) + else: + for m, j in zip(self.config['i3s_modules'], json_list): + if m == module: + ordered.append(j) + return ordered + + def write_tmp_i3status_config(self, tmpfile): + """ + Given a temporary file descriptor, write a valid i3status config file + based on the parsed one from 'i3status_config_path'. + """ + for section_name, conf in self.config.items(): + if section_name in ['i3s_modules', 'py3_modules']: + continue + elif section_name == 'order': + for module_name in conf: + if self.valid_config_param(module_name): + tmpfile.write('order += "%s"\n' % module_name) + tmpfile.write('\n') + elif self.valid_config_param(section_name): + tmpfile.write('%s {\n' % section_name) + for key, value in conf.items(): + tmpfile.write(' %s = "%s"\n' % (key, value)) + tmpfile.write('}\n\n') + tmpfile.flush() + def run(self): """ - Spawn i3status and poll its output. + Spawn i3status using a self generated config file and poll its output. """ - i3status_pipe = Popen( - ['i3status', '-c', self.i3status_config_path], - stdout=PIPE, - stderr=PIPE, - ) - self.poller_inp = IOPoller(i3status_pipe.stdout) - self.poller_err = IOPoller(i3status_pipe.stderr) + with NamedTemporaryFile(prefix='py3status_') as tmpfile: + self.write_tmp_i3status_config(tmpfile) + syslog( + LOG_INFO, + 'i3status spawned using config file {}'.format(tmpfile.name) + ) - try: - # at first, poll very quickly to avoid delay in first i3bar display - timeout = 0.001 + i3status_pipe = Popen( + ['i3status', '-c', tmpfile.name], + stdout=PIPE, + stderr=PIPE, + ) + self.poller_inp = IOPoller(i3status_pipe.stdout) + self.poller_err = IOPoller(i3status_pipe.stderr) - # loop on i3status output - while self.lock.is_set(): - line = self.poller_inp.readline(timeout) - if line: - if line.startswith('[{'): - with jsonify(line) as (prefix, json_list): - self.last_output = json_list - self.last_output_ts = datetime.utcnow() - self.last_prefix = ',' - self.update_json_list() - print_line(line) - elif not line.startswith(','): - if 'version' in line: - header = loads(line) - header.update({'click_events': True}) - line = dumps(header) - print_line(line) - else: - timeout = 0.5 - with jsonify(line) as (prefix, json_list): - self.last_output = json_list - self.last_output_ts = datetime.utcnow() - self.last_prefix = prefix - else: - err = self.poller_err.readline(timeout) - code = i3status_pipe.poll() - if code is not None: - if err: - msg = 'i3status died and said: {}'.format(err) + try: + # at first, poll very quickly + # to avoid delay in first i3bar display + timeout = 0.001 + + # loop on i3status output + while self.lock.is_set(): + line = self.poller_inp.readline(timeout) + if line: + if line.startswith('[{'): + with jsonify(line) as (prefix, json_list): + self.last_output = json_list + self.last_output_ts = datetime.utcnow() + self.last_prefix = ',' + self.update_json_list() + print_line(line) + elif not line.startswith(','): + if 'version' in line: + header = loads(line) + header.update({'click_events': True}) + line = dumps(header) + print_line(line) else: - msg = 'i3status died with code {}'.format(code) - raise IOError(msg) + timeout = 0.5 + with jsonify(line) as (prefix, json_list): + self.last_output = json_list + self.last_output_ts = datetime.utcnow() + self.last_prefix = prefix else: - # poll is CPU intensive, breath a bit - sleep(timeout) - except IOError: - err = sys.exc_info()[1] - self.error = err + err = self.poller_err.readline(timeout) + code = i3status_pipe.poll() + if code is not None: + if err: + msg = 'i3status died and said: {}'.format(err) + else: + msg = 'i3status died with code {}'.format(code) + raise IOError(msg) + else: + # poll is CPU intensive, breath a bit + sleep(timeout) + except IOError: + err = sys.exc_info()[1] + self.error = err def mock(self): """ @@ -273,7 +396,7 @@ class Events(Thread): """ This class is responsible for dispatching event JSONs sent by the i3bar. """ - def __init__(self, lock, config, modules): + def __init__(self, lock, config, modules, on_click): """ We need to poll stdin to receive i3bar messages. """ @@ -282,6 +405,7 @@ class Events(Thread): self.lock = lock self.modules = modules self.poller_inp = IOPoller(sys.stdin) + self.on_click = on_click def dispatch(self, module, obj, event): """ @@ -306,7 +430,7 @@ class Events(Thread): it will dispatch i3status click events to this module so you can catch them and trigger any function call based on the event. """ - for module in self.modules: + for module in self.modules.values(): if not module.click_events: continue if module.module_name == 'i3bar_click_events.py': @@ -314,6 +438,57 @@ class Events(Thread): else: return False + def refresh(self, module_name): + """ + Force a cache expiration for all the methods of the given module. + """ + module = self.modules.get(module_name) + if module is not None: + if self.config['debug']: + syslog(LOG_INFO, 'refresh module {}'.format(module_name)) + for obj in module.methods.values(): + obj['cached_until'] = time() + + @staticmethod + def refresh_all(module_name): + """ + Force a full refresh of py3status and i3status modules by sending + a SIGUSR1 signal to py3status. + """ + Popen(['killall', '-USR1', 'py3status']) + + def on_click_dispatcher(self, module_name, command): + """ + Dispatch on_click config parameters to either: + - Our own methods for special py3status commands (listed below) + - The i3-msg program which is part of i3wm + """ + py3_commands = ['refresh', 'refresh_all'] + if command is None: + return + elif command in py3_commands: + # this is a py3status command handled by this class + method = getattr(self, command) + method(module_name) + else: + # this is a i3 message + self.i3_msg(module_name, command) + + @staticmethod + def i3_msg(module_name, command): + """ + Execute the given i3 message and log its output. + """ + i3_msg_pipe = Popen(['i3-msg', command], stdout=PIPE) + syslog( + LOG_INFO, + 'i3-msg module={} command="{}" stdout={}'.format( + module_name, + command, + i3_msg_pipe.stdout.read() + ) + ) + def run(self): """ Wait for an i3bar JSON event, then find the right module to dispatch @@ -336,13 +511,28 @@ class Events(Thread): if self.config['debug']: syslog(LOG_INFO, 'received event {}'.format(event)) - # setup default action on button 2 press + # usage variables + button = event.get('button', 0) default_event = False dispatched = False - if 'button' in event and event['button'] == 2: + instance = event.get('instance', '') + name = event.get('name', '') + + # guess the module config name + module_name = '{} {}'.format(name, instance).strip() + + # execute any configured i3-msg command + if self.on_click.get(module_name, {}).get(button): + self.on_click_dispatcher( + module_name, + self.on_click[module_name].get(button) + ) + dispatched = True + # otherwise setup default action on button 2 press + elif button == 2: default_event = True - for module in self.modules: + for module in self.modules.values(): # skip modules not supporting click_events # unless we have a default_event set if not module.click_events and not default_event: @@ -350,9 +540,9 @@ class Events(Thread): # check for the method name/instance for obj in module.methods.values(): - if event['name'] == obj['name']: - if 'instance' in event: - if event['instance'] == obj['instance']: + if name == obj['name']: + if instance: + if instance == obj['instance']: self.dispatch(module, obj, event) dispatched = True break @@ -413,6 +603,20 @@ class Module(Thread): inst = py_mod.Py3status() return (mod_name, inst) + @staticmethod + def load_from_namespace(module_name): + """ + Load a py3status bundled module. + """ + inst = None + name = 'py3status.modules.{}'.format(module_name.split(' ')[0]) + py_mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + py_mod = getattr(py_mod, comp) + inst = py_mod.Py3status() + return (module_name, inst) + def clear_cache(self): """ Reset the cache for all methods of this module. @@ -431,9 +635,20 @@ class Module(Thread): - 'on_click' methods as they'll be called upon a click_event - 'kill' methods as they'll be called upon this thread's exit """ - module, class_inst = self.load_from_file(include_path + f_name) + if include_path is not None: + module, class_inst = self.load_from_file(include_path + f_name) + else: + module, class_inst = self.load_from_namespace(f_name) + if module and class_inst: self.module_class = class_inst + + # apply module configuration from i3status config + mod_config = self.i3status_thread.config.get(self.module_name, {}) + for config, value in mod_config.items(): + setattr(self.module_class, config, value) + + # get the available methods for execution for method in sorted(dir(class_inst)): if method.startswith('_'): continue @@ -480,7 +695,7 @@ class Module(Thread): click_method = getattr(self.module_class, 'on_click') click_method( self.i3status_thread.json_list, - self.i3status_thread.config, + self.i3status_thread.config['general'], event ) except Exception: @@ -511,18 +726,31 @@ class Module(Thread): try: # execute method and get its output method = getattr(self.module_class, meth) - position, result = method( + response = method( self.i3status_thread.json_list, - self.i3status_thread.config + self.i3status_thread.config['general'] ) - # validate the result - assert isinstance(result, dict), "should return a dict" - assert 'full_text' in result, "missing 'full_text' key" - assert 'name' in result, "missing 'name' key" + if isinstance(response, dict): + # this is a shiny new module giving a dict response + position, result = None, response + result['name'] = self.module_name.split(' ')[0] + result['instance'] = ''.join( + self.module_name.split(' ')[1:] + ) + else: + # this is an old school module reporting its position + position, result = response + if not isinstance(position, int): + raise TypeError('position is not an int') + if not isinstance(result, dict): + raise TypeError('response should be a dict') + if 'name' not in result: + raise KeyError('missing "name" key in response') - # validate the position - assert isinstance(position, int), "position is not an int" + # validate the response + if not 'full_text' in result: + raise KeyError('missing "full_text" key in response') # initialize method object if my_method['name'] is None: @@ -568,7 +796,7 @@ class Module(Thread): kill_method = getattr(self.module_class, 'kill') kill_method( self.i3status_thread.json_list, - self.i3status_thread.config + self.i3status_thread.config['general'] ) except Exception: # this would be stupid to die on exit @@ -583,8 +811,9 @@ class Py3statusWrapper(): """ Useful variables we'll need. """ - self.modules = [] + self.modules = {} self.lock = Event() + self.py3_modules = [] def get_config(self): """ @@ -666,14 +895,27 @@ class Py3statusWrapper(): def list_modules(self): """ - Search import directories and files through given include paths. + Search import directories and files through given include paths with + respect to i3status.conf configured py3status modules. + + User provided modules take precedence over py3status generic modules + but if none has been configured then we'll load every file present + as this is the legacy behavior. + This method is a generator and loves to yield. """ for include_path in sorted(self.config['include_paths']): include_path = os.path.abspath(include_path) + '/' - if os.path.isdir(include_path): - for f_name in sorted(os.listdir(include_path)): - if f_name.endswith('.py'): + if not os.path.isdir(include_path): + continue + + for f_name in sorted(os.listdir(include_path)): + if f_name.endswith('.py'): + if self.py3_modules: + mod_name = f_name.rstrip('.py') + if mod_name in self.py3_modules: + yield (include_path, f_name) + else: yield (include_path, f_name) def setup(self): @@ -718,7 +960,12 @@ class Py3statusWrapper(): ) # setup input events thread - self.events_thread = Events(self.lock, self.config, self.modules) + self.events_thread = Events( + self.lock, + self.config, + self.modules, + self.i3status_thread.config['on_click'] + ) self.events_thread.start() if self.config['debug']: syslog(LOG_INFO, 'events thread started') @@ -728,8 +975,13 @@ class Py3statusWrapper(): sys.stdout = open('/dev/null', 'w') sys.stderr = open('/dev/null', 'w') - # load and spawn modules threads + # get the list of py3status configured modules + self.py3_modules = self.i3status_thread.config['py3_modules'] + + # load and spawn external modules threads + # based on inclusion folder for include_path, f_name in self.list_modules(): + module_name = f_name.rstrip('.py') try: my_m = Module( self.lock, @@ -741,7 +993,7 @@ class Py3statusWrapper(): # only start and handle modules with available methods if my_m.methods: my_m.start() - self.modules.append(my_m) + self.modules[module_name] = my_m elif self.config['debug']: syslog( LOG_INFO, @@ -752,6 +1004,33 @@ class Py3statusWrapper(): msg = 'loading {} failed ({})'.format(f_name, err) self.i3_nagbar(msg, level='warning') + # load and spawn i3status.conf configured modules threads + for module_name in self.py3_modules: + # ignore if the user already provided this module + if module_name in self.modules: + continue + try: + my_m = Module( + self.lock, + self.config, + None, + module_name, + self.i3status_thread + ) + # only start and handle modules with available methods + if my_m.methods: + my_m.start() + self.modules[module_name] = my_m + elif self.config['debug']: + syslog( + LOG_INFO, + 'ignoring {} (no methods found)'.format(module_name) + ) + except Exception: + err = sys.exc_info()[1] + msg = 'loading {} failed ({})'.format(module_name, err) + self.i3_nagbar(msg, level='warning') + def i3_nagbar(self, msg, level='error'): """ Make use of i3-nagbar to display errors and warnings to the user. @@ -791,7 +1070,7 @@ class Py3statusWrapper(): """ For every module, reset the 'cached_until' of all its methods. """ - for module in self.modules: + for module in self.modules.values(): module.clear_cache() def get_modules_output(self, json_list): @@ -804,7 +1083,8 @@ 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]) + len(json_list) + sum([len(x.methods) for x in self.modules.values()]) + + len(json_list) ) ] @@ -817,7 +1097,7 @@ class Py3statusWrapper(): # run through modules/methods output and insert them in reverse order debug_msg = '' - for m in reversed(self.modules): + for m in reversed(self.modules.values()): for meth in m.methods: position = m.methods[meth]['position'] last_output = m.methods[meth]['last_output'] @@ -925,7 +1205,7 @@ class Py3statusWrapper(): json_list = self.i3status_thread.adjust_time(delta) # check that every module thread is alive - for module in self.modules: + for module in self.modules.values(): if not module.is_alive(): # don't spam the user with i3-nagbar warnings if not hasattr(module, 'i3_nagbar'): @@ -937,7 +1217,15 @@ class Py3statusWrapper(): # construct the global output, modules first if self.modules: - json_list = self.get_modules_output(json_list) + if self.py3_modules: + # new style i3status configured ordering + json_list = self.i3status_thread.get_modules_output( + json_list, + self.modules + ) + else: + # old style ordering + json_list = self.get_modules_output(json_list) # dump the line to stdout print_line('{}{}'.format(prefix, dumps(json_list))) diff --git a/py3status/modules/__init__.py b/py3status/modules/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/py3status/modules/__init__.py diff --git a/py3status/modules/battery-level.py b/py3status/modules/battery-level.py new file mode 100644 index 0000000..6c30556 --- /dev/null +++ b/py3status/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/py3status/modules/clementine.py b/py3status/modules/clementine.py new file mode 100644 index 0000000..61917b7 --- /dev/null +++ b/py3status/modules/clementine.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +from time import time +from subprocess import check_output + + +class Py3status: + """ + clementine.py + + This module display the current "artist - title" playing in Clementine. + + Last modified: 2014-03-23 + Author: Francois LASSERRE <choiz@me.com> + License: GNU GPL http://www.gnu.org/licenses/gpl.html + """ + def _getMetadatas(self): + """ + Get the current song metadatas (artist - title) + """ + track_id = check_output('qdbus org.mpris.clementine /TrackList org.freedesktop.MediaPlayer.GetCurrentTrack', shell=True) + metadatas = check_output('qdbus org.mpris.clementine /TrackList org.freedesktop.MediaPlayer.GetMetadata {}'.format(track_id.decode()), shell=True) + lines = metadatas.decode('utf-8').split('\n') + lines = filter(None, lines) + + now_playing = '' + + if lines: + artist = '' + title = '' + internet_radio = False + + for item in lines: + if item.find('artist:') != -1: + artist = item[8:] + if item.find('title:') != -1: + title = item[7:] + + if title.find('.wav') != -1 or title.find('.mp3') != -1: + title = title[:-4] + if title.find('http') != -1: + title = '' + internet_radio = True + + if artist and title: + now_playing = '♫ {} - {}'.format(artist, title) + elif artist: + now_playing = '♫ {}'.format(artist) + elif title: + now_playing = '♫ {}'.format(title) + elif internet_radio: + now_playing = '♫ Internet Radio' + + return now_playing + + def clementine(self, i3status_output_json, i3status_config): + """ + Get the current "artist - title" and return it. + """ + response = {'full_text': '', 'name': 'clementine'} + + response['cached_until'] = time() + response['full_text'] = self._getMetadatas() + + return (0, response) diff --git a/py3status/modules/dpms.py b/py3status/modules/dpms.py new file mode 100644 index 0000000..783c53c --- /dev/null +++ b/py3status/modules/dpms.py @@ -0,0 +1,44 @@ +from os import system + + +class Py3status: + """ + This module allows activation and deactivation + of DPMS (Display Power Management Signaling) + by clicking on 'DPMS' in the status bar. + + Written and contributed by @tasse: + Andre Doser <dosera AT tf.uni-freiburg.de> + """ + def __init__(self): + """ + Detect current state on start. + """ + self.run = system('xset -q | grep -iq "DPMS is enabled"') == 0 + + def dpms(self, i3status_output_json, i3status_config): + """ + Display a colorful state of DPMS. + """ + result = { + 'full_text': 'DPMS', + 'name': 'dpms' + } + if self.run: + result['color'] = i3status_config['color_good'] + else: + result['color'] = i3status_config['color_bad'] + return (0, result) + + def on_click(self, json, i3status_config, event): + """ + Enable/Disable DPMS on left click. + """ + if event['button'] == 1: + if self.run: + self.run = False + system("xset -dpms") + else: + self.run = True + system("xset +dpms") + system("killall -USR1 py3status") diff --git a/py3status/modules/empty_class.py b/py3status/modules/empty_class.py new file mode 100644 index 0000000..077269c --- /dev/null +++ b/py3status/modules/empty_class.py @@ -0,0 +1,41 @@ +class Py3status: + """ + Empty and basic py3status class. + + NOTE: py3status will NOT execute: + - methods starting with '_' + - methods decorated by @property and @staticmethod + + NOTE: reserved method names: + - 'kill' method for py3status exit notification + - 'on_click' method for click events from i3bar + """ + def kill(self, i3status_output_json, i3status_config): + """ + This method will be called upon py3status exit. + """ + pass + + def on_click(self, i3status_output_json, i3status_config, event): + """ + This method will be called when a click event occurs on this module's + output on the i3bar. + + Example 'event' json object: + {'y': 13, 'x': 1737, 'button': 1, 'name': 'empty', 'instance': 'first'} + """ + pass + + def empty(self, i3status_output_json, i3status_config): + """ + This method will return an empty text message + so it will NOT be displayed on your i3bar. + + If you want something displayed you should write something + in the 'full_text' key of your response. + + See the i3bar protocol spec for more information: + http://i3wm.org/docs/i3bar-protocol.html + """ + response = {'full_text': '', 'name': 'empty', 'instance': 'first'} + return (0, response) diff --git a/py3status/modules/glpi.py b/py3status/modules/glpi.py new file mode 100644 index 0000000..929749d --- /dev/null +++ b/py3status/modules/glpi.py @@ -0,0 +1,52 @@ +# You need MySQL-python from http://pypi.python.org/pypi/MySQL-python +import MySQLdb + + +class Py3status: + """ + This example class demonstrates how to display the current total number of + open tickets from GLPI in your i3bar. + + It features thresholds to colorize the output and forces a low timeout to + limit the impact of a server connectivity problem on your i3bar freshness. + + Note that we don't have to implement a cache layer as it is handled by + py3status automagically. + """ + def count_glpi_open_tickets(self, json, i3status_config): + response = {'full_text': '', 'name': 'glpi_tickets'} + + # user-defined variables + CRIT_THRESHOLD = 20 + WARN_THRESHOLD = 15 + MYSQL_DB = '' + MYSQL_HOST = '' + MYSQL_PASSWD = '' + MYSQL_USER = '' + POSITION = 0 + + mydb = MySQLdb.connect( + host=MYSQL_HOST, + user=MYSQL_USER, + passwd=MYSQL_PASSWD, + db=MYSQL_DB, + connect_timeout=5, + ) + mycr = mydb.cursor() + mycr.execute('''select count(*) + from glpi_tickets + where closedate is NULL and solvedate is NULL;''') + row = mycr.fetchone() + if row: + open_tickets = int(row[0]) + if i3status_config['colors']: + if open_tickets > CRIT_THRESHOLD: + response.update({'color': i3status_config['color_bad']}) + elif open_tickets > WARN_THRESHOLD: + response.update( + {'color': i3status_config['color_degraded']} + ) + response['full_text'] = '%s tickets' % open_tickets + mydb.close() + + return (POSITION, response) diff --git a/py3status/modules/i3bar_click_events.py b/py3status/modules/i3bar_click_events.py new file mode 100644 index 0000000..792965c --- /dev/null +++ b/py3status/modules/i3bar_click_events.py @@ -0,0 +1,127 @@ +from subprocess import Popen +from time import time + + +class Py3status: + """ + This module allows you to take actions based on click events made on + the i3status modules. For example, thanks to this module you could + launch the wicd GUI when clicking on the ethernet or wireless module + of your i3status output ! + + IMPORTANT: + This module file name is reserved and should NOT be changed if you + want py3status to handle your i3status modules click events ! + + The behavior described above will only work if this file is named + 'i3bar_click_events.py' ! + """ + def __init__(self): + """ + This is where you setup your actions based on your i3status config. + + Configuration: + -------------- + self.actions = { + "<module name and instance>": { + <button number>: [<function to run>, <arg1>, <arg2>], + } + } + + Variables: + ---------- + <button number> is an integer from 1 to 3: + 1 : left click + 2 : middle click + 3 : right click + + <function to run> is a python function written in this module. + The 'external_command' function is provided for convenience if + you want to call an external program from this module. + You can of course write your own functions and have them executed + on a click event at will with possible arguments <arg1>, <arg2>... + + <module name and instance> is a string made from the module + attribute 'name' and 'instance' using a space as separator: + For i3status modules, it's simply the name of the module as it + appears in the 'order' instruction of your i3status.conf. + Example: + in i3status.conf -> order += "wireless wlan0" + self.actions key -> "wireless wlan0" + + Usage example: + -------------- + - Open the wicd-gtk GUI when we LEFT click on the ethernet + module of i3status. + - Open emelfm2 to /home when we LEFT click on + the /home instance of disk_info + - Open emelfm2 to / when we LEFT click on + the / instance of disk_info + + The related i3status.conf looks like: + order += "disk /home" + order += "disk /" + order += "ethernet eth0" + + The resulting self.actions should be: + self.actions = { + "ethernet eth0": { + 1: [external_command, 'wicd-gtk', '-n'], + }, + "disk_info /home": { + 1: [external_command, 'emelfm2', '-1', '~'], + }, + "disk_info /": { + 1: [external_command, 'emelfm2', '-1', '/'], + }, + } + """ + # CONFIGURE ME PLEASE, LOVE YOU BIG TIME ! + self.actions = { + } + + def on_click(self, i3status_output_json, i3status_config, event): + """ + If an action is configured for the given i3status module 'name' and + 'instance' (if present), then we'll execute the given function with + its arguments (if present). + + Usually you SHOULD NOT modify this part of the code. + """ + button = event['button'] + key_name = '{} {}'.format( + event['name'], + event.get('instance', '') + ).strip() + if key_name in self.actions and button in self.actions[key_name]: + # get the function to run + func = self.actions[key_name][button][0] + assert hasattr(func, '__call__'), \ + 'first element of the action list must be a function' + + # run the function with the possibly given arguments + func(*self.actions[key_name][button][1:]) + + def i3bar_click_events(self, i3status_output_json, i3status_config): + """ + Cached empty output, this module doesn't show anything. + """ + response = {'full_text': '', 'name': 'i3bar_click_events'} + response['cached_until'] = time() + 3600 + return (-1, response) + + +def external_command(*cmd): + """ + This convenience function lets you call an external program at will. + + NOTE: + The stdout and stderr MUST be suppressed as shown here to avoid any + output from being caught by the i3bar (this would freeze it). + See issue #20 for more info. + """ + Popen( + cmd, + stdout=open('/dev/null', 'w'), + stderr=open('/dev/null', 'w') + ) diff --git a/py3status/modules/imap.py b/py3status/modules/imap.py new file mode 100644 index 0000000..5953ee3 --- /dev/null +++ b/py3status/modules/imap.py @@ -0,0 +1,58 @@ +# -*- coding: utf8 -*- + +from time import time +import imaplib + +""" +Module displaying the number of unread messages +on an IMAP inbox (configurable). + +@author obb +""" + + +class Py3status: + + def __init__(self): + self.service_name = 'Mail' + self.imap_server = '<IMAP_SERVER>' + self.port = '993' + + self.user = '<USERNAME>' + self.password = '<PASSWORD>' + + self.mailbox = 'INBOX' + self.criterion = 'UNSEEN' + + self.check_frequency = 60 + + def check_mail(self, json, i3status_config): + mail_count = self._get_mail_count() + + response = { + 'name': self.service_name + '_checker', + 'full_text': '{}: {}'.format(self.service_name, mail_count), + 'cached_until': time() + self.check_frequency + } + + new_mail_color = i3status_config['color_good'] + check_failed_color = i3status_config['color_bad'] + + if mail_count == 'N/A': + response['color'] = check_failed_color + elif mail_count != '0': + response['color'] = new_mail_color + + return (0, response) + + def _get_mail_count(self): + try: + connection = imaplib.IMAP4_SSL(self.imap_server, self.port) + connection.login(self.user, self.password) + connection.select(self.mailbox) + unseen_response = connection.search(None, self.criterion) + mails = unseen_response[1][0].split() + mail_count = len(mails) + return mail_count + except: + return 'N/A' diff --git a/py3status/modules/keyboard-layout.py b/py3status/modules/keyboard-layout.py new file mode 100644 index 0000000..1ed3cee --- /dev/null +++ b/py3status/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/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py new file mode 100644 index 0000000..28e99dc --- /dev/null +++ b/py3status/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/py3status/modules/net_rate.py b/py3status/modules/net_rate.py new file mode 100644 index 0000000..dff075c --- /dev/null +++ b/py3status/modules/net_rate.py @@ -0,0 +1,163 @@ +# -*- coding: utf8 -*- + +from __future__ import division # python2 compatibility +from time import time, sleep + + +""" +Module for displaying current network transfer rate. + +@author shadowprince +@license Eclipse Public License +""" + +CACHED_TIME = 2 # update time (in seconds) +POSITION = 0 # bar position + +DEVFILE = "/proc/net/dev" # location of dev file under /proc + +INTERFACES = [] # list of interfaces to track +ALL_INTERFACES = True # ignore INTERFACES, but not INTERFACES_BLACKLIST +INTERFACES_BLACKLIST = ["lo"] # list of interfaces to ignore + +""" +Format of status string. + +Placeholders: + interface - name of interface + total - total rate + up - upload rate + down - download rate +""" +FORMAT = "{interface}: {total}" + +PRECISION = 1 # amount of numbers after dot +MULTIPLIER_TOP = 999 # if value is greater, divide it with UNIT_MULTI and get next unit from UNITS +LEFT_ALIGN = len(str(MULTIPLIER_TOP)) + 1 + PRECISION # == 6 characters (from MULTIPLIER_TOP + dot + PRECISION) + +""" +Format of total, up and down placeholders under FORMAT. +As default, substitutes LEFT_ALIGN and PRECISION as %s and %s +Placeholders: + value - value (float) + unit - unit (string) + +""" +VALUE_FORMAT = "{value:%s.%sf} {unit}" % (LEFT_ALIGN, PRECISION) + +INITIAL_MULTI = 1024 # initial multiplier, if you want to get rid of first bytes, set to 1 to disable +UNIT_MULTI = 1024 # value to divide if rate is greater than MULTIPLIER_TOP +UNITS = ["kb/s", "mb/s", "gb/s", "tb/s", ] # list of units, first one - value/INITIAL_MULTI, second - value/1024, third - value/1024^2, etc... + +NO_CONNECTION = "! no data" # when there is no data transmitted from the start of the plugin +HIDE_IF_NO = False # hide indicator if rate == 0 + + +def get_stat(): + """ + Get statistics from devfile in list of lists of words + """ + def dev_filter(x): + # get first word and remove trailing interface number + x = x.strip().split(" ")[0][:-1] + + if x in INTERFACES_BLACKLIST: + return False + + if ALL_INTERFACES: + return True + + if x in INTERFACES: + return True + + return False + + # read devfile, skip two header files + x = filter(dev_filter, open(DEVFILE).readlines()[2:]) + + try: + # split info into words, filter empty ones + return [list(filter(lambda x: x, _x.split(" "))) for _x in x] + + except StopIteration: + return None + + +def divide_and_format(value): + """ + Divide a value and return formatted string + """ + for i, unit in enumerate(UNITS): + if value > MULTIPLIER_TOP: + value /= UNIT_MULTI + else: + break + + return VALUE_FORMAT.format(value=value, unit=unit) + + +class Py3status: + def __init__(self, *args, **kwargs): + self.last_stat = get_stat() + self.last_time = time() + self.last_interface = None + + def currentSpeed(self, json, i3status_config): + ns = get_stat() + deltas = {} + try: + # time from previous check + timedelta = time() - self.last_time + + # calculate deltas for all interfaces + for old, new in zip(self.last_stat, ns): + down = int(new[1]) - int(old[1]) + up = int(new[9]) - int(old[9]) + + down /= timedelta * INITIAL_MULTI + up /= timedelta * INITIAL_MULTI + + deltas[new[0]] = {'total': up+down, 'up': up, 'down': down, } + + # update last_ info + self.last_stat = get_stat() + self.last_time = time() + + # get the interface with max rate + interface = max(deltas, key=lambda x: deltas[x]['total']) + + # if there is no rate - show last active interface, or hide + if deltas[interface]['total'] == 0: + interface = self.last_interface + hide = HIDE_IF_NO + # if there is - update last_interface + else: + self.last_interface = interface + hide = False + + # get the deltas into variable + delta = deltas[interface] if interface else None + + except TypeError: + delta = None + interface = None + hide = HIDE_IF_NO + + return (POSITION, { + 'transformed': True, + 'full_text': "" if hide else + FORMAT.format( + total=divide_and_format(delta['total']), + up=divide_and_format(delta['up']), + down=divide_and_format(delta['down']), + interface=interface[:-1], + ) if interface else NO_CONNECTION, + 'name': 'speed', + 'cached_until': time() + CACHED_TIME, + }) + +if __name__ == "__main__": + x = Py3status() + while True: + print(x.currentSpeed(1, 1)) + sleep(1) diff --git a/py3status/modules/netdata.py b/py3status/modules/netdata.py new file mode 100644 index 0000000..729983b --- /dev/null +++ b/py3status/modules/netdata.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +# netdata + +# Netdata is a module uses great Py3status (i3status wrapper) to +# display network information (Linux systems) in i3bar. +# For more information read: +# i3wm homepage: http://i3wm.org +# py3status homepage: https://github.com/ultrabug/py3status + +# Copyright (C) <2013> <Shahin Azad [ishahinism at Gmail]> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# ----------------------------------------------------------------- # +# Notes: +# 1. netdata will check 'eth0' interface by default. You can +# change it by changing 'self.net_interface' variable in 'GetData' +# class. +# 2. Colors are depended on strict specification in traffic/netspeed methods. +# You can change them by manipulating conditions. + +import subprocess +from time import time + +# Method 'netSpeed' will use this variables to calculate downloaded +# bytes in last second. Initializing this variables globally is +# necessary since we can't use __init__ method in Py3Status class. +old_transmitted, old_received = 0, 0 + + +class GetData: + """Get system status + + """ + def __init__(self): + # You can change it to another interface. + # It'll be used for grabbing net interface data. + self.net_interface = 'eth0' + + def execCMD(self, cmd, arg): + """Take a system command and its argument, then return the result. + + Arguments: + - `cmd`: system command. + - `arg`: argument. + """ + result = subprocess.check_output([cmd, arg]) + return result + + def netBytes(self): + """Execute 'cat /proc/net/dev', find the interface line (Default + 'eth0') and grab received/transmitted bytes. + + """ + 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]) + + return received_bytes, transmitted_bytes + + +class Py3status: + """ + System status in i3bar + """ + def netSpeed(self, json, i3status_config): + """Calculate network speed ('eth0' interface) and return it. You can + change the interface using 'self.net_interface' variable in + 'GetData' class. + + """ + data = GetData() + response = {'full_text': '', 'name': 'net_speed'} + + global old_received + global old_transmitted + + received_bytes, transmitted_bytes = data.netBytes() + dl_speed = (received_bytes - old_received) / 1024. + up_speed = (transmitted_bytes - old_transmitted) / 1024. + + if dl_speed < 30: + response['color'] = i3status_config['color_bad'] + elif dl_speed < 60: + response['color'] = i3status_config['color_degraded'] + else: + response['color'] = i3status_config['color_good'] + + response['full_text'] = "LAN(Kb): {:5.1f}↓ {:5.1f}↑"\ + .format(dl_speed, up_speed) + response['cached_until'] = time() + + old_received, old_transmitted = received_bytes, transmitted_bytes + return (0, response) + + def traffic(self, json, i3status_config): + """Calculate networks used traffic. Same as 'netSpeed' method you can + change the interface. + + """ + data = GetData() + response = {'full_text': '', 'name': 'traffic'} + + received_bytes, transmitted_bytes = data.netBytes() + download = received_bytes / 1024 / 1024. + upload = transmitted_bytes / 1024 / 1024. + total = download + upload + + if total < 400: + response['color'] = i3status_config['color_good'] + elif total < 700: + response['color'] = i3status_config['color_degraded'] + else: + response['color'] = i3status_config['color_bad'] + + response['full_text'] = "T(Mb): {:3.0f}↓ {:3.0f}↑ {:3.0f}↕"\ + .format(download, upload, total) + + return (1, response) diff --git a/py3status/modules/ns_checker.py b/py3status/modules/ns_checker.py new file mode 100644 index 0000000..4a332b1 --- /dev/null +++ b/py3status/modules/ns_checker.py @@ -0,0 +1,57 @@ +import dns.resolver +import socket + + +class Py3status: + """ + This module launch a simple query on each nameservers for the specified domain. + Nameservers are dynamically retrieved. The FQDN is the only one mandatory parameter. + It's also possible to add additional nameservers by appending them in nameservers list. + + The default resolver can be overwritten with my_resolver.nameservers parameter. + + Written and contributed by @nawadanp + """ + def __init__(self): + self.domain = 'google.com' + self.lifetime = 0.3 + self.resolver = [] + self.nameservers = [] + + def ns_checker(self, i3status_output_json, i3status_config): + response = {'full_text': '', 'name': 'ns_checker'} + position = 0 + counter = 0 + error = False + nameservers = [] + + my_resolver = dns.resolver.Resolver() + my_resolver.lifetime = self.lifetime + if self.resolver: + my_resolver.nameservers = self.resolver + + my_ns = my_resolver.query(self.domain, 'NS') + + # Insert each NS ip address in nameservers + for ns in my_ns: + nameservers.append(str(socket.gethostbyname(str(ns)))) + for ns in self.nameservers: + nameservers.append(str(ns)) + + # Perform a simple DNS query, for each NS servers + for ns in nameservers: + my_resolver.nameservers = [ns] + counter += 1 + try: + my_resolver.query(self.domain, 'A') + except: + error = True + + if error: + response['full_text'] = str(counter) + ' NS NOK' + response['color'] = i3status_config['color_bad'] + else: + response['full_text'] = str(counter) + ' NS OK' + response['color'] = i3status_config['color_good'] + + return (position, response) diff --git a/py3status/modules/pingdom.py b/py3status/modules/pingdom.py new file mode 100644 index 0000000..cddf1a9 --- /dev/null +++ b/py3status/modules/pingdom.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import requests +from time import time + + +class Py3status: + """ + Dynamically display the latest response time of the configured checks using + the Pingdom API. + We also verify the status of the checks and colorize if needed. + Pingdom API doc : https://www.pingdom.com/services/api-documentation-rest/ + + #NOTE: This module needs the 'requests' python module from pypi + https://pypi.python.org/pypi/requests + """ + def pingdom_checks(self, json, i3status_config): + response = {'full_text': '', 'name': 'pingdom_checks'} + + #NOTE: configure me ! + APP_KEY = '' # create an APP KEY on pingdom first + CACHE_TIMEOUT = 600 # recheck every 10 mins + CHECKS = [] # checks' names you want added to your bar + LATENCY_THRESHOLD = 500 # when to colorize the output + LOGIN = '' # pingdom login + PASSWORD = '' # pingdom password + TIMEOUT = 15 + POSITION = 0 + + r = requests.get( + 'https://api.pingdom.com/api/2.0/checks', + auth=(LOGIN, PASSWORD), + headers={'App-Key': APP_KEY}, + timeout=TIMEOUT, + ) + result = r.json() + if 'checks' in result: + for check in [ + ck for ck in result['checks'] if ck['name'] in CHECKS + ]: + if check['status'] == 'up': + response['full_text'] += '{}: {}ms, '.format( + check['name'], + check['lastresponsetime'] + ) + if check['lastresponsetime'] > LATENCY_THRESHOLD: + response.update( + {'color': i3status_config['color_degraded']} + ) + else: + response['full_text'] += '{}: DOWN'.format( + check['name'], + check['lastresponsetime'] + ) + response.update({'color': i3status_config['color_bad']}) + response['full_text'] = response['full_text'].strip(', ') + response['cached_until'] = time() + CACHE_TIMEOUT + + return (POSITION, response) diff --git a/py3status/modules/pomodoro.py b/py3status/modules/pomodoro.py new file mode 100644 index 0000000..28b5ea1 --- /dev/null +++ b/py3status/modules/pomodoro.py @@ -0,0 +1,123 @@ +""" +Pomodoro countdown on i3bar originally written by @Fandekasp (Adrien Lemaire) +""" +from subprocess import call +from time import time + +MAX_BREAKS = 4 +POSITION = 0 +TIMER_POMODORO = 25 * 60 +TIMER_BREAK = 5 * 60 +TIMER_LONG_BREAK = 15 * 60 + + +class Py3status: + """ + """ + def __init__(self): + self.__setup('stop') + self.alert = False + self.run = False + + def on_click(self, json, i3status_config, event): + """ + Handles click events + """ + if event['button'] == 1: + if self.status == 'stop': + self.status = 'start' + self.run = True + + elif event['button'] == 2: + self.__setup('stop') + self.run = False + + elif event['button'] == 3: + self.__setup('pause') + self.run = False + + @property + def response(self): + """ + Return the response full_text string + """ + return { + 'full_text': '{} ({})'.format(self.prefix, self.timer), + 'name': 'pomodoro' + } + + def __setup(self, status): + """ + Setup a step + """ + self.status = status + if status == 'stop': + self.prefix = 'Pomodoro' + self.status = 'stop' + self.timer = TIMER_POMODORO + self.breaks = 1 + + elif status == 'start': + self.prefix = 'Pomodoro' + self.timer = TIMER_POMODORO + + elif status == 'pause': + self.prefix = 'Break #%d' % self.breaks + if self.breaks > MAX_BREAKS: + self.timer = TIMER_LONG_BREAK + self.breaks = 1 + else: + self.breaks += 1 + self.timer = TIMER_BREAK + + def __decrement(self): + """ + Countdown handler + """ + self.timer -= 1 + if self.timer < 0: + self.alert = True + self.run = False + self.__i3_nagbar() + if self.status == 'start': + self.__setup('pause') + self.status = 'pause' + elif self.status == 'pause': + self.__setup('start') + self.status = 'start' + + def __i3_nagbar(self, level='warning'): + """ + Make use of i3-nagbar to display warnings to the user. + """ + msg = '{} time is up !'.format(self.prefix) + try: + call( + ['i3-nagbar', '-m', msg, '-t', level], + stdout=open('/dev/null', 'w'), + stderr=open('/dev/null', 'w') + ) + except: + pass + + def pomodoro(self, json, i3status_config): + """ + Pomodoro response handling and countdown + """ + if self.run: + self.__decrement() + + response = self.response + if self.alert: + response['urgent'] = True + self.alert = False + + if self.status == 'start': + response['color'] = i3status_config['color_good'] + elif self.status == 'pause': + response['color'] = i3status_config['color_degraded'] + else: + response['color'] = i3status_config['color_bad'] + + response['cached_until'] = time() + return (POSITION, response) diff --git a/py3status/modules/scratchpad-counter.py b/py3status/modules/scratchpad-counter.py new file mode 100644 index 0000000..390745f --- /dev/null +++ b/py3status/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/py3status/modules/sysdata.py b/py3status/modules/sysdata.py new file mode 100644 index 0000000..fbbd157 --- /dev/null +++ b/py3status/modules/sysdata.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# sysdata + +# Sysdata is a module uses great Py3status (i3status wrapper) to +# display system information (RAM usage) in i3bar (Linux systems). +# For more information read: +# 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 +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import subprocess +from time import time + + +class GetData: + """Get system status + + """ + def execCMD(self, cmd, arg): + """Take a system command and its argument, then return the result. + + Arguments: + - `cmd`: system command. + - `arg`: argument. + """ + result = subprocess.check_output([cmd, arg]) + return result + + def cpu(self): + """Get the cpu usage data from /proc/stat : + cpu 2255 34 2290 22625563 6290 127 456 0 0 + - user: normal processes executing in user mode + - nice: niced processes executing in user mode + - system: processes executing in kernel mode + - idle: twiddling thumbs + - iowait: waiting for I/O to complete + - irq: servicing interrupts + - softirq: servicing softirqs + - steal: involuntary wait + - guest: running a normal guest + - guest_nice: running a niced guest + These numbers identify the amount of time the CPU has spent performing + different kinds of work. Time units are in USER_HZ (typically hundredths of a + second) + """ + with open('/proc/stat', 'r') as fd: + line = fd.readline() + cpu_data = line.split() + total_cpu_time = sum(map(int, cpu_data[1:])) + cpu_idle_time = int(cpu_data[4]) + + #return the cpu total&idle time + return total_cpu_time, cpu_idle_time + + def memory(self): + """Execute 'free -m' command, grab the memory capacity and used size + then return; Memory size 'total_mem', Used_mem, and percentage + of used memory. + + """ + # Run 'free -m' command and make a list from output. + mem_data = self.execCMD('free', '-m').split() + total_mem = int(mem_data[7]) / 1024. + used_mem = int(mem_data[15]) / 1024. + # Caculate percentage + used_mem_percent = int(used_mem / (total_mem / 100)) + + # Results are in kilobyte. + return total_mem, used_mem, used_mem_percent + + +class Py3status: + """ + System status in i3bar + """ + def __init__(self): + self.data = GetData() + self.cpu_total = 0 + self.cpu_idle = 0 + + def cpuInfo(self, json, i3status_config): + """calculate the CPU status and return it. + + """ + response = {'full_text': '', 'name': 'cpu_usage'} + cpu_total, cpu_idle = self.data.cpu() + used_cpu_percent = 1 - float(cpu_idle-self.cpu_idle)/float(cpu_total-self.cpu_total) + self.cpu_total = cpu_total + self.cpu_idle = cpu_idle + + if used_cpu_percent <= 40/100.0: + response['color'] = i3status_config['color_good'] + elif used_cpu_percent <= 75/100.0: + 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) + + #cache the status for 10 seconds + response['cached_until'] = time() + 10 + + return (0, response) + + def ramInfo(self, json, i3status_config): + """calculate the memory (RAM) status and return it. + + """ + response = {'full_text': '', 'name': 'ram_info'} + total_mem, used_mem, used_mem_percent = self.data.memory() + + if used_mem_percent <= 40: + response['color'] = i3status_config['color_good'] + elif used_mem_percent <= 75: + response['color'] = i3status_config['color_degraded'] + else: + response['color'] = i3status_config['color_bad'] + + response['full_text'] = "RAM: %.2f/%.2f GB (%d%%)" % \ + (used_mem, total_mem, used_mem_percent) + response['cached_until'] = time() + + return (0, response) diff --git a/py3status/modules/vnstat.py b/py3status/modules/vnstat.py new file mode 100644 index 0000000..100138c --- /dev/null +++ b/py3status/modules/vnstat.py @@ -0,0 +1,138 @@ +# -*- coding: utf8 -*- + +from __future__ import division # python2 compatibility +from time import time, sleep +from subprocess import check_output + + +""" +Module for displaying vnstat's statistics. +REQUIRE external program called "vnstat" installed and configured to work. + +@author shadowprince +@license Eclipse Public License +""" + +CACHED_TIME = 3*60 # update time (in seconds) +POSITION = 0 # bar position +STATISTICS_TYPE = "d" # d for daily, m for monthly + +""" +Coloring rules. + +If value is bigger that dict key, status string will turn to color, specified in the value. +Example: +COLORING = { + 800: "#dddd00", + 900: "#dd0000", +} +(0 - 800: white, 800-900: yellow, >900 - red) +""" +COLORING = {} + +""" +Format of status string. + +Placeholders: + total - total + up - upload + down - download +""" +FORMAT = "{total}" + +PRECISION = 1 # amount of numbers after dot +MULTIPLIER_TOP = 1024 # if value is greater, divide it with UNIT_MULTI and get next unit from UNITS +LEFT_ALIGN = 0 + +""" +Format of total, up and down placeholders under FORMAT. +As default, substitutes LEFT_ALIGN and PRECISION as %s and %s +Placeholders: + value - value (float) + unit - unit (string) + +""" +VALUE_FORMAT = "{value:%s.%sf} {unit}" % (LEFT_ALIGN, PRECISION) + +INITIAL_MULTI = 1024 # initial multiplier, if you want to get rid of first bytes, set to 1 to disable +UNIT_MULTI = 1024 # value to divide if rate is greater than MULTIPLIER_TOP +UNITS = ["kb", "mb", "gb", "tb", ] # list of units, first one - value/INITIAL_MULTI, second - value/1024, third - value/1024^2, etc... + + +def get_stat(): + """ + Get statistics from devfile in list of lists of words + """ + def filter_stat(): + for x in check_output(["vnstat", "--dumpdb"]).decode("utf-8").splitlines(): + if x.startswith("{};0;".format(STATISTICS_TYPE)): + return x + + try: + type, number, ts, rxm, txm, rxk, txk, fill = filter_stat().split(";") + except OSError as e: + print("Looks like you have'nt installed or configured vnstat!") + raise e + except ValueError: + raise RuntimeError("vnstat returned wrong output, maybe it's configured wrong or module is outdated") + + up = (int(txm) * 1024 + int(txk)) * 1024 + down = (int(rxm) * 1024 + int(rxk)) * 1024 + + return {"up": up, + "down": down, + "total": up+down, } + + +def divide_and_format(value): + """ + Divide a value and return formatted string + """ + value /= INITIAL_MULTI + for i, unit in enumerate(UNITS): + if value > MULTIPLIER_TOP: + value /= UNIT_MULTI + else: + break + + return VALUE_FORMAT.format(value=value, unit=unit) + + +class Py3status: + def __init__(self, *args, **kwargs): + self.last_stat = get_stat() + self.last_time = time() + self.last_interface = None + + def currentSpeed(self, json, i3status_config): + stat = get_stat() + + color = None + keys = list(COLORING.keys()) + keys.sort() + for k in keys: + if stat["total"] < k * 1024 * 1024: + break + else: + color = COLORING[k] + + response = { + 'transformed': True, + 'full_text': FORMAT.format( + total=divide_and_format(stat['total']), + up=divide_and_format(stat['up']), + down=divide_and_format(stat['down']),), + 'name': 'vnstat', + 'cached_until': time() + CACHED_TIME, + } + + if color: + response["color"] = color + + return POSITION, response + +if __name__ == "__main__": + x = Py3status() + while True: + print(x.currentSpeed(1, 1)) + sleep(1) diff --git a/py3status/modules/weather_yahoo.py b/py3status/modules/weather_yahoo.py new file mode 100644 index 0000000..09e1c72 --- /dev/null +++ b/py3status/modules/weather_yahoo.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" +Display current day + 3 days weather forecast as icons on your i3bar +Based on Yahoo! Weather. forecast, thanks guys ! + http://developer.yahoo.com/weather/ + +Find your city code using: + http://answers.yahoo.com/question/index?qid=20091216132708AAf7o0g + +The city_code in this example is for Paris, France => FRXX0076 +""" + +from time import time +import requests + + +class Py3status: + + # available configuration parameters + cache_timeout = 1800 + city_code = 'FRXX0076' + forecast_days = 3 + request_timeout = 10 + + def _get_forecast(self): + """ + Ask Yahoo! Weather. for a forecast + """ + q = requests.get( + 'http://query.yahooapis.com/v1/public/yql?q=' + + 'select item from weather.forecast ' + + 'where location="%s"&format=json' % self.city_code, + timeout=self.request_timeout + ) + + r = q.json() + status = q.status_code + forecasts = [] + + if status == 200: + forecasts = r['query']['results']['channel']['item']['forecast'] + # reset today + forecasts[0] = r['query']['results']['channel']['item']['condition'] + else: + raise Exception('got status {}'.format(status)) + + # return current today + forecast_days days forecast + return forecasts[:self.forecast_days + 1] + + def _get_icon(self, forecast): + """ + Return an unicode icon based on the forecast code and text + See: http://developer.yahoo.com/weather/#codes + """ + icons = ['☀', '☁', '☂', '☃', '?'] + code = int(forecast['code']) + text = forecast['text'].lower() + + # sun + if 'sun' in text or code in [31, 32, 33, 34, 36]: + code = 0 + + # cloud / early rain + elif 'cloud' in text or code in [ + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 44 + ]: + code = 1 + + # rain + elif 'rain' in text or code in [ + 0, 1, 2, 3, 4, 5, 6, 9, + 11, 12, + 37, 38, 39, + 40, 45, 47 + ]: + code = 2 + + # snow + elif 'snow' in text or code in [ + 7, 8, + 10, 13, 14, 15, 16, 17, 18, + 35, + 41, 42, 43, 46 + ]: + code = 3 + + # dunno + else: + code = -1 + + return icons[code] + + def weather_yahoo(self, json, i3status_config): + """ + This method gets executed by py3status + """ + response = { + 'cached_until': time() + self.cache_timeout, + 'full_text': '' + } + + forecasts = self._get_forecast() + for forecast in forecasts: + icon = self._get_icon(forecast) + response['full_text'] += '{} '.format(icon) + response['full_text'] = response['full_text'].strip() + + return response diff --git a/py3status/modules/whoami.py b/py3status/modules/whoami.py new file mode 100644 index 0000000..11ca9bf --- /dev/null +++ b/py3status/modules/whoami.py @@ -0,0 +1,29 @@ +""" +Simply output the currently logged in user in i3bar. + +Inspired by i3 FAQ: + https://faq.i3wm.org/question/1618/add-user-name-to-status-bar/ +""" + +from getpass import getuser +from time import time + + +class Py3status: + + # available configuration parameters + cache_timeout = 1800 + + def whoami(self, i3status_output_json, i3status_config): + """ + We use the getpass module to get the current user. + """ + # here you can change the format of the output + # default is just to show the username + username = '{}'.format(getuser()) + + response = { + 'cached_until': time() + self.cache_timeout, + 'full_text': username + } + return response diff --git a/py3status/modules/window-title.py b/py3status/modules/window-title.py new file mode 100644 index 0000000..5096bd3 --- /dev/null +++ b/py3status/modules/window-title.py @@ -0,0 +1,60 @@ +""" +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 +""" + +import i3 +from time import time + + +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: + + # available configuration parameters + cache_timeout = 0.5 + max_width = 120 # if width of title is greater, shrink it and add '...' + + 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']) > self.max_width + and "..." + window['name'][-(self.max_width-3):] + or window['name'] + ) + transformed = True + + response = { + 'cached_until': time() + self.cache_timeout, + 'full_text': self.text, + 'transformed': transformed + } + return response |
