16
16
17
17
package nextflow.trace
18
18
19
+ import java.util.regex.Pattern
20
+
19
21
import groovy.transform.CompileStatic
20
22
import jline.TerminalFactory
21
23
import nextflow.Session
@@ -85,6 +87,8 @@ class AnsiLogObserver implements TraceObserver {
85
87
86
88
private volatile int cols = 80
87
89
90
+ private volatile int rows = 24
91
+
88
92
private long startTimestamp
89
93
90
94
private long endTimestamp
@@ -181,7 +185,7 @@ class AnsiLogObserver implements TraceObserver {
181
185
wait(200 )
182
186
}
183
187
}
184
- //
188
+ //
185
189
final stats = statsObserver. getStats()
186
190
renderProgress(stats)
187
191
renderSummary(stats)
@@ -227,7 +231,7 @@ class AnsiLogObserver implements TraceObserver {
227
231
protected String getExecutorName (String key ) {
228
232
session. getExecutorFactory(). getDisplayName(key)
229
233
}
230
-
234
+
231
235
protected void renderExecutors (Ansi term ) {
232
236
int count= 0
233
237
def line = ' '
@@ -237,7 +241,7 @@ class AnsiLogObserver implements TraceObserver {
237
241
}
238
242
239
243
if ( count ) {
240
- term. a(" executor > " + line)
244
+ term. a(Attribute . INTENSITY_FAINT ) . a( " executor > " + line) . reset( )
241
245
term. newline()
242
246
}
243
247
}
@@ -251,6 +255,7 @@ class AnsiLogObserver implements TraceObserver {
251
255
}
252
256
253
257
cols = TerminalFactory . get(). getWidth()
258
+ rows = TerminalFactory . get(). getHeight()
254
259
255
260
// calc max width
256
261
final now = System . currentTimeMillis()
@@ -265,9 +270,25 @@ class AnsiLogObserver implements TraceObserver {
265
270
lastWidthReset = now
266
271
267
272
// render line
273
+ def renderedLines = 0
274
+ def skippedLines = 0
268
275
for ( ProgressRecord entry : processes ) {
269
- term. a(line(entry))
270
- term. newline()
276
+ // Only show line if we have space in the visible terminal area
277
+ // or if the process has some submitted tasks
278
+ if ( renderedLines <= rows - 5 || entry. getTotalCount() > 0 ) {
279
+ term. a(line(entry))
280
+ term. newline()
281
+ renderedLines + = 1
282
+ }
283
+ // Process with no active tasks and we're out of screen space, skip
284
+ else {
285
+ skippedLines + = 1
286
+ }
287
+ }
288
+ // Tell the user how many processes without active tasks were hidden
289
+ if ( skippedLines > 0 ){
290
+ term. a(Attribute . ITALIC ). a(Attribute . INTENSITY_FAINT ). a(" Plus " ). bold(). a(skippedLines). reset()
291
+ term. a(Attribute . ITALIC ). a(Attribute . INTENSITY_FAINT ). a(" more processes waiting for tasks…" ). reset(). newline()
271
292
}
272
293
rendered = true
273
294
}
@@ -322,7 +343,7 @@ class AnsiLogObserver implements TraceObserver {
322
343
return
323
344
if ( enableSummary == null && delta <= 60 * 1_000 )
324
345
return
325
-
346
+
326
347
if ( session. isSuccess() && stats. progressLength> 0 ) {
327
348
def report = " "
328
349
report + = " Completed at: ${ new Date(endTimestamp).format('dd-MMM-yyyy HH:mm:ss')} \n "
@@ -350,13 +371,13 @@ class AnsiLogObserver implements TraceObserver {
350
371
if ( color ) fmt = fmt. fg(Color . DEFAULT )
351
372
AnsiConsole . out. println (fmt. eraseLine())
352
373
}
353
-
374
+
354
375
protected void printAnsiLines (String lines ) {
355
376
final text = lines
356
377
.replace(' \r ' ,' ' )
357
378
.replace(NEWLINE , ansi(). eraseLine(). toString() + NEWLINE )
358
379
AnsiConsole . out. print (text)
359
- }
380
+ }
360
381
361
382
protected String fmtWidth (String name , int width , int cols ) {
362
383
assert name. size() <= width
@@ -369,36 +390,103 @@ class AnsiLogObserver implements TraceObserver {
369
390
}
370
391
371
392
protected String fmtChop (String str , int cols ) {
393
+ // Truncate the process name to fit the terminal width
372
394
if ( str. size() <= cols )
373
395
return str
374
- return cols> 3 ? str[0 .. (cols-3-1 )] + ' ...' : str[0 .. cols-1 ]
396
+ // Take the first 3 characters and the final chunk of text
397
+ // eg. for: NFCORE_RNASEQ:RNASEQ:FASTQ_SUBSAMPLE_FQ_SALMON:FQ_SUBSAMPLE
398
+ // truncate to: NFC…_SALMON:FQ_SUBSAMPLE
399
+ return cols> 5 ? str. take(3 ) + ' …' + str. takeRight(cols-1-3 ) : str[0 .. cols-1 ]
375
400
}
376
401
377
- protected String line (ProgressRecord stats ) {
402
+ private final static Pattern TAG_REGEX = ~/ \( (.+)\) ( *)$/
403
+ private final static Pattern LBL_REPLACE = ~/ \( .+\) *$/
404
+
405
+ protected Ansi line (ProgressRecord stats ) {
406
+ final term = ansi()
378
407
final float tot = stats. getTotalCount()
379
408
final float com = stats. getCompletedCount()
409
+ // Truncate or pad the label to the correct width
380
410
final label = fmtWidth(stats. taskName, labelWidth, Math . max(cols-50 , 5 ))
411
+ // Break up the process label into components for styling. eg:
412
+ // NFCORE_RNASEQ:RNASEQ:PREPARE_GENOME:GUNZIP_GTF (genes.gtf.gz)
413
+ // labelTag = genes.gtf.gz
414
+ // labelSpaces = whitespace padding after process name
415
+ // labelFinalProcess = GUNZIP_GTF
416
+ // labelNoFinalProcess = NFCORE_RNASEQ:RNASEQ:PREPARE_GENOME:
417
+ final tagMatch = TAG_REGEX . matcher(label)
418
+ final labelTag = tagMatch ? tagMatch. group(1 ) : ' '
419
+ final labelSpaces = tagMatch ? tagMatch. group(2 ) : ' '
420
+ final labelNoTag = LBL_REPLACE . matcher(label). replaceFirst(" " )
421
+ final labelFinalProcess = labelNoTag. tokenize(' :' )[-1 ]
422
+ final labelNoFinalProcess = labelFinalProcess. length() > 0 ? labelNoTag - labelFinalProcess : labelNoTag
381
423
final hh = (stats. hash && tot> 0 ? stats. hash : ' -' ). padRight(9 )
382
424
383
- if ( tot == 0 )
384
- return " [$hh ] process > $label -"
385
-
386
425
final x = tot ? Math . floor(com / tot * 100f ). toInteger() : 0
387
- final pct = " [${ String.valueOf(x).padLeft(3)} %]" . toString()
426
+ // eg. 100% (whitespace padded for alignment)
427
+ final pct = " ${ String.valueOf(x).padLeft(3)} %" . toString()
428
+ // eg. 1 of 1
429
+ final numbs = " ${ (int)com} of ${ (int)tot} " . toString()
430
+
431
+ // Task hash, eg: [fa/71091a]
432
+ term. a(Attribute . INTENSITY_FAINT ). a(' [' ). reset()
433
+ term. fg(Color . BLUE ). a(hh). reset()
434
+ term. a(Attribute . INTENSITY_FAINT ). a(' ] ' ). reset()
435
+
436
+ // Only show 'process > ' if the terminal has lots of width
437
+ if ( cols > 180 )
438
+ term. a(Attribute . INTENSITY_FAINT ). a(' process > ' ). reset()
439
+ // Stem of process name, dim text
440
+ term. a(Attribute . INTENSITY_FAINT ). a(labelNoFinalProcess). reset()
441
+ // Final process name, regular text
442
+ term. a(labelFinalProcess)
443
+ // Active process with a tag, eg: (genes.gtf.gz)
444
+ if ( labelTag ){
445
+ // Tag in yellow, () dim but tag text regular
446
+ term. fg(Color . YELLOW ). a(Attribute . INTENSITY_FAINT ). a(' (' ). reset()
447
+ term. fg(Color . YELLOW ). a(labelTag)
448
+ term. a(Attribute . INTENSITY_FAINT ). a(' )' ). reset(). a(labelSpaces)
449
+ }
450
+
451
+ // No tasks
452
+ if ( tot == 0 ) {
453
+ term. a(' -' )
454
+ return term
455
+ }
388
456
389
- final numbs = " ${ (int)com} of ${ (int)tot} " . toString()
390
- def result = " [${ hh} ] process > $label $pct $numbs "
457
+ // Progress percentage, eg: [ 80%]
458
+ if ( cols > 120 ) {
459
+ // Only show the percentage if we have lots of width
460
+ // Percentage text in green if 100%, otherwise blue
461
+ term. a(Attribute . INTENSITY_FAINT ). a(' [' ). reset()
462
+ .fg(pct == ' 100%' ? Color . GREEN : Color . BLUE ). a(pct). reset()
463
+ .a(Attribute . INTENSITY_FAINT ). a(' ]' ). reset()
464
+ }
465
+ else {
466
+ // If narrow terminal, show single pipe char instead of percentage to save space
467
+ term. a(Attribute . INTENSITY_FAINT ). a(' |' ). reset()
468
+ }
469
+ // Progress active task count, eg: 8 of 10
470
+ term. a(numbs)
471
+
472
+ // Completed task counts and status
473
+ // Dim text for cached, otherwise regular
391
474
if ( stats. cached )
392
- result + = " , cached: $stats . cached "
475
+ term . a( Attribute . INTENSITY_FAINT ) . a( " , cached: $stats . cached " ) . reset()
393
476
if ( stats. stored )
394
- result + = " , stored: $stats . stored "
477
+ term . a( " , stored: $stats . stored " )
395
478
if ( stats. failed )
396
- result + = " , failed: $stats . failed "
479
+ term . a( " , failed: $stats . failed " )
397
480
if ( stats. retries )
398
- result + = " , retries: $stats . retries "
399
- if ( stats. terminated && tot )
400
- result + = stats. errored ? ' \u 2718' : ' \u 2714'
401
- return fmtChop(result, cols)
481
+ term. a(" , retries: $stats . retries " )
482
+ // Show red cross ('✘') or green tick ('✔') according to status
483
+ if ( stats. terminated && tot ) {
484
+ if ( stats. errored )
485
+ term. fg(Color . RED ). a(' \u 2718' ). reset()
486
+ else
487
+ term. fg(Color . GREEN ). a(' \u 2714' ). reset()
488
+ }
489
+ return term
402
490
}
403
491
404
492
@Override
0 commit comments