Issue with ANSI cursor movement in goroutine

Issue

Background

I’m trying to write a Go library for creating terminal task-lists, inspired by the Node library listr.

My library, golist, prints the task list out in a background goroutine and updates the text and status characters using ANSI escape sequences.

The Problem

There’s an issue where the final print of the list will occasionally have extra spaces included, leading to some spaces or repeated lines. Here are two examples – one correct, one not – both from runs of the same exact code (here’s a link to the code).

Example

Here’s an example of what it should look like:

golsit correct example

(Here’s a gist of the raw text output for the correct output)

And here’s an example of what it sometimes looks like:

golsit incorrect example

(Here’s a gist of the raw text output for the incorrect output)

If you look at lines 184 and 185 in the gist of the incorrect version, there are two blank lines that aren’t in the correct version.

Why is this happening and why is it only happening sometimes?

Code

I’m printing the list to the terminal in the following loop:

go func() {
    defer donePrinting() // Tell the Stop function that we're done printing
    ts := l.getTaskStates()
    l.print(ts)
    for {
        select {
        case <-ctx.Done(): // Check if the print loop should stop

            // Perform a final clear and an optional print depending on `ClearOnComplete`
            ts := l.getTaskStates()
            if l.ClearOnComplete {
                l.clear(ts)
                return
            }

            l.clearThenPrint(ts)
            return

        case s := <-l.printQ: // Check if there's a message to print
            fmt.Fprintln(l.Writer, s)

        default: // Otherwise, print the list
            ts := l.getTaskStates()
            l.clearThenPrint(ts)
            l.StatusIndicator.Next()
            time.Sleep(l.Delay)
        }
    }
}()

The list is formatted as a string and then printed. The following function formats the string:

// fmtPrint returns the formatted list of messages
// and statuses, using the supplied TaskStates
func (l *List) fmtPrint(ts []*TaskState) string {
    s := make([]string, 0)
    for _, t := range ts {
        s = append(s, l.formatMessage(t))
    }
    return strings.Join(s, "\n")
}

and the following function builds the ANSI escape string to clear the lines:

// fmtClear returns a string of ANSI escape characters
// to clear the `n` lines previously printed.
func (l *List) fmtClear(n int) string {
    s := "\033[1A" // Move up a line
    s += "\033[K"  // Clear the line
    s += "\r"      // Move back to the beginning of the line
    return strings.Repeat(s, n)
}

I’m using this site as a reference for the ANSI codes.


Thanks in advance for any suggestions you might have about why this is happening!

Let me know if there’s any other information I can add that can help.

Solution

I think the ANSI codes are just a red herring. I pulled down the library and tried running it locally, and found that the following section is what is creating this issue:

 case s := <-l.printQ: // Check if there's a message to print
     fmt.Fprintln(l.Writer, s)

When the printQ channel is getting closed, this case is sometimes running, which seems to be moving the cursor down even though nothing is getting printed. This behaviour went away when I moved the call to close the channel after l.printDone is called.

...
// Wait for the print loop to finish                       
<-l.printDone                      
                                                                   
if l.printQ != nil {                     
    close(l.printQ)                                    
} 
...

This ensures that the loop is no longer running when the channel is closed, and thus the s := <-l.printQ case cannot run.

Answered By – msmith

Answer Checked By – Terry (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.