From 398d449956e8b5b1ecb3377c5c4a0a6f3aa8a489 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Mon, 21 Feb 2022 05:15:18 +0100 Subject: [PATCH 01/15] first (unconditional) use of tmux status formatting --- py3status/core.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/py3status/core.py b/py3status/core.py index 0f399335db..31f78eb2a0 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -1016,7 +1016,12 @@ def process_module_output(self, module): if "color" not in output: output["color"] = color # Create the json string output. - return ",".join(dumps(x) for x in outputs) + for output in outputs: + if 'color' in output: + output['full_text'] = f"#[fg={output['color']}]{output['full_text']}#[fg=white]" + return "".join(x['full_text'] for x in outputs) + + def i3bar_stop(self, signum, frame): if ( @@ -1098,8 +1103,8 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - write(dumps(header)) - write("\n[[]\n") + #write(dumps(header)) + #write("\n[[]\n") update_due = None # main loop @@ -1127,7 +1132,7 @@ def run(self): output[index] = out # build output string - out = ",".join(x for x in output if x) + out = "#[fg=brightblack]|#[fg=white]".join(x for x in output if x) # dump the line to stdout - write(f",[{out}]\n") + write(f"{out}\n") flush() From 2eea4e51598fd7006812394f5052c5e98cd383f6 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Mon, 21 Feb 2022 05:40:54 +0100 Subject: [PATCH 02/15] added command line option for tmux output --- py3status/argparsers.py | 14 +++++++++----- py3status/core.py | 35 ++++++++++++++++++++++------------- py3status/module_test.py | 2 +- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/py3status/argparsers.py b/py3status/argparsers.py index cccc64d6a4..87fb4b07bf 100644 --- a/py3status/argparsers.py +++ b/py3status/argparsers.py @@ -18,11 +18,14 @@ def parse_cli_args(): xdg_dirs_path = Path(os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg")) # get window manager + # defaults to i3, if running, otherwise sway, if running, otherwise i3. with Path(os.devnull).open("w") as devnull: if subprocess.call(["pgrep", "-x", "i3"], stdout=devnull) == 0: wm = "i3" - else: + elif subprocess.call(["pgrep", "-x", "sway"], stdout=devnull) == 0: wm = "sway" + else: + wm = "i3" # i3status config file default detection # respect i3status' file detection order wrt issue #43 @@ -160,8 +163,8 @@ def _format_action_invocation(self, action): dest="wm", metavar="WINDOW_MANAGER", default=wm, - choices=["i3", "sway"], - help="specify window manager i3 or sway", + choices=["i3", "sway", "tmux"], + help="specify window manager i3, sway or tmux", ) # deprecations @@ -181,8 +184,9 @@ def _format_action_invocation(self, action): # get wm options.wm_name = options.wm options.wm = { - "i3": {"msg": "i3-msg", "nag": "i3-nagbar"}, - "sway": {"msg": "swaymsg", "nag": "swaynag"}, + "i3": {"msg": ["i3-msg"], "nag": ["i3-nagbar"]}, + "sway": {"msg": ["swaymsg"], "nag": ["swaynag"]}, + "tmux": {"msg": ["tmux", "display-message"], "nag": ["tmux", "display-message"]}, }[options.wm] # make include path to search for user modules if None diff --git a/py3status/core.py b/py3status/core.py index 31f78eb2a0..5310f66e67 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -747,8 +747,8 @@ def notify_user( else: msg = f"py3status: {msg}" if level != "info" and module_name == "": - fix_msg = "{} Please try to fix this and reload i3wm (Mod+Shift+R)" - msg = fix_msg.format(msg) + fix_msg = "{} Please try to fix this and reload {}" + msg = fix_msg.format(msg, config['wm_name']) # Rate limiting. If rate limiting then we need to calculate the time # period for which the message should not be repeated. We just use # A simple chunked time model where a message cannot be repeated in a @@ -790,7 +790,7 @@ def notify_user( py3_config = self.config.get("py3_config", {}) nagbar_font = py3_config.get("py3status", {}).get("nagbar_font") wm_nag = self.config["wm"]["nag"] - cmd = [wm_nag, "-m", msg, "-t", level] + cmd = wm_nag + ["-m", msg, "-t", level] if nagbar_font: cmd += ["-f", nagbar_font] Popen( @@ -1015,11 +1015,15 @@ def process_module_output(self, module): # Color: substitute the config defined color if "color" not in output: output["color"] = color + # Create the tmux string output. + if self.options.wm_name == 'tmux': + for output in outputs: + if 'color' in output: + output['full_text'] = f"#[fg={output['color']}]{output['full_text']}#[fg=white]" + return "".join(x['full_text'] for x in outputs) # Create the json string output. - for output in outputs: - if 'color' in output: - output['full_text'] = f"#[fg={output['color']}]{output['full_text']}#[fg=white]" - return "".join(x['full_text'] for x in outputs) + else: + return ",".join(dumps(x) for x in outputs) @@ -1103,8 +1107,9 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - #write(dumps(header)) - #write("\n[[]\n") + if self.options.wm_name != "tmux": + write(dumps(header)) + write("\n[[]\n") update_due = None # main loop @@ -1131,8 +1136,12 @@ def run(self): # store the output as json output[index] = out - # build output string - out = "#[fg=brightblack]|#[fg=white]".join(x for x in output if x) - # dump the line to stdout - write(f"{out}\n") + # build output string and dump to stdout + out = "" + if self.options.wm_name == "tmux": + out = "#[fg=brightblack]|#[fg=white]".join(x for x in output if x) + write(f"{out}\n") + else: + out = ",".join(x for x in output if x) + write(f",[{out}]\n") flush() diff --git a/py3status/module_test.py b/py3status/module_test.py index d0177bc441..4f0513e968 100644 --- a/py3status/module_test.py +++ b/py3status/module_test.py @@ -27,7 +27,7 @@ def __init__(self, config): "minimum_interval": 0.1, "testing": True, "log_file": True, - "wm": {"msg": "i3-msg", "nag": "i3-nagbar"}, + "wm": {"msg": ["i3-msg"], "nag": ["i3-nagbar"]}, } self.events_thread = self.EventThread() self.udev_monitor = self.UdevMonitor() From 1a9d777537165b089b02f924ace44643dcef83d9 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Tue, 22 Feb 2022 07:26:54 +0100 Subject: [PATCH 03/15] fixing color codes to lowercase, because tmux expands #F in #FF0000 before evaluating colors (probably a bug in tmux) --- py3status/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py3status/core.py b/py3status/core.py index 5310f66e67..365783f855 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -1019,7 +1019,7 @@ def process_module_output(self, module): if self.options.wm_name == 'tmux': for output in outputs: if 'color' in output: - output['full_text'] = f"#[fg={output['color']}]{output['full_text']}#[fg=white]" + output['full_text'] = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" return "".join(x['full_text'] for x in outputs) # Create the json string output. else: @@ -1139,7 +1139,7 @@ def run(self): # build output string and dump to stdout out = "" if self.options.wm_name == "tmux": - out = "#[fg=brightblack]|#[fg=white]".join(x for x in output if x) + out = "#[fg=brightblack]|#[default]".join(x for x in output if x) write(f"{out}\n") else: out = ",".join(x for x in output if x) From 9681d9040fb9aae4326dd2db938bc9c06cd7e502 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Sun, 27 Feb 2022 19:33:15 +0100 Subject: [PATCH 04/15] output format selection tmux / i3bar now via output_format setting in py3status conf instead of --wm cli switch --- py3status/argparsers.py | 14 +++++--------- py3status/core.py | 12 ++++++------ py3status/module_test.py | 2 +- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/py3status/argparsers.py b/py3status/argparsers.py index 87fb4b07bf..cccc64d6a4 100644 --- a/py3status/argparsers.py +++ b/py3status/argparsers.py @@ -18,14 +18,11 @@ def parse_cli_args(): xdg_dirs_path = Path(os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg")) # get window manager - # defaults to i3, if running, otherwise sway, if running, otherwise i3. with Path(os.devnull).open("w") as devnull: if subprocess.call(["pgrep", "-x", "i3"], stdout=devnull) == 0: wm = "i3" - elif subprocess.call(["pgrep", "-x", "sway"], stdout=devnull) == 0: - wm = "sway" else: - wm = "i3" + wm = "sway" # i3status config file default detection # respect i3status' file detection order wrt issue #43 @@ -163,8 +160,8 @@ def _format_action_invocation(self, action): dest="wm", metavar="WINDOW_MANAGER", default=wm, - choices=["i3", "sway", "tmux"], - help="specify window manager i3, sway or tmux", + choices=["i3", "sway"], + help="specify window manager i3 or sway", ) # deprecations @@ -184,9 +181,8 @@ def _format_action_invocation(self, action): # get wm options.wm_name = options.wm options.wm = { - "i3": {"msg": ["i3-msg"], "nag": ["i3-nagbar"]}, - "sway": {"msg": ["swaymsg"], "nag": ["swaynag"]}, - "tmux": {"msg": ["tmux", "display-message"], "nag": ["tmux", "display-message"]}, + "i3": {"msg": "i3-msg", "nag": "i3-nagbar"}, + "sway": {"msg": "swaymsg", "nag": "swaynag"}, }[options.wm] # make include path to search for user modules if None diff --git a/py3status/core.py b/py3status/core.py index 365783f855..696ac2891f 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -747,8 +747,8 @@ def notify_user( else: msg = f"py3status: {msg}" if level != "info" and module_name == "": - fix_msg = "{} Please try to fix this and reload {}" - msg = fix_msg.format(msg, config['wm_name']) + fix_msg = "{} Please try to fix this and reload i3wm (Mod+Shift+R)" + msg = fix_msg.format(msg) # Rate limiting. If rate limiting then we need to calculate the time # period for which the message should not be repeated. We just use # A simple chunked time model where a message cannot be repeated in a @@ -790,7 +790,7 @@ def notify_user( py3_config = self.config.get("py3_config", {}) nagbar_font = py3_config.get("py3status", {}).get("nagbar_font") wm_nag = self.config["wm"]["nag"] - cmd = wm_nag + ["-m", msg, "-t", level] + cmd = [wm_nag, "-m", msg, "-t", level] if nagbar_font: cmd += ["-f", nagbar_font] Popen( @@ -1016,7 +1016,7 @@ def process_module_output(self, module): if "color" not in output: output["color"] = color # Create the tmux string output. - if self.options.wm_name == 'tmux': + if self.config["py3_config"]["general"].get("output_format") == 'tmux': for output in outputs: if 'color' in output: output['full_text'] = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" @@ -1107,7 +1107,7 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - if self.options.wm_name != "tmux": + if self.config["py3_config"]["general"].get("output_format") == 'tmux': write(dumps(header)) write("\n[[]\n") @@ -1138,7 +1138,7 @@ def run(self): # build output string and dump to stdout out = "" - if self.options.wm_name == "tmux": + if self.config["py3_config"]["general"].get("output_format") == 'tmux': out = "#[fg=brightblack]|#[default]".join(x for x in output if x) write(f"{out}\n") else: diff --git a/py3status/module_test.py b/py3status/module_test.py index 4f0513e968..d0177bc441 100644 --- a/py3status/module_test.py +++ b/py3status/module_test.py @@ -27,7 +27,7 @@ def __init__(self, config): "minimum_interval": 0.1, "testing": True, "log_file": True, - "wm": {"msg": ["i3-msg"], "nag": ["i3-nagbar"]}, + "wm": {"msg": "i3-msg", "nag": "i3-nagbar"}, } self.events_thread = self.EventThread() self.udev_monitor = self.UdevMonitor() From c5f3ef2f8a3262f9c16ed9197e52c72d9fa4f92c Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Mon, 28 Feb 2022 07:08:16 +0100 Subject: [PATCH 05/15] fixing inverted logical comparison --- py3status/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py3status/core.py b/py3status/core.py index 696ac2891f..49a82f4b33 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -1107,7 +1107,7 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - if self.config["py3_config"]["general"].get("output_format") == 'tmux': + if self.config["py3_config"]["general"].get("output_format") != 'tmux': write(dumps(header)) write("\n[[]\n") From d813f3f81fab06217f5ee14e85b9d748abb2b4c5 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Tue, 1 Mar 2022 17:44:57 +0100 Subject: [PATCH 06/15] fixed tox reported formatting issues --- py3status/core.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/py3status/core.py b/py3status/core.py index 49a82f4b33..d914b3a4b8 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -1016,17 +1016,16 @@ def process_module_output(self, module): if "color" not in output: output["color"] = color # Create the tmux string output. - if self.config["py3_config"]["general"].get("output_format") == 'tmux': + if self.config["py3_config"]["general"].get("output_format") == "tmux": for output in outputs: - if 'color' in output: - output['full_text'] = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" - return "".join(x['full_text'] for x in outputs) + if "color" in output: + tmux_full_text = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" + output["full_text"] = tmux_full_text + return "".join(x["full_text"] for x in outputs) # Create the json string output. else: return ",".join(dumps(x) for x in outputs) - - def i3bar_stop(self, signum, frame): if ( self.next_allowed_signal == signum @@ -1107,7 +1106,7 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - if self.config["py3_config"]["general"].get("output_format") != 'tmux': + if self.config["py3_config"]["general"].get("output_format") != "tmux": write(dumps(header)) write("\n[[]\n") @@ -1138,7 +1137,7 @@ def run(self): # build output string and dump to stdout out = "" - if self.config["py3_config"]["general"].get("output_format") == 'tmux': + if self.config["py3_config"]["general"].get("output_format") == "tmux": out = "#[fg=brightblack]|#[default]".join(x for x in output if x) write(f"{out}\n") else: From 8f7c17a5ce61b3c4b9b3768eb37bd479f094c782 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Tue, 1 Mar 2022 18:40:35 +0100 Subject: [PATCH 07/15] refactoring output_format config query into setup --- py3status/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/py3status/core.py b/py3status/core.py index d914b3a4b8..f004708194 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -722,6 +722,10 @@ def setup(self): # load and spawn i3status.conf configured modules threads self.load_modules(self.py3_modules, user_modules) + self.output_format = self.config["py3_config"]["general"].get( + "output_format", "i3bar" + ) + def notify_user( self, msg, @@ -1016,7 +1020,7 @@ def process_module_output(self, module): if "color" not in output: output["color"] = color # Create the tmux string output. - if self.config["py3_config"]["general"].get("output_format") == "tmux": + if self.output_format == "tmux": for output in outputs: if "color" in output: tmux_full_text = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" @@ -1106,7 +1110,7 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - if self.config["py3_config"]["general"].get("output_format") != "tmux": + if self.output_format != "tmux": write(dumps(header)) write("\n[[]\n") @@ -1137,7 +1141,7 @@ def run(self): # build output string and dump to stdout out = "" - if self.config["py3_config"]["general"].get("output_format") == "tmux": + if self.output_format == "tmux": out = "#[fg=brightblack]|#[default]".join(x for x in output if x) write(f"{out}\n") else: From be71232abf436913272926a8c7ca98abba01cd29 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Thu, 3 Mar 2022 15:41:50 +0100 Subject: [PATCH 08/15] added a config option for output_format_separator, which is used by tmux --- py3status/constants.py | 5 +++++ py3status/core.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/py3status/constants.py b/py3status/constants.py index 2868625e38..de61a0a37e 100644 --- a/py3status/constants.py +++ b/py3status/constants.py @@ -10,6 +10,11 @@ "output_format": "i3bar", } +DEFAULT_SEPARATORS = { + "i3bar": None, + "tmux": "#[fg={color_separator}]|#[default]", +} + MAX_NESTING_LEVELS = 4 TIME_FORMAT = "%Y-%m-%d %H:%M:%S" diff --git a/py3status/core.py b/py3status/core.py index f004708194..c15426bb88 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -13,6 +13,7 @@ from traceback import extract_tb, format_tb, format_stack from py3status.command import CommandServer +from py3status.constants import DEFAULT_SEPARATORS from py3status.events import Events from py3status.formatter import expand_color from py3status.helpers import print_stderr @@ -722,10 +723,21 @@ def setup(self): # load and spawn i3status.conf configured modules threads self.load_modules(self.py3_modules, user_modules) - self.output_format = self.config["py3_config"]["general"].get( - "output_format", "i3bar" + # determine the target output format (i3bar or tmux) + self.output_format = self.config["py3_config"]["general"]["output_format"] + + # tmux allows configurable separators between bar entries. + # handle configured value or fall back to the matching default for output_format + self.output_format_separator = self.config["py3_config"]["py3status"].get( + "output_format_separator", DEFAULT_SEPARATORS[self.output_format] ) + # inject the value of color_separator into the separator string + if self.output_format_separator is not None: + self.output_format_separator = self.output_format_separator.format( + color_separator=self.config["py3_config"]["general"]["color_separator"] + ) + def notify_user( self, msg, @@ -1142,7 +1154,7 @@ def run(self): # build output string and dump to stdout out = "" if self.output_format == "tmux": - out = "#[fg=brightblack]|#[default]".join(x for x in output if x) + out = self.output_format_separator.join(x for x in output if x) write(f"{out}\n") else: out = ",".join(x for x in output if x) From 2002371062e8d5d0dec2a793668d2e0aaccc3209 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Sat, 5 Mar 2022 15:47:15 +0100 Subject: [PATCH 09/15] applied formatting suggestion in py3status/core.py Co-authored-by: lasers --- py3status/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/py3status/core.py b/py3status/core.py index c15426bb88..54ac9cc9f6 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -1035,8 +1035,9 @@ def process_module_output(self, module): if self.output_format == "tmux": for output in outputs: if "color" in output: - tmux_full_text = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" - output["full_text"] = tmux_full_text + output["full_text"] = ( + f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" + ) return "".join(x["full_text"] for x in outputs) # Create the json string output. else: From 119fc068eb1455e9a63eb79473770696c7cfa35b Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Sat, 5 Mar 2022 16:34:01 +0100 Subject: [PATCH 10/15] increased robustness of output separator with regards to unexpected values of output_format --- py3status/constants.py | 1 - py3status/core.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/py3status/constants.py b/py3status/constants.py index de61a0a37e..1febcd8d05 100644 --- a/py3status/constants.py +++ b/py3status/constants.py @@ -11,7 +11,6 @@ } DEFAULT_SEPARATORS = { - "i3bar": None, "tmux": "#[fg={color_separator}]|#[default]", } diff --git a/py3status/core.py b/py3status/core.py index 54ac9cc9f6..f787ece0bf 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -723,13 +723,13 @@ def setup(self): # load and spawn i3status.conf configured modules threads self.load_modules(self.py3_modules, user_modules) - # determine the target output format (i3bar or tmux) + # determine the target output format self.output_format = self.config["py3_config"]["general"]["output_format"] - # tmux allows configurable separators between bar entries. - # handle configured value or fall back to the matching default for output_format + # determine the separator between status entries, if needed + default_separator = DEFAULT_SEPARATORS.get(self.output_format, None) self.output_format_separator = self.config["py3_config"]["py3status"].get( - "output_format_separator", DEFAULT_SEPARATORS[self.output_format] + "output_format_separator", default_separator ) # inject the value of color_separator into the separator string From f5e213dc2eade2e6be8a333501abf2455694f5e8 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Sat, 5 Mar 2022 16:38:53 +0100 Subject: [PATCH 11/15] fixed tox-reported issue --- py3status/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py3status/core.py b/py3status/core.py index f787ece0bf..d8b9e95ecf 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -1035,9 +1035,9 @@ def process_module_output(self, module): if self.output_format == "tmux": for output in outputs: if "color" in output: - output["full_text"] = ( - f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" - ) + output[ + "full_text" + ] = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" return "".join(x["full_text"] for x in outputs) # Create the json string output. else: From e704355d0775a9256d8ca4d35faca508370b39ae Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Sun, 6 Mar 2022 11:53:38 +0100 Subject: [PATCH 12/15] forcing i3bar output_format in the spawned i3status process configuration --- py3status/i3status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/py3status/i3status.py b/py3status/i3status.py index d6450944e6..8fc97e5500 100644 --- a/py3status/i3status.py +++ b/py3status/i3status.py @@ -330,6 +330,10 @@ def write_tmp_i3status_config(self, tmpfile): value = TZTIME_FORMAT if key == "format_time": continue + # Set output_format to i3bar for parsing regardless of what + # formatting we apply ourselves before printing + if key == "output_format": + value = "i3bar" if isinstance(value, bool): value = f"{value}".lower() self.write_in_tmpfile(f' {key} = "{value}"\n', tmpfile) From c7830d44d399460ac0338fc7a1770b614a35cde7 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Sun, 6 Mar 2022 14:45:35 +0100 Subject: [PATCH 13/15] implemented all output_format settings supported by i3status --- py3status/constants.py | 14 ++++++- py3status/core.py | 93 +++++++++++++++++++++++++++++++----------- py3status/module.py | 16 +++++--- 3 files changed, 92 insertions(+), 31 deletions(-) diff --git a/py3status/constants.py b/py3status/constants.py index 1febcd8d05..f2fcd5ff25 100644 --- a/py3status/constants.py +++ b/py3status/constants.py @@ -10,8 +10,18 @@ "output_format": "i3bar", } -DEFAULT_SEPARATORS = { - "tmux": "#[fg={color_separator}]|#[default]", +OUTPUT_FORMAT_NEEDS_SEPARATOR = [ + "dzen2", + "xmobar", + "lemonbar", + "tmux", + "term", + "none", +] + +DEFAULT_SEPARATOR = { + "dzen2": "^p(5;-2)^ro(2)^p()^p(5)", + # if it's not listed here, it defaults to " | " } MAX_NESTING_LEVELS = 4 diff --git a/py3status/core.py b/py3status/core.py index d8b9e95ecf..c49d7fd585 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -13,7 +13,7 @@ from traceback import extract_tb, format_tb, format_stack from py3status.command import CommandServer -from py3status.constants import DEFAULT_SEPARATORS +from py3status.constants import OUTPUT_FORMAT_NEEDS_SEPARATOR, DEFAULT_SEPARATOR from py3status.events import Events from py3status.formatter import expand_color from py3status.helpers import print_stderr @@ -726,17 +726,18 @@ def setup(self): # determine the target output format self.output_format = self.config["py3_config"]["general"]["output_format"] - # determine the separator between status entries, if needed - default_separator = DEFAULT_SEPARATORS.get(self.output_format, None) - self.output_format_separator = self.config["py3_config"]["py3status"].get( - "output_format_separator", default_separator - ) - - # inject the value of color_separator into the separator string - if self.output_format_separator is not None: - self.output_format_separator = self.output_format_separator.format( - color_separator=self.config["py3_config"]["general"]["color_separator"] + # determine the output separator, if needed + self.separator = None + if self.output_format in OUTPUT_FORMAT_NEEDS_SEPARATOR: + default_separator = DEFAULT_SEPARATOR.get(self.output_format, " | ") + self.separator = self.config["py3_config"]["general"].get( + "separator", default_separator ) + if self.config["py3_config"]["general"]["colors"]: + self.separator = self.format_separator( + self.separator, + self.config["py3_config"]["general"]["color_separator"], + ) def notify_user( self, @@ -1015,6 +1016,53 @@ def create_mappings(self, config): # Store mappings for later use. self.mappings_color = mappings + def format_color(self, output): + """ + Format the output of a module according to the value of output_format. + """ + full_text = output["full_text"] + if "color" in output: + if self.output_format == "dzen2": + full_text = f"^fg({output['color']})" + output["full_text"] + if self.output_format == "xmobar": + full_text = f"{output['full_text']}" + if self.output_format == "lemonbar": + full_text = f"%{{F{output['color']}}}" + output["full_text"] + if self.output_format == "tmux": + full_text = f"#[fg={output['color'].lower()}]" + output["full_text"] + if self.output_format == "term": + col = int(output["color"][1:], 16) + r = (col & (0xFF << 0)) // 0x80 + g = (col & (0xFF << 8)) // 0x8000 + b = (col & (0xFF << 16)) // 0x800000 + col = (r << 2) | (g << 1) | b + full_text = f"\033[3{col};1m" + output["full_text"] + if self.output_format == "none": + pass # colors are ignored + return full_text + + def format_separator(self, separator, color_separator): + """ + Format the output separator according to the value of output_format. + """ + if self.output_format == "dzen2": + return f"^fg({color_separator}){separator}^fg()" + if self.output_format == "xmobar": + return f"{separator}" + if self.output_format == "lemonbar": + return f"%{{F{color_separator}}}{separator}%{{F-}}" + if self.output_format == "tmux": + return f"#[fg={color_separator}]{separator}#[default]" + if self.output_format == "term": + col = int(color_separator[1:], 16) + r = (col & (0xFF << 0)) // 0x80 + g = (col & (0xFF << 8)) // 0x8000 + b = (col & (0xFF << 16)) // 0x800000 + col = (r << 2) | (g << 1) | b + return f"\033[3{col};1m{separator}\033[0m" + else: # output_format == "none" + return separator # color_separator is ignored + def process_module_output(self, module): """ Process the output for a module and return a json string representing it. @@ -1031,15 +1079,13 @@ def process_module_output(self, module): # Color: substitute the config defined color if "color" not in output: output["color"] = color - # Create the tmux string output. - if self.output_format == "tmux": - for output in outputs: - if "color" in output: - output[ - "full_text" - ] = f"#[fg={output['color'].lower()}]{output['full_text']}#[default]" - return "".join(x["full_text"] for x in outputs) - # Create the json string output. + # concatenate string output, if needed. + if self.output_format in OUTPUT_FORMAT_NEEDS_SEPARATOR: + # FIXME: `output_format = none` in config will default to i3bar. + # `output_format = "none"` is required instead. this is different + # in i3status, which behaves correctly for `output_status = none` + return "".join(self.format_color(x) for x in outputs) + # otherwise create the json string output. else: return ",".join(dumps(x) for x in outputs) @@ -1123,7 +1169,7 @@ def run(self): "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - if self.output_format != "tmux": + if self.output_format not in OUTPUT_FORMAT_NEEDS_SEPARATOR: write(dumps(header)) write("\n[[]\n") @@ -1153,9 +1199,8 @@ def run(self): output[index] = out # build output string and dump to stdout - out = "" - if self.output_format == "tmux": - out = self.output_format_separator.join(x for x in output if x) + if self.output_format in OUTPUT_FORMAT_NEEDS_SEPARATOR: + out = self.separator.join(x for x in output if x) write(f"{out}\n") else: out = ",".join(x for x in output if x) diff --git a/py3status/module.py b/py3status/module.py index 6f16863ddc..ad03cf68bf 100644 --- a/py3status/module.py +++ b/py3status/module.py @@ -338,11 +338,17 @@ def set_module_options(self, module): separator = fn(self.module_full_name, "separator") if not hasattr(separator, "none_setting"): - if not isinstance(separator, bool): - err = "Invalid `separator` attribute, should be a boolean. " - err += f"Got `{separator}`." - raise TypeError(err) - self.i3bar_module_options["separator"] = separator + # HACK: separator is a valid setting in the general section + # of the configuration. but it's a string, not a boolean. + # revisit how i3status and py3status differ in this regard. + # if not isinstance(separator, bool): + + # err = "Invalid `separator` attribute, should be a boolean. " + # err += f"Got `{separator}`." + # raise TypeError(err) + # self.i3bar_module_options["separator"] = separator + if isinstance(separator, bool): + self.i3bar_module_options["separator"] = separator separator_block_width = fn(self.module_full_name, "separator_block_width") if not hasattr(separator_block_width, "none_setting"): From e3fd0fae1867128fa6fcfbb0cf9bb9a43d453724 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Mon, 7 Mar 2022 20:57:50 +0100 Subject: [PATCH 14/15] object oriented approach for output_format handlers --- py3status/constants.py | 14 --- py3status/core.py | 98 +++------------ py3status/output.py | 269 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 97 deletions(-) create mode 100644 py3status/output.py diff --git a/py3status/constants.py b/py3status/constants.py index f2fcd5ff25..2868625e38 100644 --- a/py3status/constants.py +++ b/py3status/constants.py @@ -10,20 +10,6 @@ "output_format": "i3bar", } -OUTPUT_FORMAT_NEEDS_SEPARATOR = [ - "dzen2", - "xmobar", - "lemonbar", - "tmux", - "term", - "none", -] - -DEFAULT_SEPARATOR = { - "dzen2": "^p(5;-2)^ro(2)^p()^p(5)", - # if it's not listed here, it defaults to " | " -} - MAX_NESTING_LEVELS = 4 TIME_FORMAT = "%Y-%m-%d %H:%M:%S" diff --git a/py3status/core.py b/py3status/core.py index c49d7fd585..b59a3f181c 100644 --- a/py3status/core.py +++ b/py3status/core.py @@ -3,7 +3,6 @@ import time from collections import deque -from json import dumps from pathlib import Path from pprint import pformat from signal import signal, Signals, SIGTERM, SIGUSR1, SIGTSTP, SIGCONT @@ -13,13 +12,13 @@ from traceback import extract_tb, format_tb, format_stack from py3status.command import CommandServer -from py3status.constants import OUTPUT_FORMAT_NEEDS_SEPARATOR, DEFAULT_SEPARATOR from py3status.events import Events from py3status.formatter import expand_color from py3status.helpers import print_stderr from py3status.i3status import I3status from py3status.parse_config import process_config from py3status.module import Module +from py3status.output import OutputFormat from py3status.profiling import profile from py3status.udev_monitor import UdevMonitor @@ -724,20 +723,18 @@ def setup(self): self.load_modules(self.py3_modules, user_modules) # determine the target output format - self.output_format = self.config["py3_config"]["general"]["output_format"] + self.output_format = OutputFormat.instance_for( + self.config["py3_config"]["general"]["output_format"] + ) # determine the output separator, if needed - self.separator = None - if self.output_format in OUTPUT_FORMAT_NEEDS_SEPARATOR: - default_separator = DEFAULT_SEPARATOR.get(self.output_format, " | ") - self.separator = self.config["py3_config"]["general"].get( - "separator", default_separator - ) - if self.config["py3_config"]["general"]["colors"]: - self.separator = self.format_separator( - self.separator, - self.config["py3_config"]["general"]["color_separator"], - ) + color_separator = None + if self.config["py3_config"]["general"]["colors"]: + color_separator = self.config["py3_config"]["general"]["color_separator"] + self.output_format.format_separator( + self.config["py3_config"]["general"].get("separator", None), + color_separator, + ) def notify_user( self, @@ -1016,53 +1013,6 @@ def create_mappings(self, config): # Store mappings for later use. self.mappings_color = mappings - def format_color(self, output): - """ - Format the output of a module according to the value of output_format. - """ - full_text = output["full_text"] - if "color" in output: - if self.output_format == "dzen2": - full_text = f"^fg({output['color']})" + output["full_text"] - if self.output_format == "xmobar": - full_text = f"{output['full_text']}" - if self.output_format == "lemonbar": - full_text = f"%{{F{output['color']}}}" + output["full_text"] - if self.output_format == "tmux": - full_text = f"#[fg={output['color'].lower()}]" + output["full_text"] - if self.output_format == "term": - col = int(output["color"][1:], 16) - r = (col & (0xFF << 0)) // 0x80 - g = (col & (0xFF << 8)) // 0x8000 - b = (col & (0xFF << 16)) // 0x800000 - col = (r << 2) | (g << 1) | b - full_text = f"\033[3{col};1m" + output["full_text"] - if self.output_format == "none": - pass # colors are ignored - return full_text - - def format_separator(self, separator, color_separator): - """ - Format the output separator according to the value of output_format. - """ - if self.output_format == "dzen2": - return f"^fg({color_separator}){separator}^fg()" - if self.output_format == "xmobar": - return f"{separator}" - if self.output_format == "lemonbar": - return f"%{{F{color_separator}}}{separator}%{{F-}}" - if self.output_format == "tmux": - return f"#[fg={color_separator}]{separator}#[default]" - if self.output_format == "term": - col = int(color_separator[1:], 16) - r = (col & (0xFF << 0)) // 0x80 - g = (col & (0xFF << 8)) // 0x8000 - b = (col & (0xFF << 16)) // 0x800000 - col = (r << 2) | (g << 1) | b - return f"\033[3{col};1m{separator}\033[0m" - else: # output_format == "none" - return separator # color_separator is ignored - def process_module_output(self, module): """ Process the output for a module and return a json string representing it. @@ -1079,15 +1029,8 @@ def process_module_output(self, module): # Color: substitute the config defined color if "color" not in output: output["color"] = color - # concatenate string output, if needed. - if self.output_format in OUTPUT_FORMAT_NEEDS_SEPARATOR: - # FIXME: `output_format = none` in config will default to i3bar. - # `output_format = "none"` is required instead. this is different - # in i3status, which behaves correctly for `output_status = none` - return "".join(self.format_color(x) for x in outputs) - # otherwise create the json string output. - else: - return ",".join(dumps(x) for x in outputs) + # format output and return + return self.output_format.format(outputs) def i3bar_stop(self, signum, frame): if ( @@ -1160,18 +1103,13 @@ def run(self): # items in the bar output = [None] * len(py3_config["order"]) - write = sys.__stdout__.write - flush = sys.__stdout__.flush - # start our output header = { "version": 1, "click_events": self.config["click_events"], "stop_signal": self.stop_signal or 0, } - if self.output_format not in OUTPUT_FORMAT_NEEDS_SEPARATOR: - write(dumps(header)) - write("\n[[]\n") + self.output_format.write_header(header) update_due = None # main loop @@ -1199,10 +1137,4 @@ def run(self): output[index] = out # build output string and dump to stdout - if self.output_format in OUTPUT_FORMAT_NEEDS_SEPARATOR: - out = self.separator.join(x for x in output if x) - write(f"{out}\n") - else: - out = ",".join(x for x in output if x) - write(f",[{out}]\n") - flush() + self.output_format.write_line(output) diff --git a/py3status/output.py b/py3status/output.py new file mode 100644 index 0000000000..1ee0f66625 --- /dev/null +++ b/py3status/output.py @@ -0,0 +1,269 @@ +import sys +from json import dumps + + +class OutputFormat: + """ + A base class for formatting the output of py3status for various + different consumers + """ + + @classmethod + def instance_for(cls, output_format): + """ + A factory for OutputFormat objects + """ + supported_output_formats = { + "dzen2": Dzen2OutputFormat, + "i3bar": I3barOutputFormat, + "lemonbar": LemonbarOutputFormat, + "none": NoneOutputFormat, + "term": TermOutputFormat, + "tmux": TmuxOutputFormat, + "xmobar": XmobarOutputFormat, + } + + if output_format in supported_output_formats: + return supported_output_formats[output_format]() + raise ValueError( + f"Invalid `output_format` attribute, should be one of `{'`, `'.join(supported_output_formats.keys())}`. Got `{output_format}`." + ) + + def __init__(self): + """ + Constructor + """ + self.separator = None + + def format_separator(self, separator, color): + """ + Produce a formatted and colorized separator for the output format, + if the output_format requires it, and None otherwise. + """ + pass + + def format(self, outputs): + """ + Produce a line of output from a list of module output dictionaries + """ + raise NotImplementedError() + + def write_header(self, header): + """ + Write the header to output, if supported by the output_format + """ + raise NotImplementedError() + + def write_line(self, output): + """ + Write a line of py3status containing the given module output + """ + raise NotImplementedError() + + +class I3barOutputFormat(OutputFormat): + """ + Format the output for consumption by i3bar + """ + + def format(self, outputs): + """ + Produce a line of output from a list of module outputs for + consumption by i3bar. separator is ignored. + """ + return ",".join(dumps(x) for x in outputs) + + def write_header(self, header): + """ + Write the i3bar header to output + """ + write = sys.__stdout__.write + flush = sys.__stdout__.flush + + write(dumps(header)) + write("\n[[]\n") + flush() + + def write_line(self, output): + """ + Write a line of py3status output for consumption by i3bar + """ + write = sys.__stdout__.write + flush = sys.__stdout__.flush + + out = ",".join(x for x in output if x) + write(f",[{out}]\n") + flush() + + +class SeparatedOutputFormat(OutputFormat): + """ + Base class for formatting output as an enriched string containing + separators + """ + + def begin_color(self, color): + """ + Produce a format string for a colorized output for the output format + """ + raise NotImplementedError() + + def end_color(self): + """ + Produce a format string for ending a colorized output for the output format + """ + raise NotImplementedError() + + def end_color_quick(self): + """ + Produce a format string for ending a colorized output, but only + if it is syntactically required. (for example because a new color + declaration immediately follows) + """ + return self.end_color() + + def get_default_separator(self): + """ + Produce the default separator for the output format + """ + return " | " + + def format_separator(self, separator, color): + """ + Format the given separator with the given color + """ + if separator is None: + separator = self.get_default_separator() + if color is not None: + separator = self.begin_color(color) + separator + self.end_color() + self.separator = separator + + def format_color(self, block): + """ + Format the given block of module output + """ + full_text = block["full_text"] + if "color" in block: + full_text = ( + self.begin_color(block["color"]) + full_text + self.end_color_quick() + ) + return full_text + + def format(self, outputs): + """ + Produce a line of output from a list of module outputs by + concatenating individual blocks of formatted output + """ + return "".join(self.format_color(x) for x in outputs) + + def write_header(self, header): + """ + Not supported in separated output formats + """ + pass + + def write_line(self, output): + """ + Write a line of py3status output separated by the formatted separator + """ + write = sys.__stdout__.write + flush = sys.__stdout__.flush + + out = self.separator.join(x for x in output if x) + write(f"{out}\n") + flush() + + +class Dzen2OutputFormat(SeparatedOutputFormat): + """ + Format the output for consumption by dzen2 + """ + + def begin_color(self, color): + return f"^fg({color})" + + def end_color(self): + return "^fg()" + + def end_color_quick(self): + return "" + + def get_default_separator(self): + """ + Produce the default separator for the output format + """ + return "^p(5;-2)^ro(2)^p()^p(5)" + + +class XmobarOutputFormat(SeparatedOutputFormat): + """ + Format the output for consumption by xmobar + """ + + def begin_color(self, color): + return f"" + + def end_color(self): + return "" + + +class LemonbarOutputFormat(SeparatedOutputFormat): + """ + Format the output for consumption by lemonbar + """ + + def begin_color(self, color): + return f"%{{F{color}}}" + + def end_color(self): + return "%{F-}" + + def end_color_quick(self): + return "" + + +class TmuxOutputFormat(SeparatedOutputFormat): + """ + Format the output for consumption by tmux + """ + + def begin_color(self, color): + return f"#[fg={color.lower()}]" + + def end_color(self): + return "#[default]" + + def end_color_quick(self): + return "" + + +class TermOutputFormat(SeparatedOutputFormat): + """ + Format the output using terminal escapes + """ + + def begin_color(self, color): + col = int(color[1:], 16) + r = (col & (0xFF << 0)) // 0x80 + g = (col & (0xFF << 8)) // 0x8000 + b = (col & (0xFF << 16)) // 0x800000 + col = (r << 2) | (g << 1) | b + return f"\033[3{col};1m" + + def end_color(self): + return "\033[0m" + + def end_color_quick(self): + return "" + + +class NoneOutputFormat(SeparatedOutputFormat): + """ + Format the output without colors + """ + + def begin_color(self, color): + return "" + + def end_color(self): + return "" From 1da6c185d7783b0945f3e13feed7916412680992 Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Tue, 6 Jun 2023 09:52:51 +0200 Subject: [PATCH 15/15] making sure that the output_format setting is only overwritten for the general section, to avoid masking settings of the same name in modules --- py3status/i3status.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/py3status/i3status.py b/py3status/i3status.py index 8fc97e5500..dc9277b86f 100644 --- a/py3status/i3status.py +++ b/py3status/i3status.py @@ -330,10 +330,12 @@ def write_tmp_i3status_config(self, tmpfile): value = TZTIME_FORMAT if key == "format_time": continue - # Set output_format to i3bar for parsing regardless of what - # formatting we apply ourselves before printing - if key == "output_format": - value = "i3bar" + # Set output_format to i3bar in general section so that we + # receive predictable output from i3status, regardless of our + # own output_format configuration + if section_name == "general": + if key == "output_format": + value = "i3bar" if isinstance(value, bool): value = f"{value}".lower() self.write_in_tmpfile(f' {key} = "{value}"\n', tmpfile)