在goroutine中使用ANSI光标移动存在问题。

huangapple go评论83阅读模式
英文:

Issue with ANSI cursor movement in goroutine

问题

背景

我正在尝试编写一个Go库,用于创建终端任务列表,灵感来自于Node库listr

我的库golist在后台的goroutine中打印任务列表,并使用ANSI转义序列更新文本和状态字符。

问题

存在一个问题,即列表的最终打印会偶尔包含额外的空格,导致一些空格或重复的行。以下是两个示例 - 一个正确的示例和一个不正确的示例 - 都是使用__完全相同的代码__运行的(这里是代码的链接)。

示例

这是一个应该是正确的示例:

在goroutine中使用ANSI光标移动存在问题。

这里是正确输出的原始文本输出的gist

这是一个有时候会出现的示例:

在goroutine中使用ANSI光标移动存在问题。

这里是不正确输出的原始文本输出的gist

如果你查看不正确版本的gist中的第184行和185行,会发现两个空行不在正确版本中。

为什么会发生这种情况,而且为什么只会偶尔发生?

代码

我在以下循环中将列表打印到终端:

go func() {
	defer donePrinting() // 告诉Stop函数我们已经打印完毕
	ts := l.getTaskStates()
	l.print(ts)
	for {
		select {
		case <-ctx.Done(): // 检查打印循环是否应该停止

			// 执行最后的清除和根据`ClearOnComplete`进行可选的打印
			ts := l.getTaskStates()
			if l.ClearOnComplete {
				l.clear(ts)
				return
			}

			l.clearThenPrint(ts)
			return

		case s := <-l.printQ: // 检查是否有要打印的消息
			fmt.Fprintln(l.Writer, s)

		default: // 否则,打印列表
			ts := l.getTaskStates()
			l.clearThenPrint(ts)
			l.StatusIndicator.Next()
			time.Sleep(l.Delay)
		}
	}
}()

列表被格式化为字符串,然后打印。以下函数格式化字符串:

// fmtPrint返回格式化的消息和状态列表,使用提供的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")
}

以下函数构建用于清除行的ANSI转义字符串:

// fmtClear返回一个包含ANSI转义字符的字符串,用于清除先前打印的`n`行。
func (l *List) fmtClear(n int) string {
	s := "3[1A" // 上移一行
	s += "3[K"  // 清除行
	s += "\r"      // 移动到行的开头
	return strings.Repeat(s, n)
}

我在这个网站上参考ANSI代码。


提前感谢您对此问题可能发生原因的任何建议!

如果有任何其他信息可以帮助,请告诉我。

英文:

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:

在goroutine中使用ANSI光标移动存在问题。

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

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

在goroutine中使用ANSI光标移动存在问题。

(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&#39;re done printing
	ts := l.getTaskStates()
	l.print(ts)
	for {
		select {
		case &lt;-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 := &lt;-l.printQ: // Check if there&#39;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, &quot;\n&quot;)
}

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 := &quot;\033[1A&quot; // Move up a line
	s += &quot;\033[K&quot;  // Clear the line
	s += &quot;\r&quot;      // 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.

答案1

得分: 1

我认为 ANSI 代码只是一个误导。我下载了该库并尝试在本地运行,发现以下部分是导致此问题的原因:

case s := <-l.printQ: // 检查是否有要打印的消息
    fmt.Fprintln(l.Writer, s)

printQ 通道关闭时,有时会执行这个 case,即使没有打印任何内容,光标似乎也会向下移动。当我将关闭通道的调用移到 l.printDone 被调用之后时,这种行为消失了。

...
// 等待打印循环完成
<-l.printDone

if l.printQ != nil {
    close(l.printQ)
}
...

这确保了当通道关闭时,循环不再运行,因此 s := <-l.printQ 这个 case 就不会执行。

英文:

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 := &lt;-l.printQ: // Check if there&#39;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                       
&lt;-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 := &lt;-l.printQ case cannot run.

huangapple
  • 本文由 发表于 2021年10月19日 06:23:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/69623190.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定