Sanjay Prajapat
Sanjay Prajapat

Reputation: 305

How to update multiple line in console with Golang

I have written a go program which is doing many tasks concurrently (in goroutines), and showing the status of these task on the console like this -

[Ongoing] Task1
[Ongoing] Task2
[Ongoing] Task3

Now Any of these tasks can finish first or last. I want to update the status of these tasks at the same place. If Task2 finishes first then it should show something like this-

[Ongoing] Task1
[Done] Task2
[Ongoing] Task3

I tried uilive library, but It's always updating the last line like this. (I think it's not for updating multiple lines)

[Ongoing] Task1
[Ongoing] Task2
[Done] Task2

How do I achieve this?

Upvotes: 6

Views: 6116

Answers (2)

Bart van Oort
Bart van Oort

Reputation: 350

I was looking to implement a similar functionality and came across this question. Not sure if you're still looking for a solution, but I found one :)

It turns out uilive can very well be used to update multiple lines, by making use of the Newline() function on a *uilive.Writer. Editing the example on their repository to write the download progress on multiple lines, we get:

writer := uilive.New()       // writer for the first line
writer2 := writer.Newline()  // writer for the second line
// start listening for updates and render
writer.Start()

for i := 0; i <= 100; i++ {
    fmt.Fprintf(writer, "Downloading File 1.. %d %%\n", i)
    fmt.Fprintf(writer2, "Downloading File 2.. %d %%\n", i)
    time.Sleep(time.Millisecond * 5)
}

fmt.Fprintln(writer, "Finished downloading both files :)")
writer.Stop() // flush and stop rendering

For implementing something to track the progress of multiple concurrently running tasks, as @blami's answer also states, a good approach is to have a separate goroutine that handles all terminal output.

If you're interested, I've implemented this exact functionality in mllint, my tool for linting machine learning projects. It runs all its underlying linters (i.e. tasks) in parallel and prints their progress (running / done) in the manner that you describe in your question. See this file for the implementation of that. Specifically, the printTasks function reads from the list of tasks in the system and prints each task's display name and status on a new line.

Note that especially with multi-line printing, it becomes important to have control over flushing the print buffer, as you do not want your writer to automatically flush halfway through writing an update to the buffer. Therefore, I set the writer's RefreshDuration to something large (e.g. time.Hour) and call Flush() on the writer after printing all lines, i.e. at the end of printTasks.

Upvotes: 9

blami
blami

Reputation: 7411

Stdout is a stream so you can't address already printed line or character and change it later. Such updates in CLI libraries are made by moving cursor back (by printing escape sequences - non printable character sequences that affect users terminal) and overwriting the text. For that a reference point is needed so the library (or you) know where cursor is and how many lines were printed, etc.

One of possible approaches can be creating a separate goroutine that handles all terminal output printing and have other goroutines that do the actual work only communicate updates (e.g. over channels) to it. Centralizing "state" of terminal in such routine should make it easier to update using technique describe above.

While not a drop-in solution for your situation I recommend to look at mpb - a library that allows to render multiple asynchronously updating progress bars. Maybe you can design your solution in similar way or use it as base as it already handles differences between OS'es, etc.

Upvotes: 3

Related Questions