diff --git a/doc/man1/flux-jobs.rst b/doc/man1/flux-jobs.rst index 41244a7e4d41..29bf13d70fc3 100644 --- a/doc/man1/flux-jobs.rst +++ b/doc/man1/flux-jobs.rst @@ -275,6 +275,11 @@ string, "0s", "0.0", "0:00:00", or epoch time to a hyphen. For example, normally "{nodelist}" would output an empty string if the job has not yet run. By specifying, "{nodelist:h}", a hyphen would be presented instead. +The special presentation type *W* can be used to adjust output alignment +for wide characters. It only works with left/right alignment formatting +of the form "(<|>)N", for example ``{id.emoji:>12W}``. It is used almost +exclusively for emoji based outputs and typically used alongside ``+:``. + The special suffix *+* can be used to indicate if a string was truncated by including a ``+`` character when truncation occurs. If both *h* and *+* are being used, then the *+* must appear after the *h*. diff --git a/src/bindings/python/flux/util.py b/src/bindings/python/flux/util.py index b489d74df885..514771d2264b 100644 --- a/src/bindings/python/flux/util.py +++ b/src/bindings/python/flux/util.py @@ -32,6 +32,7 @@ from typing import Mapping import yaml +from wcwidth import wcswidth # tomllib added to standard library in Python 3.11 # flux-core minimum is Python 3.6. @@ -578,6 +579,26 @@ def format_field(self, value, spec): basecases = empty_outputs() value = "-" if str(value) in basecases else str(value) spec = spec[:-1] + "s" + + # Normal python output will not consider emoji width with + # width formatting. So we will adjust the output width of + # field accordingly to account for it if the "W" modifier + # is specified. + if spec.endswith("W") and isinstance(value, str): + match = re.search(r"^([<>])(\d+)W", spec) + if match: + align = match[1] + width = int(match[2]) + display_width = wcswidth(value) + normal_width = len(value) + width_diff = display_width - normal_width + if width_diff > 0 and width > width_diff: + width -= width_diff + spec = f"{align}{width}s" + # if spec was not modified above, need to convert "W" to "s" + if spec.endswith("W"): + spec = spec[:-1] + "s" + retval = super().format_field(value, spec) if denote_truncation and len(retval) < len(str(value)): @@ -734,6 +755,7 @@ class FormatSpec: "precision_str", "type", "hyphen", + "wide", "truncate", ) @@ -758,6 +780,7 @@ def __init__(self, spec): r"(?:(?P\.)(?P\d+))?" r"(?P[bcdeEfFgGnosxX%])?" r"(?Ph)?" + r"(?PW)?" r"(?P\+)?" ) try: @@ -1037,7 +1060,7 @@ def sentinel_keys(): for item in items: for entry in lst: result = formatter.format(entry["fmt"], item) - width = 0 if result in empty else len(result) + width = 0 if result in empty else wcswidth(result) if width > entry["maxwidth"]: entry["maxwidth"] = width if width > entry["width"]: diff --git a/src/cmd/flux-jobs.py b/src/cmd/flux-jobs.py index e66ca0e14f7e..0896c9dc1a14 100755 --- a/src/cmd/flux-jobs.py +++ b/src/cmd/flux-jobs.py @@ -48,8 +48,8 @@ class FluxJobsConfig(UtilConfig): "cute": { "description": "Cute flux-jobs format string (default with emojis)", "format": ( - "{id.f58:>12} ?:{queue:<8.8} +:{username:<8} {name:<10.10+} " - "{status_emoji:>5.5} {ntasks:>6} {nnodes:>6h} " + "+:{id.emoji:>12W} ?:{queue:<8.8} +:{username:<8} {name:<10.10+} " + "+:{status_emoji:>5W} {ntasks:>6} +:{nnodes:>6h} " "{contextual_time!F:>8h} {contextual_info}" ), }, diff --git a/t/t2800-jobs-cmd.t b/t/t2800-jobs-cmd.t index 28a3f9b9df42..78574f1a4498 100755 --- a/t/t2800-jobs-cmd.t +++ b/t/t2800-jobs-cmd.t @@ -554,6 +554,17 @@ test_expect_success 'flux-jobs --format={id.f58},{id.f58plain},{id.hex},{id.doth test_cmp ids.XX.expected ids.XX.out ' +# N.B. don't use test_cmp for match, there can be minor differences in spacing depending +# on terminal and font. Just do a grep. +test_expect_success 'flux-jobs --format={id.emoji},{id.emoji:>12W},{id.emoji:<12W} works' ' + flux jobs -ano "{id.emoji},{id.emoji:>12W},{id.emoji:<12W}" \ + > ids.emoji.out && + for id in $(cat all.ids); do + idemoji=$(flux job id --to=emoji $id) && + grep ${idemoji} ids.emoji.out + done +' + test_expect_success 'flux-jobs --format={userid},{username} works' ' flux jobs --no-header -a --format="{userid},{username}" > user.out && id=`id -u` &&