From bbebef353f95aca512fb61e83cb28fddc91a70f8 Mon Sep 17 00:00:00 2001 From: lasers Date: Sun, 13 May 2018 19:21:09 -0500 Subject: [PATCH 01/15] formatter: parse placeholders with periods --- py3status/formatter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From ad6909770ef3e5739855c098a7e9f5670fb5ae43 Mon Sep 17 00:00:00 2001 From: btmcg Date: Fri, 13 Jul 2018 02:53:14 -0400 Subject: [PATCH 02/15] arch_updates: Better error handling when no internet connection This fixes the exception that occurs when the module runs without an internet connection. It also reworks the two methods (pacman, aur) of update-checking to differentiate between 0 updates and errors (when there is no internet connection). --- py3status/modules/arch_updates.py | 35 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) 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__": From 5957cdf241690f344c08ec31daeb6d6a84588516 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Wed, 18 Jul 2018 12:14:04 +0200 Subject: [PATCH 03/15] fix AttributeError: 'NoneType' object has no attribute 'get' when api not reply well --- py3status/modules/air_quality.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/py3status/modules/air_quality.py b/py3status/modules/air_quality.py index 0e531a90de..54e0312f83 100644 --- a/py3status/modules/air_quality.py +++ b/py3status/modules/air_quality.py @@ -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), From 26fe212ec5e194cc9059169fbfa82f1028431eec Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Thu, 19 Jul 2018 08:33:33 +0200 Subject: [PATCH 04/15] return None instead of {} on RequestException --- py3status/modules/air_quality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py3status/modules/air_quality.py b/py3status/modules/air_quality.py index 54e0312f83..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 = {} From 6569badb07448b6eeff66cc69121c65d641bc906 Mon Sep 17 00:00:00 2001 From: Evy Bongers Date: Thu, 19 Jul 2018 22:05:46 +0200 Subject: [PATCH 05/15] Add support for setting background --- py3status/modules/xrandr.py | 42 +++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/py3status/modules/xrandr.py b/py3status/modules/xrandr.py index 71846e3a52..fe3a9898ac 100644 --- a/py3status/modules/xrandr.py +++ b/py3status/modules/xrandr.py @@ -109,8 +109,7 @@ """ -from collections import deque -from collections import OrderedDict +from collections import OrderedDict, deque from itertools import combinations from time import sleep @@ -119,6 +118,7 @@ class Py3status: """ """ # available configuration parameters + background_command = None cache_timeout = 10 fallback = True fixed_width = True @@ -233,8 +233,8 @@ def _set_available_combinations(self): # Preserve the order in which user defined the output combinations if whitelist: - available = reversed([comb for comb in whitelist - if comb in available]) + available = reversed( + [comb for comb in whitelist if comb in available]) self.available_combinations = deque(available) self.combinations_map = combinations_map @@ -256,8 +256,8 @@ def _choose_what_to_display(self, force_refresh=False): we display the last selected combination. """ for _ in range(len(self.available_combinations)): - if (self.displayed is None and - self.available_combinations[0] == self.active_layout): + if (self.displayed is None + and self.available_combinations[0] == self.active_layout): self.displayed = self.available_combinations[0] break else: @@ -304,24 +304,24 @@ def _apply(self, force=False): pos = getattr(self, '{}_pos'.format(output), '0x0') rotation = getattr(self, '{}_rotate'.format(output), 'normal') resolution = getattr(self, '{}_mode'.format(output), None) - resolution = '--mode {}'.format(resolution) if resolution else '--auto' + resolution = '--mode {}'.format( + resolution) if resolution else '--auto' if rotation not in ['inverted', 'left', 'normal', 'right']: - self.py3.log('configured rotation {} is not valid'.format( - rotation)) + self.py3.log( + 'configured rotation {} is not valid'.format(rotation)) rotation = 'normal' # if mode == 'clone' and previous_output is not None: cmd += ' {} --same-as {}'.format(resolution, previous_output) else: - if ('above' in pos or 'below' in pos or 'left-of' in pos or - 'right-of' in pos): - cmd += ' {} --{} --rotate {}'.format(resolution, pos, - rotation) + if ('above' in pos or 'below' in pos or 'left-of' in pos + or 'right-of' in pos): + cmd += ' {} --{} --rotate {}'.format( + resolution, pos, rotation) else: - cmd += ' {} --pos {} --rotate {}'.format(resolution, - pos, - rotation) + cmd += ' {} --pos {} --rotate {}'.format( + resolution, pos, rotation) previous_output = output else: cmd += ' --off' @@ -333,9 +333,14 @@ def _apply(self, force=False): self.active_mode = mode self.py3.log('command "{}" exit code {}'.format(cmd, code)) + self._apply_background() # move workspaces to outputs as configured self._apply_workspaces(combination, mode) + def _apply_background(self): + if self.background_command: + self.py3.command_run(self.background_command) + def _apply_workspaces(self, combination, mode): """ Allows user to force move a comma separated list of workspaces to the @@ -371,6 +376,7 @@ def _fallback_to_available_output(self): on your laptop by switching back to the integrated screen automatically ! """ + if len(self.active_comb) == 1: self._choose_what_to_display(force_refresh=True) self._apply() @@ -426,8 +432,8 @@ def xrandr(self): self._set_available_combinations() self._choose_what_to_display() - if (len(self.available_combinations) < 2 and - self.hide_if_single_combination): + if (len(self.available_combinations) < 2 + and self.hide_if_single_combination): full_text = self.py3.safe_format(self.format, {'output': ''}) else: if self.fixed_width is True: From 29624d6da2494f10b3c0e166694e15b95dd45afd Mon Sep 17 00:00:00 2001 From: Evy Bongers Date: Fri, 20 Jul 2018 17:31:50 +0200 Subject: [PATCH 06/15] Restore code format to satisfy flake8 check --- py3status/modules/xrandr.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/py3status/modules/xrandr.py b/py3status/modules/xrandr.py index fe3a9898ac..8780c52844 100644 --- a/py3status/modules/xrandr.py +++ b/py3status/modules/xrandr.py @@ -109,7 +109,8 @@ """ -from collections import OrderedDict, deque +from collections import deque +from collections import OrderedDict from itertools import combinations from time import sleep @@ -233,8 +234,8 @@ def _set_available_combinations(self): # Preserve the order in which user defined the output combinations if whitelist: - available = reversed( - [comb for comb in whitelist if comb in available]) + available = reversed([comb for comb in whitelist + if comb in available]) self.available_combinations = deque(available) self.combinations_map = combinations_map @@ -256,8 +257,8 @@ def _choose_what_to_display(self, force_refresh=False): we display the last selected combination. """ for _ in range(len(self.available_combinations)): - if (self.displayed is None - and self.available_combinations[0] == self.active_layout): + if (self.displayed is None and + self.available_combinations[0] == self.active_layout): self.displayed = self.available_combinations[0] break else: @@ -304,24 +305,24 @@ def _apply(self, force=False): pos = getattr(self, '{}_pos'.format(output), '0x0') rotation = getattr(self, '{}_rotate'.format(output), 'normal') resolution = getattr(self, '{}_mode'.format(output), None) - resolution = '--mode {}'.format( - resolution) if resolution else '--auto' + resolution = '--mode {}'.format(resolution) if resolution else '--auto' if rotation not in ['inverted', 'left', 'normal', 'right']: - self.py3.log( - 'configured rotation {} is not valid'.format(rotation)) + self.py3.log('configured rotation {} is not valid'.format( + rotation)) rotation = 'normal' # if mode == 'clone' and previous_output is not None: cmd += ' {} --same-as {}'.format(resolution, previous_output) else: - if ('above' in pos or 'below' in pos or 'left-of' in pos - or 'right-of' in pos): - cmd += ' {} --{} --rotate {}'.format( - resolution, pos, rotation) + if ('above' in pos or 'below' in pos or 'left-of' in pos or + 'right-of' in pos): + cmd += ' {} --{} --rotate {}'.format(resolution, pos, + rotation) else: - cmd += ' {} --pos {} --rotate {}'.format( - resolution, pos, rotation) + cmd += ' {} --pos {} --rotate {}'.format(resolution, + pos, + rotation) previous_output = output else: cmd += ' --off' @@ -432,8 +433,8 @@ def xrandr(self): self._set_available_combinations() self._choose_what_to_display() - if (len(self.available_combinations) < 2 - and self.hide_if_single_combination): + if (len(self.available_combinations) < 2 and + self.hide_if_single_combination): full_text = self.py3.safe_format(self.format, {'output': ''}) else: if self.fixed_width is True: From c91d82fe760dff4f51f936c160d39b3b42c18215 Mon Sep 17 00:00:00 2001 From: Evy Bongers Date: Fri, 20 Jul 2018 17:34:45 +0200 Subject: [PATCH 07/15] Apply suggestions by @lasers in PR #1397 - Rename 'background_command' to 'command' as it can be used as general purpose command - Removed '_apply_background' method in favor or adding the two lines of code directly in '_apply' --- py3status/modules/xrandr.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/py3status/modules/xrandr.py b/py3status/modules/xrandr.py index 8780c52844..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 @@ -119,8 +121,8 @@ class Py3status: """ """ # available configuration parameters - background_command = None cache_timeout = 10 + command = None fallback = True fixed_width = True force_on_start = None @@ -334,14 +336,12 @@ def _apply(self, force=False): self.active_mode = mode self.py3.log('command "{}" exit code {}'.format(cmd, code)) - self._apply_background() + if self.command: + self.py3.command_run(self.command) + # move workspaces to outputs as configured self._apply_workspaces(combination, mode) - def _apply_background(self): - if self.background_command: - self.py3.command_run(self.background_command) - def _apply_workspaces(self, combination, mode): """ Allows user to force move a comma separated list of workspaces to the @@ -377,7 +377,6 @@ def _fallback_to_available_output(self): on your laptop by switching back to the integrated screen automatically ! """ - if len(self.active_comb) == 1: self._choose_what_to_display(force_refresh=True) self._apply() From 66f54ec36ac2d9a0153b2c2ddfe102af5a29005e Mon Sep 17 00:00:00 2001 From: lasers Date: Mon, 30 Jul 2018 16:41:10 -0500 Subject: [PATCH 08/15] volume_status: skip pactl if no pulseaudio --- py3status/modules/volume_status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/py3status/modules/volume_status.py b/py3status/modules/volume_status.py index f9eb28f289..62ddd62b07 100644 --- a/py3status/modules/volume_status.py +++ b/py3status/modules/volume_status.py @@ -304,6 +304,10 @@ def post_config_hook(self): if not self.command: self.command = self.py3.check_commands( ['pamixer', 'pactl', 'amixer']) + # pactl may be automatically installed, check for pulseaudio too + if self.command == 'pactl': + if not self.py3.check_commands('pulseaudio'): + self.command = self.py3.check_commands('amixer') elif self.command not in ['amixer', 'pamixer', 'pactl']: raise Exception(STRING_ERROR % self.command) elif not self.py3.check_commands(self.command): From 99d0187f924bb9b6df211083c315e49c51df9a19 Mon Sep 17 00:00:00 2001 From: lasers Date: Thu, 31 May 2018 05:26:02 -0500 Subject: [PATCH 09/15] timer: rename mins to minutes for cleaner docs --- py3status/modules/timer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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'] From 7788f8fb1289beabf381eeb2b8983ff34c588844 Mon Sep 17 00:00:00 2001 From: lasers Date: Sat, 2 Jun 2018 00:49:13 -0500 Subject: [PATCH 10/15] doc: update py3-cmd.rst to reflect new changes --- doc/py3-cmd.rst | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/doc/py3-cmd.rst b/doc/py3-cmd.rst index 333d0cbd98..d023a50b7a 100644 --- a/doc/py3-cmd.rst +++ b/doc/py3-cmd.rst @@ -18,16 +18,16 @@ 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 + # refresh all modules py3-cmd refresh all @@ -39,22 +39,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 From 9db97f62e64659db4858677f31044c576c5b801a Mon Sep 17 00:00:00 2001 From: lasers Date: Sat, 2 Jun 2018 00:24:19 -0500 Subject: [PATCH 11/15] py3-cmd: rename output to verbose --- py3status/command.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/py3status/command.py b/py3status/command.py index bd56dcad6a..6db287e5fd 100644 --- a/py3status/command.py +++ b/py3status/command.py @@ -263,7 +263,7 @@ def send_command(): py3status instances. """ - def output(msg): + def verbose(msg): """ print output if verbose is set. """ @@ -295,23 +295,23 @@ def output(msg): msg = msg.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 +319,9 @@ def output(msg): continue try: # Send data - output('sending') + verbose('sending') sock.sendall(msg) finally: - output('closing socket') + verbose('closing socket') sock.close() From 03ea40cf3d7038a92ea9c4d9e4c403eafc5bd2a7 Mon Sep 17 00:00:00 2001 From: lasers Date: Thu, 31 May 2018 04:38:15 -0500 Subject: [PATCH 12/15] py3-cmd: allow sending events with more options --- py3status/command.py | 275 +++++++++++++++++++++++++++---------------- 1 file changed, 174 insertions(+), 101 deletions(-) diff --git a/py3status/command.py b/py3status/command.py index 6db287e5fd..9d8f7af855 100644 --- a/py3status/command.py +++ b/py3status/command.py @@ -3,21 +3,73 @@ 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. +""" +INFORMATION = [ + ('V', 'version', 'show version number and exit'), + ('v', 'verbose', 'enable verbose mode'), +] +SUBPARSERS = [ + ('click', 'click modules'), + ('refresh', 'refresh modules'), +] +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'), +] class CommandRunner: @@ -75,9 +127,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 +136,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 +174,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 +186,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,64 +242,117 @@ 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 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='+', 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) + + # 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 + if 'all' in options.module: + options.command = 'refresh_all' + options.module = [] + 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 @@ -270,30 +366,8 @@ def verbose(msg): 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: verbose('Message length too long, max length (%s)' % MAX_SIZE) @@ -321,7 +395,6 @@ def verbose(msg): # Send data verbose('sending') sock.sendall(msg) - finally: verbose('closing socket') sock.close() From a77371be801702135c03a82448febeeeafe0be94 Mon Sep 17 00:00:00 2001 From: lasers Date: Wed, 25 Jul 2018 06:51:02 -0500 Subject: [PATCH 13/15] py3-cmd: rename all to --all --- doc/py3-cmd.rst | 3 +-- py3status/command.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/doc/py3-cmd.rst b/doc/py3-cmd.rst index d023a50b7a..f2f8fd644f 100644 --- a/doc/py3-cmd.rst +++ b/doc/py3-cmd.rst @@ -14,7 +14,6 @@ refresh ^^^^^^^ Cause named module(s) to have their output refreshed. -To refresh all modules use the ``all`` keyword. .. code-block:: shell @@ -28,7 +27,7 @@ To refresh all modules use the ``all`` keyword. py3-cmd refresh "weather_yahoo chicago" # refresh all modules - py3-cmd refresh all + py3-cmd refresh --all click diff --git a/py3status/command.py b/py3status/command.py index 9d8f7af855..af016492dc 100644 --- a/py3status/command.py +++ b/py3status/command.py @@ -21,7 +21,7 @@ py3-cmd refresh "weather_yahoo chicago" # refresh all modules - py3-cmd refresh all + py3-cmd refresh --all """ CLICK_EPILOG = """ examples: @@ -57,8 +57,8 @@ ('v', 'verbose', 'enable verbose mode'), ] SUBPARSERS = [ - ('click', 'click modules'), - ('refresh', 'refresh modules'), + ('click', 'click modules', '+'), + ('refresh', 'refresh modules', '*'), ] CLICK_OPTIONS = [ ('button', 'specify a button number (default %(default)s)'), @@ -70,6 +70,9 @@ ('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: @@ -270,7 +273,7 @@ def _check_value(self, action, value): sps = {} # subparsers: add refresh, click - for name, msg in SUBPARSERS: + for name, msg, nargs in SUBPARSERS: sps[name] = subparsers.add_parser( name, epilog=eval('{}_EPILOG'.format(name.upper())), @@ -278,7 +281,7 @@ def _check_value(self, action, value): add_help=False, help=msg ) - sps[name].add_argument(nargs='+', dest='module', help='module name') + sps[name].add_argument(nargs=nargs, dest='module', help='module name') # ALIAS_DEPRECATION: subparsers: add click (aliases) buttons = { @@ -300,6 +303,12 @@ def _check_value(self, action, value): 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() @@ -312,9 +321,21 @@ def _check_value(self, action, value): pass elif options.command == 'refresh': # refresh all - if 'all' in options.module: + # 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 6e10f3dca0dc89435c640f5c31b81f35cfb2b5b2 Mon Sep 17 00:00:00 2001 From: lasers Date: Wed, 25 Jul 2018 11:02:26 -0500 Subject: [PATCH 14/15] py3-cmd: create exec subparser for tobes --- py3status/command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py3status/command.py b/py3status/command.py index af016492dc..0bca8ec272 100644 --- a/py3status/command.py +++ b/py3status/command.py @@ -52,6 +52,7 @@ # 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'), @@ -59,6 +60,7 @@ SUBPARSERS = [ ('click', 'click modules', '+'), ('refresh', 'refresh modules', '*'), + # ('exec', 'execute methods', '+'), ] CLICK_OPTIONS = [ ('button', 'specify a button number (default %(default)s)'), From fbfa64dff3e47e84df75a9e1583065a6ea5d7b00 Mon Sep 17 00:00:00 2001 From: lasers Date: Mon, 30 Jul 2018 23:11:17 -0500 Subject: [PATCH 15/15] volume_status: skip pamixer if no pulseaudio --- py3status/modules/volume_status.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/py3status/modules/volume_status.py b/py3status/modules/volume_status.py index 62ddd62b07..8d830a7543 100644 --- a/py3status/modules/volume_status.py +++ b/py3status/modules/volume_status.py @@ -302,12 +302,11 @@ def deprecate_function(config): def post_config_hook(self): if not self.command: - self.command = self.py3.check_commands( - ['pamixer', 'pactl', 'amixer']) - # pactl may be automatically installed, check for pulseaudio too - if self.command == 'pactl': - if not self.py3.check_commands('pulseaudio'): - self.command = self.py3.check_commands('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):