Skip to content

Commit

Permalink
implement handling of output_format in py3status for i3bar, dzen2, xm…
Browse files Browse the repository at this point in the history
…obar, lemonbar, tmux, term, none (#2104)
  • Loading branch information
oaken-source authored Jun 18, 2023
1 parent 3bb3306 commit dc3c2c4
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 18 deletions.
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,
)

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

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

0 comments on commit dc3c2c4

Please sign in to comment.