summaryrefslogtreecommitdiffstats
path: root/py3status
diff options
context:
space:
mode:
authorUltrabug <ultrabug@gentoo.org>2013-04-07 15:10:52 +0200
committerUltrabug <ultrabug@gentoo.org>2013-04-07 15:10:52 +0200
commitb1683acaef4c2efe08bd06c9818c89a1b5a98af5 (patch)
tree8ab3f31133c4db8484b35718dd714a81537cde52 /py3status
parentba3beddbfc006f0325b24bbab8d1d2081e037162 (diff)
major cleanup, better threading of i3status, enhanced main logic for better responsiveness, drop useless variables, respect PEP8 identation, more PEP8 variable cleanup
Diffstat (limited to 'py3status')
-rwxr-xr-xpy3status/__init__.py580
1 files changed, 321 insertions, 259 deletions
diff --git a/py3status/__init__.py b/py3status/__init__.py
index 3e794e9..a021356 100755
--- a/py3status/__init__.py
+++ b/py3status/__init__.py
@@ -30,6 +30,7 @@ import imp
import sys
import argparse
import threading
+from threading import Thread
from json import loads
from json import dumps
@@ -52,301 +53,362 @@ from syslog import LOG_ERR
from syslog import LOG_INFO
try:
- # python3
- from queue import Queue
- from queue import Empty
+ # python3
+ from queue import Queue
+ from queue import Empty
except ImportError:
- # python2
- from Queue import Queue
- from Queue import Empty
+ # python2
+ from Queue import Queue
+ from Queue import Empty
+
+# module globals and defaults
+################################################################################
+CACHE_TIMEOUT = 60
+DISABLE_TRANSFORM = False
+I3STATUS_CONFIG = '/etc/i3status.conf'
+INCLUDE_PATH = '.i3/py3status'
+INTERVAL = 1
+USER_CACHE = {}
+USER_CLASSES = {}
# functions
################################################################################
def print_line(message):
- """
- Non-buffered printing to stdout
- """
- sys.stdout.write(message + '\n')
- sys.stdout.flush()
+ """
+ Non-buffered printing to stdout
+ """
+ sys.stdout.write(message + '\n')
+ sys.stdout.flush()
def read_line():
- """
- Interrupted respecting reader for stdin
- """
- try:
- line = sys.stdin.readline().strip()
- # i3status sends EOF, or an empty line
- if not line:
- sys.exit(3)
- return line
- except KeyboardInterrupt:
- sys.exit()
+ """
+ Interrupted respecting reader for stdin
+ """
+ try:
+ line = sys.stdin.readline().strip()
+ # i3status sends EOF, or an empty line
+ if not line:
+ sys.exit(3)
+ return line
+ except KeyboardInterrupt:
+ sys.exit()
+
+def i3status_config_reader(config_file):
+ """
+ i3status.conf reader so we can adapt our code to the i3status config
+ """
+ in_time = False
+ in_general = False
+ config = {
+ 'colors': False,
+ 'color_good': None,
+ 'color_bad' : None,
+ 'color_degraded' : None,
+ 'color_separator': None,
+ 'interval': 5,
+ 'output_format': None,
+ 'time_format': '%Y-%m-%d %H:%M:%S',
+ }
+ for line in open(config_file, 'r'):
+ line = line.strip(' \t\n\r')
+ if line.startswith('general'):
+ in_general = True
+ elif line.startswith('time'):
+ in_time = True
+ elif line.startswith('}'):
+ in_general = False
+ in_time = False
+ if in_general and '=' in line:
+ key, value = line.split('=')[0].strip(), line.split('=')[1].strip()
+ if key in config:
+ if value in ['true', 'false']:
+ value = 'True' if value == 'true' else 'False'
+ config[key] = eval(value)
+ if in_time and '=' in line:
+ key, value = line.split('=')[0].strip(), line.split('=')[1].strip()
+ if 'time_' + key in config:
+ config['time_' + key] = eval(value)
+ return config
-def i3status_config_reader():
- """
- i3status.conf reader so we can adapt our code to the i3status config
- """
- in_time = False
- in_general = False
- config = {
- 'colors': False,
- 'color_good': None,
- 'color_bad' : None,
- 'color_degraded' : None,
- 'color_separator': None,
- 'interval': 5,
- 'output_format': None,
- 'time_format': '%Y-%m-%d %H:%M:%S',
- }
- for line in open(I3STATUS_CONFIG_FILE, 'r'):
- line = line.strip(' \t\n\r')
- if line.startswith('general'):
- in_general = True
- elif line.startswith('time'):
- in_time = True
- elif line.startswith('}'):
- in_general = False
- in_time = False
- if in_general and '=' in line:
- key, value = line.split('=')[0].strip(), line.split('=')[1].strip()
- if key in config:
- if value in ['true', 'false']:
- value = 'True' if value == 'true' else 'False'
- config[key] = eval(value)
- if in_time and '=' in line:
- key, value = line.split('=')[0].strip(), line.split('=')[1].strip()
- if 'time_' + key in config:
- config['time_' + key] = eval(value)
- return config
+class I3status(Thread):
+ """
+ Run i3status in a thread and send its output to a Queue
+ """
+ def __init__(self, config_file):
+ """
+ set our useful properties
+ """
+ Thread.__init__(self)
+ self.config_file = config_file
+ self.kill = False
+ self.queue = Queue()
+ self.init = True
+ self.started = False
-def i3status(message_queue):
- """
- Execute i3status in a thread and send its output to a Queue to py3status
- """
- i3status_pipe = Popen(['i3status', '-c', I3STATUS_CONFIG_FILE], stdout=PIPE, stderr=PIPE)
- message_queue.put(i3status_pipe.stdout.readline())
- message_queue.put(i3status_pipe.stdout.readline())
- while True:
- line = i3status_pipe.stdout.readline()
- if len(line) > 0:
- message_queue.put(line)
- else:
- break
+ def stop(self):
+ """
+ break the i3status loop
+ """
+ self.kill = True
+
+ def run(self):
+ """
+ run i3status and queue its output
+ """
+ i3status_pipe = Popen(
+ ['i3status', '-c', self.config_file],
+ stdout=PIPE,
+ stderr=PIPE,
+ )
+ self.queue.put(i3status_pipe.stdout.readline())
+ self.queue.put(i3status_pipe.stdout.readline())
+ while not self.kill:
+ line = i3status_pipe.stdout.readline()
+ if len(line) > 0:
+ self.queue.put(line)
+ try:
+ line = line.decode()
+ except Exception:
+ pass
+ if len(line) > 1 and line.startswith('['):
+ self.init = False
+ elif line.startswith(',['):
+ self.started = True
+ else:
+ break
def process_line(line, **kwargs):
- """
- Main line processor logic
- """
- if line.startswith('{') and 'version' in line:
- print_line(line.strip('\n'))
- elif line == '[\n':
- print_line(line.strip('\n'))
- else:
- prefix = ''
- if line.startswith(','):
- line, prefix = line[1:], ','
- elif kwargs['delta'] > 0:
- prefix = ','
+ """
+ Main line processor logic
+ """
+ if line.startswith('{') and 'version' in line:
+ print_line(line.strip('\n'))
+ elif line == '[\n':
+ print_line(line.strip('\n'))
+ else:
+ prefix = ''
+ if line.startswith(','):
+ line, prefix = line[1:], ','
+ elif kwargs['delta'] > 0:
+ prefix = ','
- # integrated transformations
- if not DISABLE_TRANSFORM:
- j = transform(loads(line), **kwargs)
- else:
- j = loads(line)
+ # integrated transformations
+ if not DISABLE_TRANSFORM:
+ j = transform(loads(line), **kwargs)
+ else:
+ j = loads(line)
- # user-based injection and transformation
- j = inject(j)
+ # user-based injection and transformation
+ j = inject(j)
- print_line(prefix+dumps(j))
+ output = prefix+dumps(j)
+ print_line(output)
+ return output
def inject(j):
- """
- Run on every user class included and execute every method on the json,
- then inject the result at the start of the json
- """
- # inject our own functions' results
- for fn in sorted( USER_CLASSES.keys() ):
- my_class, my_methods = USER_CLASSES[fn]
- for my_method in my_methods:
- try:
- # handle a cache on user class methods results
- try:
- index, result = USER_CACHE[my_method]
- if time() > result['cached_until']:
- raise KeyError('cache timeout')
- except KeyError:
- # execute the method
- try:
- meth = getattr(my_class, my_method)
- index, result = meth(j, I3STATUS_CONFIG)
- except Exception:
- err = sys.exc_info()[1]
- syslog(LOG_ERR, "user method %s failed (%s)" % (my_method, str(err)))
- index, result = (0, {'name': '', 'full_text': ''})
+ """
+ Run on every user class included and execute every method on the json,
+ then inject the result at the start of the json
+ """
+ # inject our own functions' results
+ for class_name in sorted( USER_CLASSES.keys() ):
+ my_class, my_methods = USER_CLASSES[class_name]
+ for my_method in my_methods:
+ try:
+ # handle a cache on user class methods results
+ try:
+ index, result = USER_CACHE[my_method]
+ if time() > result['cached_until']:
+ raise KeyError('cache timeout')
+ except KeyError:
+ # execute the method
+ try:
+ meth = getattr(my_class, my_method)
+ index, result = meth(j, I3STATUS_CONFIG)
+ except Exception:
+ err = sys.exc_info()[1]
+ syslog(LOG_ERR, "user method %s failed (%s)" \
+ % (my_method, str(err)))
+ index, result = (0, {'name': '', 'full_text': ''})
- # respect user-defined cache timeout for this module
- if 'cached_until' not in result:
- result['cached_until'] = time() + CACHE_TIMEOUT
+ # respect user-defined cache timeout for this module
+ if 'cached_until' not in result:
+ result['cached_until'] = time() + CACHE_TIMEOUT
- # validate the response
- assert isinstance(result, dict), "user method didn't return a dict"
- assert 'full_text' in result, "missing 'full_text' key"
- assert 'name' in result, "missing 'name' key"
- finally:
- USER_CACHE[my_method] = (index, result)
- j.insert(index, result)
- except Exception:
- err = sys.exc_info()[1]
- syslog(LOG_ERR, "injection failed (%s)" % str(err))
- return j
+ # validate the response
+ assert isinstance(result, dict), "user should return a dict"
+ assert 'full_text' in result, "missing 'full_text' key"
+ assert 'name' in result, "missing 'name' key"
+ finally:
+ USER_CACHE[my_method] = (index, result)
+ j.insert(index, result)
+ except Exception:
+ err = sys.exc_info()[1]
+ syslog(LOG_ERR, "injection failed (%s)" % str(err))
+ return j
def transform(j, **kwargs):
- """
- Integrated transformations:
- - update the 'time' object so that it's updated at INTERVAL seconds
- - update the 'run_watch' objects so that we rely on the color instead of useless 'yes' or 'no' status
- """
- try:
- for item in j:
- # time modification
- if item['name'] in [ 'time', 'tztime' ]:
- date = datetime.strptime(item['full_text'], I3STATUS_CONFIG['time_format']) + timedelta(seconds=kwargs['delta'])
- item['full_text'] = date.strftime(I3STATUS_CONFIG['time_format'])
- except Exception:
- err = sys.exc_info()[1]
- syslog(LOG_ERR, "transformation failed (%s)" % (str(err)))
- return j
+ """
+ Integrated transformations:
+ - update the 'time' object so that it's updated at INTERVAL seconds
+ """
+ try:
+ for item in j:
+ # time modification
+ if item['name'] in [ 'time', 'tztime' ]:
+ time_format = I3STATUS_CONFIG['time_format']
+ date = datetime.strptime( item['full_text'], time_format ) \
+ + timedelta(seconds=kwargs['delta'])
+ item['full_text'] = date.strftime(time_format)
+ if kwargs['delta'] > 0:
+ item['transformed'] = True
+ except Exception:
+ err = sys.exc_info()[1]
+ syslog(LOG_ERR, "transformation failed (%s)" % (str(err)))
+ return j
def load_from_file(filepath):
- """
- Load Py3status user class for later injection
- """
- inst = None
- expected_class = 'Py3status'
- mod_name, file_ext = os.path.splitext(os.path.split(filepath)[-1])
- if file_ext.lower() == '.py':
- py_mod = imp.load_source(mod_name, filepath)
- if hasattr(py_mod, expected_class):
- inst = py_mod.Py3status()
- return (mod_name, inst)
+ """
+ Load Py3status user class for later injection
+ """
+ inst = None
+ expected_class = 'Py3status'
+ mod_name, file_ext = os.path.splitext(os.path.split(filepath)[-1])
+ if file_ext.lower() == '.py':
+ py_mod = imp.load_source(mod_name, filepath)
+ if hasattr(py_mod, expected_class):
+ inst = py_mod.Py3status()
+ return (mod_name, inst)
# main stuff
################################################################################
def main():
- try:
- # global definition
- global CACHE_TIMEOUT, DISABLE_TRANSFORM, I3STATUS_CONFIG_FILE
- global I3STATUS_CONFIG, INCLUDE_PATH, INTERVAL, USER_CACHE, USER_CLASSES
+ """
+ Main logic function
+ """
+ try:
+ # global definition
+ global CACHE_TIMEOUT, DISABLE_TRANSFORM
+ global I3STATUS_CONFIG, INCLUDE_PATH, INTERVAL, USER_CACHE, USER_CLASSES
+
+ # command line options
+ parser = argparse.ArgumentParser(
+ description='The agile, python-powered, i3status wrapper')
+ parser = argparse.ArgumentParser(add_help=True)
+ parser.add_argument('-c', action="store",
+ dest="i3status_conf", type=str,
+ default=I3STATUS_CONFIG, help="path to i3status config file")
+ parser.add_argument('-d', action="store_true",
+ dest="disable_transform", help="disable integrated transformations")
+ parser.add_argument('-i', action="store",
+ dest="include_path", type=str,
+ default=INCLUDE_PATH, help="user-based class include directory")
+ parser.add_argument('-n', action="store",
+ dest="interval", type=int,
+ default=INTERVAL, help="update interval in seconds (default 1 sec)")
+ parser.add_argument('-t', action="store",
+ dest="cache_timeout", type=int, default=CACHE_TIMEOUT,
+ help="default injection cache timeout in seconds (default 60 sec)")
+ options = parser.parse_args()
- # command line options
- PARSER = argparse.ArgumentParser(description='The agile, python-powered, i3status wrapper')
- PARSER = argparse.ArgumentParser(add_help=True)
- PARSER.add_argument('-c', action="store", dest="i3status_conf", type=str, default="/etc/i3status.conf", help="path to i3status config file")
- PARSER.add_argument('-d', action="store_true", dest="disable_transform", help="disable integrated transformations")
- PARSER.add_argument('-i', action="store", dest="include_path", type=str, default='.i3/py3status', help="user-based class include directory")
- PARSER.add_argument('-n', action="store", dest="interval", type=int, default=1, help="polling interval in seconds (default 1 sec)")
- PARSER.add_argument('-t', action="store", dest="cache_timeout", type=int, default=60, help="injection cache timeout in seconds (default 60 sec)")
- OPTS = PARSER.parse_args()
+ # configuration and helper variables
+ CACHE_TIMEOUT = options.cache_timeout
+ DISABLE_TRANSFORM = True if options.disable_transform else False
+ I3STATUS_CONFIG = i3status_config_reader(options.i3status_conf)
+ INCLUDE_PATH = os.path.abspath( options.include_path ) + '/'
+ INTERVAL = options.interval
- # configuration and helper variables
- CACHE_TIMEOUT = OPTS.cache_timeout
- DELTA = 0
- DISABLE_TRANSFORM = True if OPTS.disable_transform else False
- FORCED = False
- I3STATUS_CONFIG_FILE = OPTS.i3status_conf
- I3STATUS_CONFIG = i3status_config_reader()
- INCLUDE_PATH = os.path.abspath( OPTS.include_path ) + '/'
- INTERVAL = OPTS.interval
- STARTED = False
+ # py3status uses only the i3bar protocol
+ assert I3STATUS_CONFIG['output_format'] == 'i3bar', \
+ 'i3status output_format should be set to "i3bar"'
+ except Exception:
+ err = sys.exc_info()[1]
+ syslog(LOG_ERR, "py3status init error (%s)" % str(err))
+ sys.exit(1)
- # py3status uses only the i3bar protocol
- assert I3STATUS_CONFIG['output_format'] == 'i3bar', 'unsupported output_format'
+ try:
+ USER_CACHE = {}
+ USER_CLASSES = {}
+ # read user-written Py3status class files for dynamic inclusion
+ if INCLUDE_PATH and os.path.isdir(INCLUDE_PATH):
+ for file_name in os.listdir(INCLUDE_PATH):
+ module, class_inst = load_from_file(INCLUDE_PATH + file_name)
+ if module and class_inst:
+ USER_CLASSES[file_name] = (class_inst, [])
+ for method in dir(class_inst):
+ if not method.startswith('__'):
+ USER_CLASSES[file_name][1].append(method)
- # read user-written Py3status class files for dynamic inclusion
- USER_CACHE = {}
- USER_CLASSES = {}
- if INCLUDE_PATH and os.path.isdir(INCLUDE_PATH):
- for fn in os.listdir(INCLUDE_PATH):
- module, class_inst = load_from_file(INCLUDE_PATH + fn)
- if module and class_inst:
- USER_CLASSES[fn] = (class_inst, [])
- for method in dir(class_inst):
- if not method.startswith('__'):
- USER_CLASSES[fn][1].append(method)
+ # spawn a i3status process on a separate thread
+ # we will receive its output on a Queue which we will poll for messages
+ i3status_thread = I3status(options.i3status_conf)
+ i3status_thread.start()
- # spawn a i3status process on a separate thread
- # we will receive its output on a Queue which we will poll for messages
- MESSAGE_QUEUE = Queue()
- I3STATUS_THREAD = threading.Thread(target=i3status, name='i3status', args=(MESSAGE_QUEUE, ))
- I3STATUS_THREAD.start()
+ # SIGUSR1 forces a refresh of the bar both for py3status and i3status,
+ # this mimics the USR1 signal handling of i3status (see man i3status)
+ def sig_handler(signum, frame):
+ """
+ Raise a Warning level exception when a user sends a SIGUSR1 signal
+ """
+ raise UserWarning("received USR1, forcing refresh")
+ signal(SIGUSR1, sig_handler)
- # SIGUSR1 forces a refresh of the bar both for py3status and i3status,
- # this mimics the USR1 signal handling of i3status (see man i3status)
- def sig_handler(signum, frame):
- """
- Raise a Warning level exception when a user sends a SIGUSR1 signal
- """
- raise UserWarning("received USR1, forcing refresh")
- signal(SIGUSR1, sig_handler)
+ # control variables
+ forced = False
- # main loop
- while True:
- try:
- # get a timestamp of now
- TS = time()
+ # main loop
+ while True:
+ try:
+ # get a timestamp of now
+ tst = time()
- # try to read i3status' output for at most INTERVAL seconds else
- # raise the Empty exception
- LINE = MESSAGE_QUEUE.get(timeout=INTERVAL)
- try:
- # python3 compatibility code
- LINE = LINE.decode()
- except UnicodeDecodeError:
- pass
+ # try to read i3status' output for at most INTERVAL seconds else
+ # raise the Empty exception
+ line = i3status_thread.queue.get(timeout=INTERVAL)
+ try:
+ # python3 compatibility code
+ line = line.decode()
+ except UnicodeDecodeError:
+ pass
- # i3status first output lines should be processed asap as only
- # the following lines will be processed by py3status
- if LINE.startswith(',['):
- if not FORCED and I3STATUS_CONFIG['interval'] > INTERVAL:
- # add a calculated sleep honoring py3status refresh
- # time of the bar every INTERVAL seconds
- sleep( INTERVAL - float( '{:.2}'.format( time()-TS ) ) )
- else:
- # reset the SIGUSR1 flag forcing the refresh of the bar
- FORCED = False
- # flag i3status as started
- STARTED = True
+ # i3status first output lines should be processed asap as only
+ # the following lines will be processed by py3status
+ if i3status_thread.started:
+ if not forced and I3STATUS_CONFIG['interval'] > INTERVAL:
+ # add a calculated sleep honoring py3status refresh
+ # time of the bar every INTERVAL seconds
+ sleep(INTERVAL - float( '{:.2}'.format( time()-tst ) ))
+ else:
+ # reset the SIGUSR1 flag forcing the refresh of the bar
+ forced = False
- # process the output line and reset the time DELTA
- process_line(LINE, delta=0)
- DELTA = 0
- except Empty:
- # make sure i3status has started before modifying its output
- if STARTED:
- # the DELTA helps us adjust the clock without a syscall
- DELTA += INTERVAL
- process_line(LINE, delta=DELTA)
- if threading.active_count() < 2:
- # i3status died ? oups
- break
- else:
- syslog(LOG_INFO, "waiting for i3status")
- except UserWarning:
- # SIGUSR1 was received, we also force i3status to refresh by
- # sending it a SIGUSR1 as well then we refresh the bar asap
- msg = sys.exc_info()[1]
- syslog(LOG_INFO, str(msg))
- call(["killall", "-s", "USR1", "i3status"])
- USER_CACHE = {}
- FORCED = True
- except KeyboardInterrupt:
- break
+ # process the output line straight from i3status
+ process_line(line, delta=0)
+ except Empty:
+ # make sure i3status has started before modifying its output
+ if i3status_thread.started or not i3status_thread.init:
+ line = process_line(line, delta=INTERVAL)
+ if threading.active_count() < 2:
+ # i3status died ? oups
+ break
+ else:
+ syslog(LOG_INFO, "waiting for i3status")
+ except UserWarning:
+ # SIGUSR1 was received, we also force i3status to refresh by
+ # sending it a SIGUSR1 as well then we refresh the bar asap
+ msg = sys.exc_info()[1]
+ syslog(LOG_INFO, str(msg))
+ call(["killall", "-s", "USR1", "i3status"])
+ USER_CACHE = {}
+ forced = True
+ except KeyboardInterrupt:
+ break
- I3STATUS_THREAD.join()
- except Exception:
- err = sys.exc_info()[1]
- syslog(LOG_ERR, "py3status error (%s)" % str(err))
- sys.exit(1)
+ i3status_thread.stop()
+ except Exception:
+ err = sys.exc_info()[1]
+ syslog(LOG_ERR, "py3status error (%s)" % str(err))
+ sys.exit(2)
if __name__ == '__main__':
- main()
+ main()