Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,36 @@ class AnsiLogObserver implements TraceObserverV2 {
protected int printAndCountLines(String str) {
if( str ) {
printAnsiLines(str)
return str.count(NEWLINE)
return countVisualLines(str)
}
else
return 0
}

/**
* Count the number of visual lines the string occupies on the terminal,
* accounting for lines that wrap when they exceed the terminal width.
*/
protected int countVisualLines(String str) {
final lines = str.split(NEWLINE, -1)
int count = 0
// the last element after split is always empty (trailing newline), skip it
for( int i=0; i<lines.length-1; i++ ) {
final visualLen = stripAnsi(lines[i]).length()
// each line takes at least 1 visual line, plus extra lines for wrapping
count += visualLen > 0 && cols > 0 ? Math.ceil((double)visualLen / cols).intValue() : 1
}
return count
}

/**
* Strip ANSI escape codes and OSC hyperlinks from a string
* to determine its visual display width.
*/
protected static String stripAnsi(String str) {
return ANSI_ESCAPE.matcher(str).replaceAll('')
}

protected void renderSummary(WorkflowStats stats) {
final delta = endTimestamp-startTimestamp
if( enableSummary == false )
Expand Down Expand Up @@ -415,6 +439,7 @@ class AnsiLogObserver implements TraceObserverV2 {

private final static Pattern TAG_REGEX = ~/ \((.+)\)( *)$/
private final static Pattern LBL_REPLACE = ~/ \(.+\) *$/
private final static Pattern ANSI_ESCAPE = ~/\033\[[0-9;]*[a-zA-Z]|\033][^\007]*\007/

// OSC 8 hyperlink escape sequences (using BEL as String Terminator)
private final static String HYPERLINK_START = '\033]8;;'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,36 @@ class AnsiLogObserverTest extends Specification {
's3://bucket/work/4e/486876abc' | 's3://bucket/work/4e/486876abc'
}

def 'should strip ansi escape codes' () {
expect:
AnsiLogObserver.stripAnsi('hello') == 'hello'
AnsiLogObserver.stripAnsi('\033[32mgreen\033[0m') == 'green'
AnsiLogObserver.stripAnsi('\033[1;31mbold red\033[0m') == 'bold red'
AnsiLogObserver.stripAnsi('\033]8;;http://example.com\007link\033]8;;\007') == 'link'
AnsiLogObserver.stripAnsi('\033[2m[\033[0m\033[34mab/123456\033[0m\033[2m] \033[0mfoo') == '[ab/123456] foo'
}

@Unroll
def 'should count visual lines with wrapping' () {
given:
def observer = new AnsiLogObserver()
observer.@cols = COLS

expect:
observer.countVisualLines(INPUT) == EXPECTED

where:
COLS | INPUT | EXPECTED
80 | 'short line\n' | 1
80 | 'line1\nline2\n' | 2
80 | 'a' * 80 + '\n' | 1 // exactly fits, no wrap
80 | 'a' * 81 + '\n' | 2 // wraps to 2 lines
80 | 'a' * 160 + '\n' | 2 // exactly 2 lines
80 | 'a' * 161 + '\n' | 3 // wraps to 3 lines
40 | 'a' * 100 + '\n' | 3 // 100 chars in 40-col terminal
80 | 'short\n' + 'a' * 200 + '\n'| 4 // 1 + 3 lines
}

def 'should not render hyperlink when cleanup is enabled' () {
given:
def session = Mock(Session) { getConfig() >> [cleanup: true] }
Expand Down
Loading