def run(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) syslog(LOG_INFO, 'i3status spawned using config file {}'.format( tmpfile.name)) 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) self.tmpfile_path = tmpfile.name try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: if line.startswith('[{'): print_line(line) with jsonify(line) as (prefix, json_list): self.last_output = json_list self.last_prefix = ',' self.set_responses(json_list) self.ready = True elif not line.startswith(','): if 'version' in line: header = loads(line) header.update({'click_events': True}) line = dumps(header) print_line(line) else: with jsonify(line) as (prefix, json_list): self.last_output = json_list self.last_prefix = prefix self.set_responses(json_list) else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err except OSError: # we cleanup the tmpfile ourselves so when the delete will occur # it will usually raise an OSError: No such file or directory pass
def spawn_i3status(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix="py3status_") as tmpfile: self.write_tmp_i3status_config(tmpfile) i3status_pipe = Popen( [self.i3status_path, "-c", tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGTSTP signal for this subprocess preexec_fn=lambda: signal(SIGTSTP, SIG_IGN), ) self.py3_wrapper.log( "i3status spawned using config file {}".format(tmpfile.name) ) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.py3_wrapper.running: line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ",": line = line[1:] if line.startswith("[{"): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = "i3status died" if err: msg += " and said: {}".format(err) else: msg += " with code {}".format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err self.py3_wrapper.log(err, "error") except OSError: self.error = "Problem starting i3status maybe it is not installed" except Exception: self.py3_wrapper.report_exception("", notify_user=True) self.i3status_pipe = None
def spawn_i3status(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) self.py3_wrapper.log( 'i3status spawned using config file {}'.format( tmpfile.name)) i3status_pipe = Popen( ['i3status', '-c', tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGTSTP signal for this subprocess preexec_fn=lambda: signal(SIGTSTP, SIG_IGN) ) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ',': line = line[1:] if line.startswith('[{'): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err self.py3_wrapper.log(err, 'error') except Exception: err = sys.exc_info()[1] self.error = err self.py3_wrapper.log(err, 'error') self.i3status_pipe = None
def run(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) syslog(LOG_INFO, 'i3status spawned using config file {}'.format( tmpfile.name)) i3status_pipe = Popen( ['i3status', '-c', tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGUSR2 signal for this subprocess preexec_fn=lambda: signal(SIGUSR2, SIG_IGN) ) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ',': line = line[1:] if line.startswith('[{'): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err except OSError: # we cleanup the tmpfile ourselves so when the delete will occur # it will usually raise an OSError: No such file or directory pass self.i3status_pipe = None
class I3status(Thread): """ This class is responsible for spawning i3status and reading its output. """ def __init__(self, py3_wrapper): """ Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) self.error = None self.i3modules = {} self.i3status_module_names = [ "battery", "cpu_temperature", "cpu_usage", "ddate", "disk", "ethernet", "ipv6", "load", "path_exists", "run_watch", "time", "tztime", "volume", "wireless", ] self.i3status_pipe = None self.i3status_path = py3_wrapper.config["i3status_path"] self.json_list = None self.json_list_ts = None self.last_output = None self.last_refresh_ts = time() self.lock = py3_wrapper.lock self.new_update = False self.py3_config = py3_wrapper.config["py3_config"] self.py3_wrapper = py3_wrapper self.ready = False self.standalone = py3_wrapper.config["standalone"] self.time_modules = [] self.tmpfile_path = None self.update_due = 0 # the update interval is useful to know self.update_interval = self.py3_wrapper.get_config_attribute( "general", "interval" ) # do any initialization self.setup() def setup(self): """ Do any setup work needed to run i3status modules """ for conf_name in self.py3_config["i3s_modules"]: module = I3statusModule(conf_name, self) self.i3modules[conf_name] = module if module.is_time_module: self.time_modules.append(module) def valid_config_param(self, param_name, cleanup=False): """ Check if a given section name is a valid parameter for i3status. """ if cleanup: valid_config_params = [ _ for _ in self.i3status_module_names if _ not in ["cpu_usage", "ddate", "ipv6", "load", "time"] ] else: valid_config_params = self.i3status_module_names + ["general", "order"] return param_name.split(" ")[0] in valid_config_params def set_responses(self, json_list): """ Set the given i3status responses on their respective configuration. """ self.update_json_list() updates = [] for index, item in enumerate(self.json_list): conf_name = self.py3_config["i3s_modules"][index] module = self.i3modules[conf_name] if module.update_from_item(item): updates.append(conf_name) if updates: self.py3_wrapper.notify_update(updates) def update_json_list(self): """ Copy the last json list output from i3status so that any module can modify it without altering the original output. This is done so that any module's alteration of a i3status output json will not be overwritten when the next i3status output gets polled. """ self.json_list = deepcopy(self.last_output) @staticmethod def write_in_tmpfile(text, tmpfile): """ Write the given text in the given tmpfile in python2 and python3. """ try: tmpfile.write(text) except TypeError: tmpfile.write(str.encode(text)) except UnicodeEncodeError: tmpfile.write(text.encode("utf-8")) 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'. """ # order += ... for module in self.py3_config["i3s_modules"]: self.write_in_tmpfile('order += "%s"\n' % module, tmpfile) self.write_in_tmpfile("\n", tmpfile) # config params for general section and each module for section_name in ["general"] + self.py3_config["i3s_modules"]: section = self.py3_config[section_name] self.write_in_tmpfile("%s {\n" % section_name, tmpfile) for key, value in section.items(): # don't include color values except in the general section if key.startswith("color"): if ( section_name.split(" ")[0] not in I3S_COLOR_MODULES or key not in I3S_ALLOWED_COLORS ): continue # Set known fixed format for time and tztime so we can work # out the timezone if section_name.split()[0] in TIME_MODULES: if key == "format": value = TZTIME_FORMAT if key == "format_time": continue if isinstance(value, bool): value = "{}".format(value).lower() self.write_in_tmpfile(' %s = "%s"\n' % (key, value), tmpfile) self.write_in_tmpfile("}\n\n", tmpfile) tmpfile.flush() def suspend_i3status(self): # Put i3status to sleep if self.i3status_pipe: self.i3status_pipe.send_signal(SIGSTOP) def refresh_i3status(self): # refresh i3status. This is rate limited if time() > (self.last_refresh_ts + 0.1): if self.py3_wrapper.config["debug"]: self.py3_wrapper.log("refreshing i3status") if self.i3status_pipe: self.i3status_pipe.send_signal(SIGUSR1) self.last_refresh_ts = time() @profile def run(self): # if the i3status process dies we want to restart it. # We give up restarting if we have died too often for x in range(10): if not self.py3_wrapper.running: break self.spawn_i3status() # check if we never worked properly and if so quit now if not self.ready: break # limit restart rate self.lock.wait(5) def spawn_i3status(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix="py3status_") as tmpfile: self.write_tmp_i3status_config(tmpfile) i3status_pipe = Popen( [self.i3status_path, "-c", tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGTSTP signal for this subprocess preexec_fn=lambda: signal(SIGTSTP, SIG_IGN), ) self.py3_wrapper.log( "i3status spawned using config file {}".format(tmpfile.name) ) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.py3_wrapper.running: line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ",": line = line[1:] if line.startswith("[{"): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = "i3status died" if err: msg += " and said: {}".format(err) else: msg += " with code {}".format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err self.py3_wrapper.log(err, "error") except OSError: self.error = "Problem starting i3status maybe it is not installed" except Exception: self.py3_wrapper.report_exception("", notify_user=True) self.i3status_pipe = None def mock(self): """ Mock i3status behavior, used in standalone mode. """ # mock thread is_alive() method self.is_alive = lambda: True
class I3status(Thread): """ This class is responsible for spawning i3status and reading its output. """ def __init__(self, py3_wrapper): """ Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) 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.i3modules = {} self.json_list = None self.json_list_ts = None self.last_output = None self.lock = py3_wrapper.lock self.new_update = False self.py3_wrapper = py3_wrapper self.ready = False self.standalone = py3_wrapper.config['standalone'] self.time_modules = [] self.tmpfile_path = None # config_path = py3_wrapper.config['i3status_config_path'] self.config = self.i3status_config_reader(config_path) def update_times(self): """ Update time for any i3status time/tztime items. """ updated = [] for module in self.i3modules.values(): if module.is_time_module: if module.update_time_value(): updated.append(module.module_name) if updated: # trigger the update so new time is shown self.py3_wrapper.notify_update(updated) def valid_config_param(self, param_name, cleanup=False): """ Check if a given section name is a valid parameter for i3status. """ if cleanup: valid_config_params = [ _ for _ in self.i3status_module_names if _ not in ['cpu_usage', 'ddate', 'ipv6', 'load', 'time'] ] else: valid_config_params = self.i3status_module_names + [ 'general', 'order' ] return param_name.split(' ')[0] in valid_config_params @staticmethod def eval_config_parameter(param): """ Try to evaluate the given parameter as a string or integer and return it properly. This is used to parse i3status configuration parameters such as 'disk "/home" {}' or worse like '"cpu_temperature" 0 {}'. """ params = param.split(' ') result_list = list() for p in params: try: e_value = eval(p) if isinstance(e_value, str) or isinstance(e_value, int): p = str(e_value) else: raise ValueError() except (NameError, SyntaxError, ValueError): pass finally: result_list.append(p) return ' '.join(result_list) @staticmethod def eval_config_value(value): """ Try to evaluate the given parameter as a string or integer and return it properly. This is used to parse i3status configuration parameters such as 'disk "/home" {}' or worse like '"cpu_temperature" 0 {}'. """ if value.lower() in ('true', 'false'): return eval(value.title()) try: e_value = eval(value) if isinstance(e_value, str): if e_value.lower() in ('true', 'false'): value = eval(e_value.title()) else: value = e_value elif isinstance(e_value, int): value = e_value else: raise ValueError() except (NameError, ValueError): pass finally: return value def i3status_config_reader(self, i3status_config_path): """ Parse i3status.conf so we can adapt our code to the i3status config. """ config = { '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': [], '.group_extras': [], # extra i3status modules needed by groups '.module_groups': {}, # record groups that modules are in 'py3_modules': [] } # some ugly parsing in_section = False section_name = '' group_name = None 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 and line.startswith('group'): group_name = line.split('{')[0].strip() config[group_name] = {'items': []} continue if not in_section and group_name and line == '}': group_name = None continue if group_name and not in_section and '=' in line: # check this is not a section definition if '{' not in line or line.index('{') > line.index('='): key = line.split('=', 1)[0].strip() key = self.eval_config_parameter(key) value = line.split('=', 1)[1].strip() value = self.eval_config_value(value) if not key.startswith('on_click'): config[group_name][key] = value else: # on_click special parameters try: 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 group {}'.format(group_name)) except ValueError as e: raise ValueError('invalid "button id" ' 'for "on_click" parameter ' 'in group {} ({})'.format( group_name, e)) on_c = config['on_click'] on_c[group_name] = on_c.get(group_name, {}) on_c[group_name][button] = value continue if not in_section: section_name = line.split('{')[0].strip() section_name = self.eval_config_parameter(section_name) if not section_name: continue else: in_section = True if section_name not in config: config[section_name] = {} if group_name: # update the items in the group config[group_name]['items'].append(section_name) section = config['.module_groups'].setdefault(section_name, []) if group_name not in section: section.append(group_name) if not self.valid_config_param(section_name): # py3status module add a reference to the group and # make sure we have it in the list of modules to # run if section_name not in config['py3_modules']: config['py3_modules'].append(section_name) else: # i3status module. Add to the list of needed # modules and add to the `.group-extras` config to # ensure that it gets run even though not in # `order` config if section_name not in config['i3s_modules']: config['i3s_modules'].append(section_name) if section_name not in config['.group_extras']: config['.group_extras'].append(section_name) if '{' in line: in_section = True if section_name and '=' in line: section_line = line # one liner cases if line.endswith('}'): section_line = section_line.split('}', -1)[0].strip() if line.startswith(section_name + ' {'): section_line = section_line.split(section_name + ' {')[ 1].strip() key = section_line.split('=', 1)[0].strip() key = self.eval_config_parameter(key) value = section_line.split('=', 1)[1].strip() value = self.eval_config_value(value) if section_name == 'order': config[section_name].append(value) line = '}' # create an empty config for this module if value not in config: config[value] = {} # 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: 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 if line.endswith('}'): 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, ' or on your own {}/.i3status.conf'.format( os.path.expanduser( '~')) if i3status_config_path == '/etc/i3status.conf' else '')) # time and tztime modules need a format for correct processing for name in config: if name.split()[0] in TIME_MODULES and 'format' not in config[ name]: if name.split()[0] == 'time': config[name]['format'] = TIME_FORMAT else: config[name]['format'] = TZTIME_FORMAT def clean_i3status_modules(key): # cleanup unconfigured i3status modules that have no default for module_name in deepcopy(config[key]): if (self.valid_config_param(module_name, cleanup=True) and not config.get(module_name)): config.pop(module_name) if module_name in config['i3s_modules']: config['i3s_modules'].remove(module_name) config[key].remove(module_name) clean_i3status_modules('order') clean_i3status_modules('.group_extras') return config def set_responses(self, json_list): """ Set the given i3status responses on their respective configuration. """ self.update_json_list() updates = [] for index, item in enumerate(self.json_list): conf_name = self.config['i3s_modules'][index] if conf_name not in self.i3modules: self.i3modules[conf_name] = I3statusModule(conf_name, self.py3_wrapper) if self.i3modules[conf_name].update_from_item(item): updates.append(conf_name) self.py3_wrapper.notify_update(updates) def update_json_list(self): """ Copy the last json list output from i3status so that any module can modify it without altering the original output. This is done so that any module's alteration of a i3status output json will not be overwritten when the next i3status output gets polled. """ self.json_list = deepcopy(self.last_output) @staticmethod def write_in_tmpfile(text, tmpfile): """ Write the given text in the given tmpfile in python2 and python3. """ try: tmpfile.write(text) except TypeError: tmpfile.write(str.encode(text)) 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 sorted(self.config.items()): if section_name in ['i3s_modules', 'py3_modules', '.group_extras', '.module_groups']: continue elif section_name == 'order': for module_name in conf: if self.valid_config_param(module_name): self.write_in_tmpfile('order += "%s"\n' % module_name, tmpfile) # we need to make sure any additional i3status modules needed # for groups are added to the i3status config for module_name in self.config['.group_extras']: self.write_in_tmpfile('order += "%s"\n' % module_name, tmpfile) self.write_in_tmpfile('\n', tmpfile) elif self.valid_config_param(section_name) and conf: self.write_in_tmpfile('%s {\n' % section_name, tmpfile) for key, value in conf.items(): # Set known fixed format for time and tztime so we can work # out the timezone if section_name.split()[ 0] in TIME_MODULES: if key == 'format': value = TZTIME_FORMAT if key == 'format_time': continue if isinstance(value, bool): value = '{}'.format(value).lower() self.write_in_tmpfile(' %s = "%s"\n' % (key, value), tmpfile) self.write_in_tmpfile('}\n\n', tmpfile) tmpfile.flush() def suspend_i3status(self): # Put i3status to sleep if self.i3status_pipe: self.i3status_pipe.send_signal(SIGSTOP) @profile def run(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) syslog(LOG_INFO, 'i3status spawned using config file {}'.format( tmpfile.name)) i3status_pipe = Popen( ['i3status', '-c', tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGUSR2 signal for this subprocess preexec_fn=lambda: signal(SIGUSR2, SIG_IGN) ) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ',': line = line[1:] if line.startswith('[{'): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err except OSError: # we cleanup the tmpfile ourselves so when the delete will occur # it will usually raise an OSError: No such file or directory pass self.i3status_pipe = None def cleanup_tmpfile(self): """ Cleanup i3status tmp configuration file. """ if os.path.isfile(self.tmpfile_path): os.remove(self.tmpfile_path) def mock(self): """ Mock i3status behavior, used in standalone mode. """ # mock thread is_alive() method self.is_alive = lambda: True # mock i3status output parsing self.last_output = [] self.update_json_list()
class I3status(Thread): """ This class is responsible for spawning i3status and reading its output. """ def __init__(self, py3_wrapper, config): """ Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) self.config = config 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.i3modules = {} self.json_list = None self.json_list_ts = None self.last_output = None self.last_refresh_ts = time() self.lock = py3_wrapper.lock self.new_update = False self.py3_wrapper = py3_wrapper self.ready = False self.standalone = py3_wrapper.config['standalone'] self.i3status_pipe = None self.time_modules = [] self.tmpfile_path = None def update_times(self): """ Update time for any i3status time/tztime items. """ updated = [] for module in self.i3modules.values(): if module.is_time_module: if module.update_time_value(): updated.append(module.module_name) if updated: # trigger the update so new time is shown self.py3_wrapper.notify_update(updated) def valid_config_param(self, param_name, cleanup=False): """ Check if a given section name is a valid parameter for i3status. """ if cleanup: valid_config_params = [ _ for _ in self.i3status_module_names if _ not in ['cpu_usage', 'ddate', 'ipv6', 'load', 'time'] ] else: valid_config_params = self.i3status_module_names + [ 'general', 'order' ] return param_name.split(' ')[0] in valid_config_params def set_responses(self, json_list): """ Set the given i3status responses on their respective configuration. """ self.update_json_list() updates = [] for index, item in enumerate(self.json_list): conf_name = self.config['i3s_modules'][index] if conf_name not in self.i3modules: self.i3modules[conf_name] = I3statusModule(conf_name, self.py3_wrapper) if self.i3modules[conf_name].update_from_item(item): updates.append(conf_name) self.py3_wrapper.notify_update(updates) def update_json_list(self): """ Copy the last json list output from i3status so that any module can modify it without altering the original output. This is done so that any module's alteration of a i3status output json will not be overwritten when the next i3status output gets polled. """ self.json_list = deepcopy(self.last_output) @staticmethod def write_in_tmpfile(text, tmpfile): """ Write the given text in the given tmpfile in python2 and python3. """ try: tmpfile.write(text) except TypeError: tmpfile.write(str.encode(text)) except UnicodeEncodeError: tmpfile.write(text.encode('utf-8')) 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'. """ # order += ... for module in self.config['i3s_modules']: self.write_in_tmpfile('order += "%s"\n' % module, tmpfile) self.write_in_tmpfile('\n', tmpfile) # config params for general section and each module for section_name in ['general'] + self.config['i3s_modules']: section = self.config[section_name] self.write_in_tmpfile('%s {\n' % section_name, tmpfile) for key, value in section.items(): # don't include color values except in the general section if key.startswith('color'): if (section_name.split(' ')[0] not in I3S_COLOR_MODULES or key not in I3S_ALLOWED_COLORS): continue # Set known fixed format for time and tztime so we can work # out the timezone if section_name.split()[0] in TIME_MODULES: if key == 'format': value = TZTIME_FORMAT if key == 'format_time': continue if isinstance(value, bool): value = '{}'.format(value).lower() self.write_in_tmpfile(' %s = "%s"\n' % (key, value), tmpfile) self.write_in_tmpfile('}\n\n', tmpfile) tmpfile.flush() def suspend_i3status(self): # Put i3status to sleep if self.i3status_pipe: self.i3status_pipe.send_signal(SIGSTOP) def refresh_i3status(self): # refresh i3status. This is rate limited if time() > (self.last_refresh_ts + 0.1): if self.py3_wrapper.config['debug']: self.py3_wrapper.log('refreshing i3status') if self.i3status_pipe: self.i3status_pipe.send_signal(SIGUSR1) self.last_refresh_ts = time() @profile def run(self): # if the i3status process dies we want to restart it. # We give up restarting if we have died too often for x in range(10): if not self.lock.is_set(): break self.spawn_i3status() # check if we never worked properly and if so quit now if not self.ready: break # limit restart rate sleep(5) def spawn_i3status(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) self.py3_wrapper.log( 'i3status spawned using config file {}'.format( tmpfile.name)) i3status_pipe = Popen( ['i3status', '-c', tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGTSTP signal for this subprocess preexec_fn=lambda: signal(SIGTSTP, SIG_IGN) ) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ',': line = line[1:] if line.startswith('[{'): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err self.py3_wrapper.log(err, 'error') except Exception: self.py3_wrapper.report_exception('', notify_user=True) self.i3status_pipe = None def mock(self): """ Mock i3status behavior, used in standalone mode. """ # mock thread is_alive() method self.is_alive = lambda: True # mock i3status output parsing self.last_output = [] self.update_json_list()
class I3status(Thread): """ This class is responsible for spawning i3status and reading its output. """ def __init__(self, py3_wrapper): """ Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) self.error = None self.i3modules = {} self.i3status_pipe = None self.i3status_path = py3_wrapper.config["i3status_path"] self.json_list = None self.json_list_ts = None self.last_output = None self.last_refresh_ts = time() self.lock = py3_wrapper.lock self.new_update = False self.py3_config = py3_wrapper.config["py3_config"] self.py3_wrapper = py3_wrapper self.ready = False self.standalone = py3_wrapper.config["standalone"] self.time_modules = [] self.tmpfile_path = None self.update_due = 0 # the update interval is useful to know self.update_interval = self.py3_wrapper.get_config_attribute( "general", "interval") # do any initialization self.setup() def setup(self): """ Do any setup work needed to run i3status modules """ for conf_name in self.py3_config["i3s_modules"]: module = I3statusModule(conf_name, self) self.i3modules[conf_name] = module if module.is_time_module: self.time_modules.append(module) def set_responses(self, json_list): """ Set the given i3status responses on their respective configuration. """ self.update_json_list() updates = [] for index, item in enumerate(self.json_list): conf_name = self.py3_config["i3s_modules"][index] module = self.i3modules[conf_name] if module.update_from_item(item): updates.append(conf_name) if updates: self.py3_wrapper.notify_update(updates) def update_json_list(self): """ Copy the last json list output from i3status so that any module can modify it without altering the original output. This is done so that any module's alteration of a i3status output json will not be overwritten when the next i3status output gets polled. """ self.json_list = deepcopy(self.last_output) @staticmethod def write_in_tmpfile(text, tmpfile): """ Write the given text in the given tmpfile in python2 and python3. """ try: tmpfile.write(text) except TypeError: tmpfile.write(str.encode(text)) except UnicodeEncodeError: tmpfile.write(text.encode("utf-8")) 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'. """ # order += ... for module in self.py3_config["i3s_modules"]: self.write_in_tmpfile('order += "%s"\n' % module, tmpfile) self.write_in_tmpfile("\n", tmpfile) # config params for general section and each module for section_name in ["general"] + self.py3_config["i3s_modules"]: section = self.py3_config[section_name] self.write_in_tmpfile("%s {\n" % section_name, tmpfile) for key, value in section.items(): # don't include color values except in the general section if key.startswith("color"): if (section_name.split(" ")[0] not in I3S_COLOR_MODULES or key not in I3S_ALLOWED_COLORS): continue # Set known fixed format for time and tztime so we can work # out the timezone if section_name.split()[0] in TIME_MODULES: if key == "format": value = TZTIME_FORMAT if key == "format_time": continue if isinstance(value, bool): value = "{}".format(value).lower() self.write_in_tmpfile(' {} = "{}"\n'.format(key, value), tmpfile) self.write_in_tmpfile("}\n\n", tmpfile) tmpfile.flush() def suspend_i3status(self): # Put i3status to sleep if self.i3status_pipe: self.i3status_pipe.send_signal(SIGSTOP) def refresh_i3status(self): # refresh i3status. This is rate limited if time() > (self.last_refresh_ts + 0.1): if self.py3_wrapper.config["debug"]: self.py3_wrapper.log("refreshing i3status") if self.i3status_pipe: self.i3status_pipe.send_signal(SIGUSR1) self.last_refresh_ts = time() @profile def run(self): # if the i3status process dies we want to restart it. # We give up restarting if we have died too often for x in range(10): if not self.py3_wrapper.running: break self.spawn_i3status() # check if we never worked properly and if so quit now if not self.ready: break # limit restart rate self.lock.wait(5) def spawn_i3status(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix="py3status_") as tmpfile: self.write_tmp_i3status_config(tmpfile) i3status_pipe = Popen( [self.i3status_path, "-c", tmpfile.name], stdout=PIPE, stderr=PIPE, # Ignore the SIGTSTP signal for this subprocess preexec_fn=lambda: signal(SIGTSTP, SIG_IGN), ) self.py3_wrapper.log( "i3status spawned using config file {}".format( tmpfile.name)) self.poller_inp = IOPoller(i3status_pipe.stdout) self.poller_err = IOPoller(i3status_pipe.stderr) self.tmpfile_path = tmpfile.name # Store the pipe so we can signal it self.i3status_pipe = i3status_pipe try: # loop on i3status output while self.py3_wrapper.running: line = self.poller_inp.readline() if line: # remove leading comma if present if line[0] == ",": line = line[1:] if line.startswith("[{"): json_list = loads(line) self.last_output = json_list self.set_responses(json_list) self.ready = True else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = "i3status died" if err: msg += " and said: {}".format(err) else: msg += " with code {}".format(code) raise OSError(msg) except OSError: err = sys.exc_info()[1] self.error = err self.py3_wrapper.log(err, "error") except OSError: self.error = "Problem starting i3status maybe it is not installed" except Exception: self.py3_wrapper.report_exception("", notify_user=True) self.i3status_pipe = None def mock(self): """ Mock i3status behavior, used in standalone mode. """ # mock thread is_alive() method self.is_alive = lambda: True
class I3status(Thread): """ This class is responsible for spawning i3status and reading its output. """ def __init__(self, py3_wrapper): """ Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) 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.i3modules = {} self.json_list = None self.json_list_ts = None self.last_output = None self.last_prefix = None self.lock = py3_wrapper.lock self.new_update = False self.py3_wrapper = py3_wrapper self.ready = False self.standalone = py3_wrapper.config['standalone'] self.time_modules = [] self.tmpfile_path = None # config_path = py3_wrapper.config['i3status_config_path'] self.config = self.i3status_config_reader(config_path) def update_times(self): """ Update time for any i3status time/tztime items. """ updated = [] for module in self.i3modules.values(): if module.is_time_module: if module.update_time_value(): updated.append(module.module_name) if updated: # trigger the update so new time is shown self.py3_wrapper.notify_update(updated) def valid_config_param(self, param_name, cleanup=False): """ Check if a given section name is a valid parameter for i3status. """ if cleanup: valid_config_params = [ _ for _ in self.i3status_module_names if _ not in ['cpu_usage', 'ddate', 'ipv6', 'load', 'time'] ] else: valid_config_params = self.i3status_module_names + [ 'general', 'order' ] return param_name.split(' ')[0] in valid_config_params @staticmethod def eval_config_parameter(param): """ Try to evaluate the given parameter as a string or integer and return it properly. This is used to parse i3status configuration parameters such as 'disk "/home" {}' or worse like '"cpu_temperature" 0 {}'. """ params = param.split(' ') result_list = list() for p in params: try: e_value = eval(p) if isinstance(e_value, str) or isinstance(e_value, int): p = str(e_value) else: raise ValueError() except (NameError, SyntaxError, ValueError): pass finally: result_list.append(p) return ' '.join(result_list) @staticmethod def eval_config_value(value): """ Try to evaluate the given parameter as a string or integer and return it properly. This is used to parse i3status configuration parameters such as 'disk "/home" {}' or worse like '"cpu_temperature" 0 {}'. """ if value.lower() in ('true', 'false'): return eval(value.title()) try: e_value = eval(value) if isinstance(e_value, str): if e_value.lower() in ('true', 'false'): value = eval(e_value.title()) else: value = e_value elif isinstance(e_value, int): value = e_value else: raise ValueError() except (NameError, ValueError): pass finally: return value def i3status_config_reader(self, i3status_config_path): """ Parse i3status.conf so we can adapt our code to the i3status config. """ config = { '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': [], '.group_extras': [], # extra i3status modules needed by groups '.module_groups': {}, # record groups that modules are in 'py3_modules': [] } # some ugly parsing in_section = False section_name = '' group_name = None 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 and line.startswith('group'): group_name = line.split('{')[0].strip() config[group_name] = {'items': []} continue if not in_section and group_name and line == '}': group_name = None continue if group_name and not in_section and '=' in line: # check this is not a section definition if '{' not in line or line.index('{') > line.index('='): key = line.split('=', 1)[0].strip() key = self.eval_config_parameter(key) value = line.split('=', 1)[1].strip() value = self.eval_config_value(value) if not key.startswith('on_click'): config[group_name][key] = value else: # on_click special parameters try: 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 group {}'.format(group_name)) except ValueError as e: raise ValueError('invalid "button id" ' 'for "on_click" parameter ' 'in group {} ({})'.format( group_name, e)) on_c = config['on_click'] on_c[group_name] = on_c.get(group_name, {}) on_c[group_name][button] = value continue if not in_section: section_name = line.split('{')[0].strip() section_name = self.eval_config_parameter(section_name) if not section_name: continue else: in_section = True if section_name not in config: config[section_name] = {} if group_name: # update the items in the group config[group_name]['items'].append(section_name) section = config['.module_groups'].setdefault( section_name, []) if group_name not in section: section.append(group_name) if not self.valid_config_param(section_name): # py3status module add a reference to the group and # make sure we have it in the list of modules to # run if section_name not in config['py3_modules']: config['py3_modules'].append(section_name) else: # i3status module. Add to the list of needed # modules and add to the `.group-extras` config to # ensure that it gets run even though not in # `order` config if section_name not in config['i3s_modules']: config['i3s_modules'].append(section_name) if section_name not in config['.group_extras']: config['.group_extras'].append(section_name) if '{' in line: in_section = True if section_name and '=' in line: section_line = line # one liner cases if line.endswith('}'): section_line = section_line.split('}', -1)[0].strip() if line.startswith(section_name + ' {'): section_line = section_line.split(section_name + ' {')[1].strip() key = section_line.split('=', 1)[0].strip() key = self.eval_config_parameter(key) value = section_line.split('=', 1)[1].strip() value = self.eval_config_value(value) if section_name == 'order': config[section_name].append(value) line = '}' # create an empty config for this module if value not in config: config[value] = {} # 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: 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 if line.endswith('}'): 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, ' or on your own {}/.i3status.conf'. format(os.path.expanduser('~')) if i3status_config_path == '/etc/i3status.conf' else '')) # time and tztime modules need a format for correct processing for name in config: if name.split( )[0] in TIME_MODULES and 'format' not in config[name]: if name.split()[0] == 'time': config[name]['format'] = TIME_FORMAT else: config[name]['format'] = TZTIME_FORMAT def clean_i3status_modules(key): # cleanup unconfigured i3status modules that have no default for module_name in deepcopy(config[key]): if (self.valid_config_param(module_name, cleanup=True) and not config.get(module_name)): config.pop(module_name) if module_name in config['i3s_modules']: config['i3s_modules'].remove(module_name) config[key].remove(module_name) clean_i3status_modules('order') clean_i3status_modules('.group_extras') return config def set_responses(self, json_list): """ Set the given i3status responses on their respective configuration. """ self.update_json_list() updates = [] for index, item in enumerate(self.json_list): conf_name = self.config['i3s_modules'][index] if conf_name not in self.i3modules: self.i3modules[conf_name] = I3statusModule( conf_name, self.py3_wrapper) if self.i3modules[conf_name].update_from_item(item): updates.append(conf_name) self.py3_wrapper.notify_update(updates) def update_json_list(self): """ Copy the last json list output from i3status so that any module can modify it without altering the original output. This is done so that any module's alteration of a i3status output json will not be overwritten when the next i3status output gets polled. """ self.json_list = deepcopy(self.last_output) @staticmethod def write_in_tmpfile(text, tmpfile): """ Write the given text in the given tmpfile in python2 and python3. """ try: tmpfile.write(text) except TypeError: tmpfile.write(str.encode(text)) 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 sorted(self.config.items()): if section_name in [ 'i3s_modules', 'py3_modules', '.group_extras', '.module_groups' ]: continue elif section_name == 'order': for module_name in conf: if self.valid_config_param(module_name): self.write_in_tmpfile('order += "%s"\n' % module_name, tmpfile) # we need to make sure any additional i3status modules needed # for groups are added to the i3status config for module_name in self.config['.group_extras']: self.write_in_tmpfile('order += "%s"\n' % module_name, tmpfile) self.write_in_tmpfile('\n', tmpfile) elif self.valid_config_param(section_name) and conf: self.write_in_tmpfile('%s {\n' % section_name, tmpfile) for key, value in conf.items(): # Set known fixed format for time and tztime so we can work # out the timezone if section_name.split( )[0] in TIME_MODULES and key == 'format': value = TZTIME_FORMAT if isinstance(value, bool): value = '{}'.format(value).lower() self.write_in_tmpfile(' %s = "%s"\n' % (key, value), tmpfile) self.write_in_tmpfile('}\n\n', tmpfile) tmpfile.flush() @profile def run(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) syslog( LOG_INFO, 'i3status spawned using config file {}'.format( tmpfile.name)) 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) self.tmpfile_path = tmpfile.name try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: if line.startswith('[{'): print_line(line) with jsonify(line) as (prefix, json_list): self.last_output = json_list self.last_prefix = ',' self.set_responses(json_list) self.ready = True elif not line.startswith(','): if 'version' in line: header = loads(line) header.update({'click_events': True}) line = dumps(header) print_line(line) else: with jsonify(line) as (prefix, json_list): self.last_output = json_list self.last_prefix = prefix self.set_responses(json_list) else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err except OSError: # we cleanup the tmpfile ourselves so when the delete will occur # it will usually raise an OSError: No such file or directory pass def cleanup_tmpfile(self): """ Cleanup i3status tmp configuration file. """ if os.path.isfile(self.tmpfile_path): os.remove(self.tmpfile_path) def mock(self): """ Mock i3status behavior, used in standalone mode. """ # mock thread is_alive() method self.is_alive = lambda: True # mock i3status base output init_output = ['{"click_events": true, "version": 1}', '[', '[]'] for line in init_output: print_line(line) # mock i3status output parsing self.last_output = [] self.last_prefix = ',' self.update_json_list()
class I3status(Thread): """ This class is responsible for spawning i3status and reading its output. """ def __init__(self, lock, i3status_config_path, standalone): """ Our output will be read asynchronously from 'last_output'. """ Thread.__init__(self) 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.ready = False self.standalone = standalone self.tmpfile_path = None # self.config = self.i3status_config_reader(i3status_config_path) def valid_config_param(self, param_name, cleanup=False): """ Check if a given section name is a valid parameter for i3status. """ if cleanup: valid_config_params = [ _ for _ in self.i3status_module_names if _ not in ['cpu_usage', 'ddate', 'ipv6', 'load', 'time'] ] else: valid_config_params = self.i3status_module_names + [ 'general', 'order' ] return param_name.split(' ')[0] in valid_config_params @staticmethod def eval_config_parameter(param): """ Try to evaluate the given parameter as a string or integer and return it properly. This is used to parse i3status configuration parameters such as 'disk "/home" {}' or worse like '"cpu_temperature" 0 {}'. """ params = param.split(' ') result_list = list() for p in params: try: e_value = eval(p) if isinstance(e_value, str) or isinstance(e_value, int): p = str(e_value) else: raise ValueError() except (NameError, SyntaxError, ValueError): pass finally: result_list.append(p) return ' '.join(result_list) @staticmethod def eval_config_value(value): """ Try to evaluate the given parameter as a string or integer and return it properly. This is used to parse i3status configuration parameters such as 'disk "/home" {}' or worse like '"cpu_temperature" 0 {}'. """ if value.lower() in ('true', 'false'): return eval(value.title()) try: e_value = eval(value) if isinstance(e_value, str): if e_value.lower() in ('true', 'false'): value = eval(e_value.title()) else: value = e_value elif isinstance(e_value, int): value = e_value else: raise ValueError() except (NameError, ValueError): pass finally: return value def i3status_config_reader(self, i3status_config_path): """ Parse i3status.conf so we can adapt our code to the i3status config. """ config = { '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': [], '.group_extras': [], # extra i3status modules needed by groups 'py3_modules': [] } # some ugly parsing in_section = False section_name = '' group_name = None 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 and line.startswith('group'): group_name = line.split('{')[0].strip() config[group_name] = {'items': []} continue if not in_section and group_name and line == '}': group_name = None continue if group_name and not in_section and '=' in line: # check this is not a section definition if '{' not in line or line.index('{') > line.index('='): key = line.split('=', 1)[0].strip() key = self.eval_config_parameter(key) value = line.split('=', 1)[1].strip() value = self.eval_config_value(value) config[group_name][key] = value continue if not in_section: section_name = line.split('{')[0].strip() section_name = self.eval_config_parameter(section_name) if not section_name: continue else: in_section = True if section_name not in config: config[section_name] = {} if group_name: # update the items in the group config[group_name]['items'].append(section_name) if not self.valid_config_param(section_name): # py3status module add a reference to the group and # make sure we have it in the list of modules to # run config[section_name]['.group'] = group_name if section_name not in config['py3_modules']: config['py3_modules'].append(section_name) else: # i3status module. Add to the list of needed # modules and add to the `.group-extras` config to # ensure that it gets run even though not in # `order` config if section_name not in config['i3s_modules']: config['i3s_modules'].append(section_name) if section_name not in config['.group_extras']: config['.group_extras'].append(section_name) if '{' in line: in_section = True if section_name and '=' in line: section_line = line # one liner cases if line.endswith('}'): section_line = section_line.split('}', -1)[0].strip() if line.startswith(section_name + ' {'): section_line = section_line.split(section_name + ' {')[ 1].strip() key = section_line.split('=', 1)[0].strip() key = self.eval_config_parameter(key) value = section_line.split('=', 1)[1].strip() value = self.eval_config_value(value) if section_name == 'order': config[section_name].append(value) line = '}' # create an empty config for this module if value not in config: config[value] = {} # 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: 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 if line.endswith('}'): 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, ' or on your own {}/.i3status.conf'.format( os.path.expanduser( '~')) if i3status_config_path == '/etc/i3status.conf' else '')) def clean_i3status_modules(key): # cleanup unconfigured i3status modules that have no default for module_name in deepcopy(config[key]): if (self.valid_config_param(module_name, cleanup=True) and not config.get(module_name)): config.pop(module_name) if module_name in config['i3s_modules']: config['i3s_modules'].remove(module_name) config[key].remove(module_name) clean_i3status_modules('order') clean_i3status_modules('.group_extras') return config def set_responses(self, json_list): """ Set the given i3status responses on their respective configuration. """ for index, item in enumerate(self.json_list): conf_name = self.config['i3s_modules'][index] self.config[conf_name]['response'] = item def get_delta_from_format(self, i3s_time, time_format): """ Guess the time delta from %z time formats such as +0400. When such a format is found, replace it in the string so we respect i3status' output while being able to correctly adjust the time. """ try: if '%z' in time_format: res = findall('[\-+]{1}[\d]{4}', i3s_time)[0] if res: operator = res[0] hours = int(res[1:3]) minutes = int(res[-2:]) return (time_format.replace('%z', res), timedelta( hours=eval('{}{}'.format(operator, hours)), minutes=eval('{}{}'.format(operator, minutes)))) except Exception: err = sys.exc_info()[1] syslog( LOG_ERR, 'i3status get_delta_from_format failed "{}" "{}" ({})'.format( i3s_time, time_format, err)) return (time_format, None) def set_time_modules(self): """ This method is executed only once after the first i3status output. We parse all the i3status time and tztime modules and generate a datetime for each of them while preserving (or defaulting) their configured time format. We also calculate a timedelta for each of them representing their timezone offset. This is this delta that we'll be using from now on as any future time or tztime update from i3status will be overwritten thanks to our pre-parsed date here. """ default_time_format = '%Y-%m-%d %H:%M:%S' default_tztime_format = '%Y-%m-%d %H:%M:%S %Z' utcnow = self.last_output_ts # for index, item in enumerate(self.json_list): if item.get('name') in ['time', 'tztime']: conf_name = self.config['i3s_modules'][index] time_name = item.get('name') # time and tztime have different defaults if time_name == 'time': time_format = self.config.get( conf_name, {}).get('format', default_time_format) else: time_format = self.config.get( conf_name, {}).get('format', default_tztime_format) # handle format_time parameter if 'format_time' in self.config.get(conf_name, {}): time_format = time_format.replace( '%time', self.config[conf_name]['format_time']) # parse i3status date i3s_time = item['full_text'].encode('UTF-8', 'replace') try: # python3 compatibility code i3s_time = i3s_time.decode() except: pass time_format, delta = self.get_delta_from_format(i3s_time, time_format) try: if '%Z' in time_format: raise ValueError('%Z directive is not supported') # add mendatory items in i3status time format wrt issue #18 time_fmt = time_format for fmt in ['%Y', '%m', '%d']: if fmt not in time_format: time_fmt = '{} {}'.format(time_fmt, fmt) i3s_time = '{} {}'.format( i3s_time, datetime.now().strftime(fmt)) # get a datetime from the parsed string date date = datetime.strptime(i3s_time, time_fmt) # calculate the delta if needed if not delta: delta = ( datetime(date.year, date.month, date.day, date.hour, date.minute) - datetime( utcnow.year, utcnow.month, utcnow.day, utcnow.hour, utcnow.minute)) except ValueError: date = i3s_time except Exception: err = sys.exc_info()[1] syslog(LOG_ERR, 'i3status set_time_modules "{}" failed ({})'.format( conf_name, err)) date = i3s_time finally: self.config[conf_name]['date'] = date self.config[conf_name]['delta'] = delta self.config[conf_name]['time_format'] = time_format def tick_time_modules(self, json_list, force): """ Adjust the 'time' and 'tztime' objects from the given json_list so that they are updated only at py3status interval seconds. This method is used to overwrite any i3status time or tztime output with respect to their parsed and timezone offset detected on start. """ utcnow = datetime.utcnow() # every whole minute, resync our time from i3status' # this ensures we will catch any daylight savings time change if utcnow.second == 0: self.set_time_modules() # for index, item in enumerate(json_list): if item.get('name') in ['time', 'tztime']: conf_name = self.config['i3s_modules'][index] time_module = self.config[conf_name] if not isinstance(time_module['date'], datetime): # something went wrong in the datetime parsing # output i3status' date string item['full_text'] = time_module['date'] else: if force: date = utcnow + time_module['delta'] time_module['date'] = date else: date = time_module['date'] time_format = self.config[conf_name].get('time_format') # set the full_text date on the json_list to be returned item['full_text'] = date.strftime(time_format) json_list[index] = item # reset the full_text date on the config object for next # iteration to be consistent with this one time_module['response']['full_text'] = item['full_text'] return json_list def update_json_list(self): """ Copy the last json list output from i3status so that any module can modify it without altering the original output. This is done so that any module's alteration of a i3status output json will not be overwritten when the next i3status output gets polled. """ 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_name in self.config['order']: if module_name in py3_modules: for method in py3_modules[module_name].methods.values(): ordered.append(method['last_output']) else: if self.config.get(module_name, {}).get('response'): ordered.append(self.config[module_name]['response']) return ordered @staticmethod def write_in_tmpfile(text, tmpfile): """ Write the given text in the given tmpfile in python2 and python3. """ try: tmpfile.write(text) except TypeError: tmpfile.write(str.encode(text)) 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 sorted(self.config.items()): if section_name in ['i3s_modules', 'py3_modules', '.group_extras']: continue elif section_name == 'order': for module_name in conf: if self.valid_config_param(module_name): self.write_in_tmpfile('order += "%s"\n' % module_name, tmpfile) # we need to make sure any additional i3status modules needed # for groups are added to the i3status config for module_name in self.config['.group_extras']: self.write_in_tmpfile('order += "%s"\n' % module_name, tmpfile) self.write_in_tmpfile('\n', tmpfile) elif self.valid_config_param(section_name) and conf: self.write_in_tmpfile('%s {\n' % section_name, tmpfile) for key, value in conf.items(): if isinstance(value, bool): value = '{}'.format(value).lower() self.write_in_tmpfile(' %s = "%s"\n' % (key, value), tmpfile) self.write_in_tmpfile('}\n\n', tmpfile) tmpfile.flush() @profile def run(self): """ Spawn i3status using a self generated config file and poll its output. """ try: with NamedTemporaryFile(prefix='py3status_') as tmpfile: self.write_tmp_i3status_config(tmpfile) syslog(LOG_INFO, 'i3status spawned using config file {}'.format( tmpfile.name)) 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) self.tmpfile_path = tmpfile.name try: # loop on i3status output while self.lock.is_set(): line = self.poller_inp.readline() if line: if line.startswith('[{'): print_line(line) 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() self.set_responses(json_list) # on first i3status output, we parse # the time and tztime modules self.set_time_modules() self.ready = True elif not line.startswith(','): if 'version' in line: header = loads(line) header.update({'click_events': True}) line = dumps(header) print_line(line) else: with jsonify(line) as (prefix, json_list): self.last_output = json_list self.last_output_ts = datetime.utcnow() self.last_prefix = prefix self.update_json_list() self.set_responses(json_list) else: err = self.poller_err.readline() code = i3status_pipe.poll() if code is not None: msg = 'i3status died' if err: msg += ' and said: {}'.format(err) else: msg += ' with code {}'.format(code) raise IOError(msg) except IOError: err = sys.exc_info()[1] self.error = err except OSError: # we cleanup the tmpfile ourselves so when the delete will occur # it will usually raise an OSError: No such file or directory pass def cleanup_tmpfile(self): """ Cleanup i3status tmp configuration file. """ if os.path.isfile(self.tmpfile_path): os.remove(self.tmpfile_path) def mock(self): """ Mock i3status behavior, used in standalone mode. """ # mock thread is_alive() method self.is_alive = lambda: True # mock i3status base output init_output = ['{"click_events": true, "version": 1}', '[', '[]'] for line in init_output: print_line(line) # mock i3status output parsing self.last_output = [] self.last_output_ts = datetime.utcnow() self.last_prefix = ',' self.update_json_list()