diff --git a/doc/py3-cmd.rst b/doc/py3-cmd.rst index 333d0cbd98..f2f8fd644f 100644 --- a/doc/py3-cmd.rst +++ b/doc/py3-cmd.rst @@ -14,21 +14,20 @@ refresh ^^^^^^^ Cause named module(s) to have their output refreshed. -To refresh all modules use the ``all`` keyword. .. code-block:: shell - # refresh any wifi modules + # refresh all instances of the wifi module py3-cmd refresh wifi - # refresh wifi module instance eth0 - py3-cmd refresh "wifi eth0" + # refresh multiple modules + py3-cmd refresh coin_market github weather_yahoo - # refresh any wifi or whatismyip modules - py3-cmd refresh wifi whatismyip + # refresh module with instance name + py3-cmd refresh "weather_yahoo chicago" - # refresh all py3status modules - py3-cmd refresh all + # refresh all modules + py3-cmd refresh --all click @@ -39,22 +38,27 @@ You can specify the button to simulate. .. code-block:: shell - # send a click event to the whatismyip module (button 1) - py3-cmd click whatismyip + # send a left/middle/right click + py3-cmd click --button 1 dpms # left + py3-cmd click --button 2 sysdata # middle + py3-cmd click --button 3 pomodoro # right - # send a click event to the backlight module with button 5 - py3-cmd click 5 backlight - -You can also specify the button using one of the named shortcuts -``leftclick``, ``rightclick``, ``middleclick``, ``scrollup``, ``scrolldown``. + # send a up/down click + py3-cmd click --button 4 volume_status # up + py3-cmd click --button 5 volume_status # down .. code-block:: shell - # send a click event to the whatismyip module (button 1) - py3-cmd leftclick whatismyip + # toggle button in frame module + py3-cmd click --button 1 --index button frame # left + + # change modules in group module + py3-cmd click --button 5 --index button group # down - # send a click event to the backlight module with button 5 - py3-cmd scrolldown backlight + # change time units in timer module + py3-cmd click --button 4 --index hours timer # up + py3-cmd click --button 4 --index minutes timer # up + py3-cmd click --button 4 --index seconds timer # up Calling commands from i3 diff --git a/py3status/command.py b/py3status/command.py index bd56dcad6a..0bca8ec272 100644 --- a/py3status/command.py +++ b/py3status/command.py @@ -3,21 +3,78 @@ import json import os import socket -import sys import threading -from py3status.version import version - SERVER_ADDRESS = '/tmp/py3status_uds' MAX_SIZE = 1024 -BUTTONS = { - 'leftclick': 1, - 'middleclick': 2, - 'rightclick': 3, - 'scrollup': 4, - 'scrolldown': 5, -} +REFRESH_EPILOG = """ +examples: + refresh: + # refresh all instances of the wifi module + py3-cmd refresh wifi + + # refresh multiple modules + py3-cmd refresh coin_market github weather_yahoo + + # refresh a module with instance name + py3-cmd refresh "weather_yahoo chicago" + + # refresh all modules + py3-cmd refresh --all +""" +CLICK_EPILOG = """ +examples: + button: + # send a left/middle/right click + py3-cmd click --button 1 dpms # left + py3-cmd click --button 2 sysdata # middle + py3-cmd click --button 3 pomodoro # right + + # send a up/down click + py3-cmd click --button 4 volume_status # up + py3-cmd click --button 5 volume_status # down + + index: + # toggle button in frame module + py3-cmd click --button 1 --index button frame # left + + # change modules in group module + py3-cmd click --button 5 --index button group # down + + # change time units in timer module + py3-cmd click --button 4 --index hours timer # up + py3-cmd click --button 4 --index minutes timer # up + py3-cmd click --button 4 --index seconds timer # up + + width, height, relative_x, relative_y, x, y: + # py3-cmd allows users to specify click events with + # more options. however, there are no modules that + # uses the aforementioned options. +""" +# EXEC_EPILOG = '' +INFORMATION = [ + ('V', 'version', 'show version number and exit'), + ('v', 'verbose', 'enable verbose mode'), +] +SUBPARSERS = [ + ('click', 'click modules', '+'), + ('refresh', 'refresh modules', '*'), + # ('exec', 'execute methods', '+'), +] +CLICK_OPTIONS = [ + ('button', 'specify a button number (default %(default)s)'), + ('height', 'specify a height of the block, in pixel'), + ('index', 'specify an index value often found in modules'), + ('relative_x', 'specify relative X on the block, from the top left'), + ('relative_y', 'specify relative Y on the block, from the top left'), + ('width', 'specify a width of the block, in pixel'), + ('x', 'specify absolute X on the bar, from the top left'), + ('y', 'specify absolute Y on the bar, from the top left'), +] +REFRESH_OPTIONS = [ + ('all', 'refresh all modules') +] class CommandRunner: @@ -75,9 +132,7 @@ def click(self, data): """ send a click event to the module(s) """ - button = data.get('button') modules = data.get('module') - for module_name in self.find_modules(modules): module = self.py3_wrapper.output_modules[module_name] if module['type'] == 'py3status': @@ -86,14 +141,11 @@ def click(self, data): else: name = module['module'].name instance = module['module'].instance - # our fake event, we do not know x, y so set to None - event = { - 'y': None, - 'x': None, - 'button': button, - 'name': name, - 'instance': instance, - } + # make an event + event = {'name': name, 'instance': instance} + for name, message in CLICK_OPTIONS: + event[name] = data.get(name) + if self.debug: self.py3_wrapper.log(event) # trigger the event @@ -127,10 +179,7 @@ def __init__(self, py3_wrapper): self.py3_wrapper = py3_wrapper self.command_runner = CommandRunner(py3_wrapper) - - pid = os.getpid() - - server_address = '{}.{}'.format(SERVER_ADDRESS, pid) + server_address = '{}.{}'.format(SERVER_ADDRESS, os.getpid()) self.server_address = server_address # Make sure the socket does not already exist @@ -142,7 +191,6 @@ def __init__(self, py3_wrapper): # Create a UDS socket sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(server_address) if self.debug: @@ -199,119 +247,168 @@ def command_parser(): """ build and return our command parser """ - - parser = argparse.ArgumentParser( - description='Send commands to running py3status instances' - ) - parser = argparse.ArgumentParser(add_help=True) - parser.add_argument( - '-v', - '--verbose', - action='store_true', - default=False, - dest='verbose', - help='print information', - ) - parser.add_argument( - '--version', - action='store_true', - default=False, - dest='version', - help='print version information', - ) - - subparsers = parser.add_subparsers(dest='command', help='commands') - subparsers.required = False - - # Refresh - refresh_parser = subparsers.add_parser( - 'refresh', help='refresh named module(s)' - ) - refresh_parser.add_argument(nargs='+', dest='module', help='module(s)') - - # Click - click_parser = subparsers.add_parser( - 'click', help='simulate click on named module(s)' - ) - click_parser.add_argument( - nargs='?', - type=int, - default=1, - dest='button', - help='button number to use', - ) - click_parser.add_argument(nargs='+', dest='module', help='module(s)') - - # add shortcut commands for named buttons - for k in sorted(BUTTONS, key=BUTTONS.get): - click_parser = subparsers.add_parser( - k, help='simulate %s on named module(s)' % k + class Parser(argparse.ArgumentParser): + # print usages and exit on errors + def error(self, message): + print('\x1b[1;31merror: \x1b[0m{}'.format(message)) + self.print_help() + self.exit(1) + + # hide aliases on errors + def _check_value(self, action, value): + if action.choices is not None and value not in action.choices: + raise argparse.ArgumentError( + action, "invalid choice: '{}'".format(value) + ) + + # make parser + parser = Parser(formatter_class=argparse.RawTextHelpFormatter) + + # parser: add verbose, version + for short, name, msg in INFORMATION: + short = '-%s' % short + arg = '--%s' % name + parser.add_argument(short, arg, action='store_true', help=msg) + + # make subparsers // ALIAS_DEPRECATION: remove metavar later + subparsers = parser.add_subparsers(dest='command', metavar='{click,refresh}') + sps = {} + + # subparsers: add refresh, click + for name, msg, nargs in SUBPARSERS: + sps[name] = subparsers.add_parser( + name, + epilog=eval('{}_EPILOG'.format(name.upper())), + formatter_class=argparse.RawTextHelpFormatter, + add_help=False, + help=msg ) + sps[name].add_argument(nargs=nargs, dest='module', help='module name') + + # ALIAS_DEPRECATION: subparsers: add click (aliases) + buttons = { + 'leftclick': 1, 'middleclick': 2, 'rightclick': 3, + 'scrollup': 4, 'scrolldown': 5 + } + for name in sorted(buttons): + sps[name] = subparsers.add_parser(name) + sps[name].add_argument(nargs='+', dest='module', help='module name') + + # click subparser: add button, index, width, height, relative_{x,y}, x, y + sp = sps['click'] + for name, msg in CLICK_OPTIONS: + arg = '--{}'.format(name) + if name == 'button': + sp.add_argument(arg, metavar='INT', type=int, help=msg, default=1) + elif name == 'index': + sp.add_argument(arg, metavar='INDEX', help=msg) + else: + sp.add_argument(arg, metavar='INT', type=int, help=msg) + + # refresh subparser: add all + sp = sps['refresh'] + for name, msg in REFRESH_OPTIONS: + arg = '--{}'.format(name) + sp.add_argument(arg, action='store_true', help=msg) + + # parse args, post-processing + options = parser.parse_args() + + if options.command == 'click': + # cast string index to int + if options.index: + try: + options.index = int(options.index) + except: + pass + elif options.command == 'refresh': + # refresh all + # ALL_DEPRECATION + if options.module is None: + options.module = [] + # end + valid = False + if options.all: # keep this + options.command = 'refresh_all' + options.module = [] + valid = True + if 'all' in options.module: # remove this later + options.command = 'refresh_all' + options.module = [] + valid = True + if not options.module and not valid: + sps['refresh'].error('missing positional or optional arguments') + elif options.version: + # print version + from platform import python_version + from py3status.version import version + print('py3status {} (python {})'.format(version, python_version())) + parser.exit() + elif not options.command: + parser.error('too few arguments') + + # ALIAS_DEPRECATION + alias = options.command in buttons + + # py3-cmd click 3 dpms ==> py3-cmd click --button 3 dpms + new_modules = [] + for index, name in enumerate(options.module): + if name.isdigit(): + if alias: + continue + if not index: # zero index + options.button = int(name) + else: + new_modules.append(name) + + # ALIAS_DEPRECATION: Convert (click) aliases to buttons + if alias: + options.button = buttons[options.command] + options.command = 'click' - click_parser.add_argument(nargs='+', dest='module', help='module(s)') + if options.command == 'click' and not new_modules: + sps[options.command].error('too few arguments') + options.module = new_modules - return parser + return options def send_command(): """ - Run a remote command. - This is called via the py3status-command utility. + Run a remote command. This is called via py3-cmd utility. We look for any uds sockets with the correct name prefix and send our command to all that we find. This allows us to communicate with multiple py3status instances. """ - def output(msg): + def verbose(msg): """ print output if verbose is set. """ if options.verbose: print(msg) - parser = command_parser() - options = parser.parse_args() - - # convert named buttons to click command for processing - if options.command in BUTTONS: - options.button = BUTTONS[options.command] - options.command = 'click' - - if options.command == 'refresh' and 'all' in options.module: - options.command = 'refresh_all' - - if options.version: - import platform - print('py3status-command version {} (python {})'.format( - version, platform.python_version() - )) - sys.exit(0) - - if options.command: - msg = json.dumps(vars(options)) - else: - sys.exit(1) - - msg = msg.encode('utf-8') + options = command_parser() + msg = json.dumps(vars(options)).encode('utf-8') if len(msg) > MAX_SIZE: - output('Message length too long, max length (%s)' % MAX_SIZE) + verbose('Message length too long, max length (%s)' % MAX_SIZE) # find all likely socket addresses uds_list = glob.glob('{}.[0-9]*'.format(SERVER_ADDRESS)) - output('message "%s"' % msg) + verbose('message "%s"' % msg) for uds in uds_list: # Create a UDS socket sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) # Connect the socket to the port where the server is listening - output('connecting to %s' % uds) + verbose('connecting to %s' % uds) try: sock.connect(uds) except socket.error: # this is a stale socket so delete it - output('stale socket deleting') + verbose('stale socket deleting') try: os.unlink(uds) except OSError: @@ -319,9 +416,8 @@ def output(msg): continue try: # Send data - output('sending') + verbose('sending') sock.sendall(msg) - finally: - output('closing socket') + verbose('closing socket') sock.close() diff --git a/py3status/formatter.py b/py3status/formatter.py index 2a8da5c458..25be20b857 100644 --- a/py3status/formatter.py +++ b/py3status/formatter.py @@ -255,8 +255,8 @@ def get(self, get_params, block): value = float(value) if 'd' in self.format: value = int(float(value)) - output = u'{%s%s}' % (self.key, self.format) - value = output.format(**{self.key: value}) + output = u'{[%s]%s}' % (self.key, self.format) + value = output.format({self.key: value}) value_ = float(value) except ValueError: pass diff --git a/py3status/modules/air_quality.py b/py3status/modules/air_quality.py index 0e531a90de..cdcbe8f94d 100644 --- a/py3status/modules/air_quality.py +++ b/py3status/modules/air_quality.py @@ -154,7 +154,7 @@ def _get_aqi_data(self): self.url, params=self.auth_token, timeout=self.request_timeout ).json() except self.py3.RequestException: - return {} + return None def _organize(self, data): new_data = {} @@ -178,11 +178,12 @@ def _manipulate(self, data): def air_quality(self): aqi_data = self._get_aqi_data() - if aqi_data.get('status') == 'ok': - aqi_data = self._organize(aqi_data) - aqi_data = self._manipulate(aqi_data) - elif aqi_data.get('status') == 'error': - self.py3.error(aqi_data.get('data')) + if aqi_data: + if aqi_data.get('status') == 'ok': + aqi_data = self._organize(aqi_data) + aqi_data = self._manipulate(aqi_data) + elif aqi_data.get('status') == 'error': + self.py3.error(aqi_data.get('data')) return { 'cached_until': self.py3.time_in(self.cache_timeout), diff --git a/py3status/modules/arch_updates.py b/py3status/modules/arch_updates.py index cd95db2b67..9398b61243 100644 --- a/py3status/modules/arch_updates.py +++ b/py3status/modules/arch_updates.py @@ -69,12 +69,16 @@ def post_config_hook(self): self.include_aur = False def check_updates(self): - pacman_updates = self._check_pacman_updates() - aur_updates = self._check_aur_updates() - if aur_updates == '?': - total = pacman_updates - else: - total = pacman_updates + aur_updates + pacman_updates = aur_updates = total = None + + if self.include_pacman: + pacman_updates = self._check_pacman_updates() + + if self.include_aur: + aur_updates = self._check_aur_updates() + + if pacman_updates is not None or aur_updates is not None: + total = (pacman_updates or 0) + (aur_updates or 0) if self.hide_if_zero and total == 0: full_text = '' @@ -97,10 +101,12 @@ def _check_pacman_updates(self): This method will use the 'checkupdates' command line utility to determine how many updates are waiting to be installed via 'pacman -Syu'. + Returns: None if unable to determine number of pending updates """ - if not self.include_pacman: - return 0 - pending_updates = str(subprocess.check_output(["checkupdates"])) + try: + pending_updates = str(subprocess.check_output(["checkupdates"])) + except subprocess.CalledProcessError: + return None return pending_updates.count(LINE_SEPARATOR) def _check_aur_updates(self): @@ -108,21 +114,18 @@ def _check_aur_updates(self): This method will use the 'cower' command line utility to determine how many updates are waiting to be installed from the AUR. + Returns: None if unable to determine number of pending updates """ # For reasons best known to its author, 'cower' returns a non-zero # status code upon successful execution, if there is any output. # See https://github.com/falconindy/cower/blob/master/cower.c#L2596 - if not self.include_aur: - return '?' - pending_updates = b"" try: - pending_updates = str(subprocess.check_output(["cower", "-u"])) + subprocess.check_output(["cower", "--update"]) except subprocess.CalledProcessError as cp_error: pending_updates = cp_error.output - except: - pending_updates = '?' - return str(pending_updates).count(LINE_SEPARATOR) + return str(pending_updates).count(LINE_SEPARATOR) + return None if __name__ == "__main__": diff --git a/py3status/modules/timer.py b/py3status/modules/timer.py index 7d0e3db459..a6ce969c3c 100644 --- a/py3status/modules/timer.py +++ b/py3status/modules/timer.py @@ -100,7 +100,7 @@ def make_2_didget(value): # Hours hours, t = divmod(t, 3600) # Minutes - mins, t = divmod(t, 60) + minutes, t = divmod(t, 60) # Seconds seconds = t @@ -119,9 +119,9 @@ def make_2_didget(value): 'full_text': ':', }, { - 'full_text': make_2_didget(mins), + 'full_text': make_2_didget(minutes), 'color': self.color, - 'index': 'mins', + 'index': 'minutes', }, { 'full_text': ':', @@ -146,7 +146,7 @@ def make_2_didget(value): def on_click(self, event): deltas = { 'hours': 3600, - 'mins': 60, + 'minutes': 60, 'seconds': 1 } index = event['index'] diff --git a/py3status/modules/volume_status.py b/py3status/modules/volume_status.py index f9eb28f289..8d830a7543 100644 --- a/py3status/modules/volume_status.py +++ b/py3status/modules/volume_status.py @@ -302,8 +302,11 @@ def deprecate_function(config): def post_config_hook(self): if not self.command: - self.command = self.py3.check_commands( - ['pamixer', 'pactl', 'amixer']) + commands = ['pamixer', 'pactl', 'amixer'] + # pamixer, pactl requires pulseaudio to work + if not self.py3.check_commands('pulseaudio'): + commands = ['amixer'] + self.command = self.py3.check_commands(commands) elif self.command not in ['amixer', 'pamixer', 'pactl']: raise Exception(STRING_ERROR % self.command) elif not self.py3.check_commands(self.command): diff --git a/py3status/modules/xrandr.py b/py3status/modules/xrandr.py index 71846e3a52..9f0a09626a 100644 --- a/py3status/modules/xrandr.py +++ b/py3status/modules/xrandr.py @@ -18,6 +18,8 @@ Configuration parameters: cache_timeout: how often to (re)detect the outputs (default 10) + command: a custom command to be run after display configuration changes + (default None) fallback: when the current output layout is not available anymore, fallback to this layout if available. This is very handy if you have a laptop and switched to an external screen for presentation @@ -120,6 +122,7 @@ class Py3status: """ # available configuration parameters cache_timeout = 10 + command = None fallback = True fixed_width = True force_on_start = None @@ -333,6 +336,9 @@ def _apply(self, force=False): self.active_mode = mode self.py3.log('command "{}" exit code {}'.format(cmd, code)) + if self.command: + self.py3.command_run(self.command) + # move workspaces to outputs as configured self._apply_workspaces(combination, mode)