freva / ascii-table

ASCII Tables for Java

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using colour output leads to table miss alignment

davsclaus opened this issue · comments

I am experimenting with adding colour output via
https://github.com/fusesource/jansi

This is what for example Apache Maven, Spring Boot, etc uses to log in colour.

I wanted to mark Camel components that are in preview mode with a yellow colour, that are in a middle column. When doing this then the column width gets larger ... I just wanted to let you know and we can maybe work on having better color support over time.

See the screenshot below

Ah, I was expecting this... Handling ANSI escape codes shouldn't be too hard, but this problem will come back again whenever someone decides to put some other funny unicode characters (e.g. emojis) in the future.

I'll try to fix soon (next few days).

After having thought about this some more and then trying to implement this, I've found at least 3 problems, in increasingly order of difficulty:

  1. When calculating column width, all ANSI escape codes should be ignored. This needs to be done at least 2 places:
    • When sizing up all input data to decide final column widths (LineUtils::maxLineLength). Initially I thought this was the only issue, so I fixed that in f810a2d
    • When justifying a cell (AsciiTable::writeJustified).
  2. LineUtils::splitTextIntoLinesOfMaxLength needs to preserve ANSI escape codes, but ignore them when calculating string length/deciding where to break line.
  3. How should ANSI escape codes be handled if the cell is overflowing? Let's say the cell value is <red>Hello world<reset>, max column width is 5, and overflow behavior is:
    • NEWLINE: The naive implementation is to return [<red>Hello, world<reset>], but that makes the red bleed over to the neighboring column(s), potentially until the next row where it'll finally be reset, whereas the user would probably expect something like [<red>Hello<reset>, <red>world<reset>] instead.
    • CLIP/ELLIPSIS_RIGHT: The naive implementation would return [<red>Hello], never resetting the styles.
    • CLIP/ELLIPSIS_LEFT: The naive implementation would return [world<reset>], i.e. all styles would be lost.

1 & 2 is the bare minimum, just to support cells that never overflow. Supporting 3 requires full support for parsing ANSI escape codes to rewrite the input in a way that makes sense, which is a lot of work.

Another possible direction to go is to allow users to set column styles in column definition and let this library generate the ANSI escape codes, but even that is a lot of work and would be limited to setting styles for the entire cell (i.e. not possible to style only a portion of the cell).

I think this is a good issue so I'll leave it open, and I'm open to contributions, but I don't think I'll be able to fix this anytime soon.

Thanks @freva for the detailed explanation.

I can see how overflow is a problem. At first although then Apache Camel would need colours in columns that does not overflow (eg we use overflow for long human descriptions).

Maybe that could be a candidate for a release with partially color support.

For 3 then I wonder if some kind of callback api, that end users at first is need to use (beforeCell | afterCell) - where the end user can store the state of the color for the cell, and restore it.
then I can use the fusesource jansi library in those callbacks.

column A |  column B | column C
hello | <red>world | I am normal text ...
 nothing | I am still red</red> | and I am still normal

Then there are callbacks ala

onCellStart(A)
onCellEnd(A)
onCellStart(B)
onCellEndB)
onCellStart(C)
onCellEnd(C)
onCellStart(A)
onCellEnd(A)
onCellStart(B)
onCellEndB)
onCellStart(C)
onCellEnd(C)

The API may have finer grained for start ... pause ... resume .. end (find better words for pause/resume, eg when the cell is suspended due to rendering another cell)

I don't think adding support for ANSI escape codes into the library is the way to go because ultimately it nearly impossible to support all cases, but maybe it's possible to get something good enough with callback system similar to what you describe.

For example, maybe the user can pass an implementation of an interface like

private interface Transformer {

    /**
     * Transform the cell value, e.g. by adding ANSI escape codes.
     * The give data is potentially split into multiple lines and is justified.
     * The return value must NOT change the data with any visible characters.
     * 
     * @param column Column of the cell value
     * @param row row number of the cell value (0-indexed)
     * @param col column number of the cell value (0-indexed)
     * @param data List of strings (lines) in this cell. Guaranteed to be at least 1 element. Unless the original
     *             cell data was longer than the max column width AND text overflow behavior is NEWLINE, this will
     *             contain exactly 1 element.
     * @return List of strings (lines) in this cell.
     */
    List<String> transform(Column column, int row, int col, List<String> data);

    List<String> transformHeader(Column column, int col, List<String> data);

    List<String> transformFooter(Column column, int col, List<String> data);
}

Then in your case the implementation would be something like

@Override
public List<String> transform(Column column, int row, int col, List<String> data) {
    if (!column.getHeader().equals("LEVEL")) return data;
    return data.stream()
            .map(line -> Ansi.ansi().fg(Ansi.Color.RED).a(line).reset().toString())
            .collect(Collectors.toList());
}

yes this looks really good, thanks

I renamed the interface to Styler and renamed the methods, but otherwise implemented as described above.

Released in 1.8.0.

Thanks you very much