Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement proper handling of output_format in py3status for i3bar, dzen2, xmobar, lemonbar, tmux, term, none #2104

Merged
merged 15 commits into from
Jun 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions py3status/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +18,7 @@
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

Expand Down Expand Up @@ -722,6 +722,20 @@ def setup(self):
# load and spawn i3status.conf configured modules threads
self.load_modules(self.py3_modules, user_modules)

# determine the target output format
self.output_format = OutputFormat.instance_for(
self.config["py3_config"]["general"]["output_format"]
)

# determine the output separator, if needed
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,
)
oaken-source marked this conversation as resolved.
Show resolved Hide resolved

def notify_user(
self,
msg,
Expand Down Expand Up @@ -1015,8 +1029,8 @@ def process_module_output(self, module):
# Color: substitute the config defined color
if "color" not in output:
output["color"] = color
# Create the json string output.
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 (
Expand Down Expand Up @@ -1089,17 +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,
}
write(dumps(header))
write("\n[[]\n")
self.output_format.write_header(header)

update_due = None
# main loop
Expand All @@ -1126,8 +1136,5 @@ def run(self):
# store the output as json
output[index] = out

# build output string
out = ",".join(x for x in output if x)
# dump the line to stdout
write(f",[{out}]\n")
flush()
# build output string and dump to stdout
self.output_format.write_line(output)
6 changes: 6 additions & 0 deletions py3status/i3status.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ def write_tmp_i3status_config(self, tmpfile):
value = TZTIME_FORMAT
if key == "format_time":
continue
# 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)
Expand Down
16 changes: 11 additions & 5 deletions py3status/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting, I did not recall that at all...

Copy link
Contributor

@lasers lasers Mar 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick glance.. Maybe things are a bit hazy, but it looks like there could be two separator configs.

... both may gotten mixed... Putting separator = False in py3status config section should turn them all off.

Maybe we need two different separator coding.... One for general (string) and one for py3status (boolean).

EDIT: Or the separator = value was in wrong config section.... Need more inspecting.

# 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"):
Expand Down
269 changes: 269 additions & 0 deletions py3status/output.py
Original file line number Diff line number Diff line change
@@ -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
"""
oaken-source marked this conversation as resolved.
Show resolved Hide resolved

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"<fc={color}>"

def end_color(self):
return "</fc>"


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 ""