From a3f619a1a161eb3ab8f6c2d64f48f243e98b4e6c Mon Sep 17 00:00:00 2001 From: Andreas Grapentin Date: Mon, 7 Mar 2022 20:57:50 +0100 Subject: [PATCH] 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 c7791a9def..a1518612e2 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 @@ -697,20 +696,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, @@ -989,53 +986,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. @@ -1052,15 +1002,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 ( @@ -1128,18 +1071,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 @@ -1169,10 +1107,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 ""