你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

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

How can I use a spinner in addition to goroutines so that the output is not overriding each other?

问题

我正在尝试使用一个旋转器来指示几个 goroutine 中的工作进度。我遇到的问题是旋转器消息和作业完成消息都被记录在同一行中。我的问题是如何将旋转器固定在底部,以便旋转器状态不会妨碍日志消息本身?

我得到的输出是:

a
⡿ 1/26c
⣽ 2/26b
⣯ 3/26e
⢿ 4/26d
⣾ 5/26g
⣟ 6/26f

我想要的输出是:

a
b
c
d
e
f
⣟ 6/26

临时工作代码(我意识到我的 goroutine 可能不太优雅,但这只是一个示例)。

package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/theckman/yacspin"
)

func main() {
	// 创建一个旋转器
	spinner, err := createSpinner()
	if err != nil {
		fmt.Printf("failed to make spinner from config struct: %v\n", err)
		os.Exit(1)
	}

	// 启动旋转器
	if err := spinner.Start(); err != nil {
		panic(err)
	}

	wg := &sync.WaitGroup{}
	wg.Add(1)
	var (
		total   int
		current int
	)

	spinnerCh := make(chan int, 0)
	data := make(chan string)

	// 只想要一个 goroutine 在运行,但这不重要
	max := make(chan struct{}, 1)

	go func(s *yacspin.Spinner) {
		// 尝试将此代码移到 worker goroutine 中,但效果相同
		for range spinnerCh {
			current += 1
			if err := s.Pause(); err != nil {
				panic(err)
			}
			s.Message(fmt.Sprintf("%d/%d", current, total))
			if err := s.Unpause(); err != nil {
				panic(err)
			}
		}
	}(spinner)

	go func() {
		defer wg.Done()
		for d := range data {
			wg.Add(1)
			go func(wg *sync.WaitGroup, d string) {
				max <- struct{}{}
				defer func() {
					<-max
				}()

				// 执行工作并在完成后打印结果。
				fmt.Println(d)

				// 向旋转器的 goroutine 发送一个值,以便它可以显示更新后的计数
				time.Sleep(500 * time.Millisecond)
				spinnerCh <- 1
				wg.Done()
			}(wg, d)
		}
	}()

	// 模拟排队一些工作
	ss := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
	for _, s := range ss {
		data <- s
	}
	total = len(ss)

	close(data)
	wg.Wait()
	close(spinnerCh)
}

func createSpinner() (*yacspin.Spinner, error) {
	// 构建配置,每个字段都有文档说明
	cfg := yacspin.Config{
		Frequency:         100 * time.Millisecond,
		CharSet:           yacspin.CharSets[11],
		Suffix:            " ", // 在动画旋转器和消息之间至少放一个空格
		// Message:           "collecting files",
		SuffixAutoColon:   true,
		ColorAll:          true,
		Colors:            []string{"fgYellow"},
		StopCharacter:     "✓",
		StopColors:        []string{"fgGreen"},
		StopMessage:       "done",
		StopFailCharacter: "✗",
		StopFailColors:    []string{"fgRed"},
		StopFailMessage:   "failed",
	}

	s, err := yacspin.New(cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to make spinner from struct: %w", err)
	}

	return s, nil
}

func stopOnSignal(spinner *yacspin.Spinner) {
	// 确保在退出之前停止旋转器,否则光标将保持隐藏,终端将需要“reset”
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-sigCh

		spinner.StopFailMessage("interrupted")

		// 故意忽略错误
		_ = spinner.StopFail()

		os.Exit(0)
	}()
}

英文:

What I am trying to do is use a spinner to indicate work progress in a few goroutines. The issue I am having is that both the spinner message and job completion message is being logged in the same line. What I am asking is how can I have the spinner pinned to the bottom of so that the spinner status is not getting in the way of the logged messages itself?

The output I am getting is

a
⡿ 1/26c
⣽ 2/26b
⣯ 3/26e
⢿ 4/26d
⣾ 5/26g
⣟ 6/26f

What I am trying to get is

a
b
c
d
e
f
⣟ 6/26

Temp working code. (I realize my go routines may not be very elegant, but this is just an example).

package main

import (
	&quot;fmt&quot;
	&quot;os&quot;
	&quot;os/signal&quot;
	&quot;sync&quot;
	&quot;syscall&quot;
	&quot;time&quot;

	&quot;github.com/theckman/yacspin&quot;
)

func main() {
	// create a spinner
	spinner, err := createSpinner()
	if err != nil {
		fmt.Printf(&quot;failed to make spinner from config struct: %v\n&quot;, err)
		os.Exit(1)
	}

	// start the spinner
	if err := spinner.Start(); err != nil {
		panic(err)
	}

	wg := &amp;sync.WaitGroup{}
	wg.Add(1)
	var (
		total   int
		current int
	)

	spinnerCh := make(chan int, 0)
	data := make(chan string)

	// only want one go routine at a time but this is not important
	max := make(chan struct{}, 1)

	go func(s *yacspin.Spinner) {
        // tried moving this into the worker goroutine also, but same effect
		for range spinnerCh {
			current += 1
			if err := s.Pause(); err != nil {
				panic(err)
			}
			s.Message(fmt.Sprintf(&quot;%d/%d&quot;, current, total))
			if err := s.Unpause(); err != nil {
				panic(err)
			}
		}
	}(spinner)

	go func() {
		defer wg.Done()
		for d := range data {
			wg.Add(1)
			go func(wg *sync.WaitGroup, d string) {
				max &lt;- struct{}{}
				defer func() {
					&lt;-max
				}()

				// function is doing work and printing the result once done.
				fmt.Println(d)

				// sends a value to the spinner go routine so that it can show
				// the updated count
				time.Sleep(500 * time.Millisecond)
				spinnerCh &lt;- 1
				wg.Done()
			}(wg, d)
		}
	}()

	// simulate queing some work
	ss := []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;, &quot;i&quot;, &quot;j&quot;, &quot;k&quot;, &quot;l&quot;, &quot;m&quot;, &quot;n&quot;, &quot;o&quot;, &quot;p&quot;, &quot;q&quot;, &quot;r&quot;, &quot;s&quot;, &quot;t&quot;, &quot;u&quot;, &quot;v&quot;, &quot;w&quot;, &quot;x&quot;, &quot;y&quot;, &quot;z&quot;}
	for _, s := range ss {
		data &lt;- s
	}
	total = len(ss)

	close(data)
	wg.Wait()
	close(spinnerCh)
}

func createSpinner() (*yacspin.Spinner, error) {
	// build the configuration, each field is documented
	cfg := yacspin.Config{
		Frequency: 100 * time.Millisecond,
		CharSet:   yacspin.CharSets[11],
		Suffix:    &quot; &quot;, // puts a least one space between the animating spinner and the Message
		// Message:           &quot;collecting files&quot;,
		SuffixAutoColon:   true,
		ColorAll:          true,
		Colors:            []string{&quot;fgYellow&quot;},
		StopCharacter:     &quot;&quot;,
		StopColors:        []string{&quot;fgGreen&quot;},
		StopMessage:       &quot;done&quot;,
		StopFailCharacter: &quot;&quot;,
		StopFailColors:    []string{&quot;fgRed&quot;},
		StopFailMessage:   &quot;failed&quot;,
	}

	s, err := yacspin.New(cfg)
	if err != nil {
		return nil, fmt.Errorf(&quot;failed to make spinner from struct: %w&quot;, err)
	}

	return s, nil
}

func stopOnSignal(spinner *yacspin.Spinner) {
	// ensure we stop the spinner before exiting, otherwise cursor will remain
	// hidden and terminal will require a `reset`
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	go func() {
		&lt;-sigCh

		spinner.StopFailMessage(&quot;interrupted&quot;)

		// ignoring error intentionally
		_ = spinner.StopFail()

		os.Exit(0)
	}()
}

答案1

得分: 1

根据你的初始问题,以下是不显示行中旋转器的解决方案:

1)引入一个变量 pauseItForAMoment := false,它将帮助在一个goroutine中忽略是否暂停。

func main() {
	pauseItForAMoment := false

goroutine看起来像这样:

	go func(s *yacspin.Spinner) {
		// tried moving this into the worker goroutine also, but same effect
		for range spinnerCh {
			current += 1
			if !pauseItForAMoment {
				if err := s.Pause(); err != nil {
					panic(err)
				}
				s.Message(fmt.Sprintf("%d/%d", current, total))
				if err := s.Unpause(); err != nil {
					panic(err)
				}
			}
		}
	}(spinner)

2)在打印d时停止和启动旋转器:

				pauseItForAMoment = true
				spinner.Prefix(d)
				spinner.Stop()
				// fmt.Println(d)
				spinner.Start()
				pauseItForAMoment = false

3)将旋转器的一些配置值更改为:

		StopCharacter:   " ",
		// StopMessage:       "done",

请注意,你的goroutines并不完全正确地按顺序打印它们,你已经知道了,希望你能修复它。

输出如下所示:

你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

如果有什么不清楚的地方,这是代码 https://go.dev/play/p/0lOza3aapf2

我更喜欢以下面这种方式显示输出,代码在这里 https://go.dev/play/p/W8Y1kwNqphl (只是我的个人意见;-) 如果你不需要可以忽略它)

你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

英文:

Based on your initial question, here is the solution for not showing the spinner in the lines:

  1. Introduce a variable

pauseItForAMoment := false which will help to ignore to do Pause or not in one of your goroutine.

func main() {
pauseItForAMoment := false

And the goroutine looks like this:

		// tried moving this into the worker goroutine also, but same effect
		for range spinnerCh {
			current += 1
			if !pauseItForAMoment {
				if err := s.Pause(); err != nil {
					panic(err)
				}
				s.Message(fmt.Sprintf(&quot;%d/%d&quot;, current, total))
				if err := s.Unpause(); err != nil {
					panic(err)
				}
			}
		}
	}(spinner)
  1. Stop and Start the spinner while printing d
				pauseItForAMoment = true
spinner.Prefix(d)
spinner.Stop()
// fmt.Println(d)
spinner.Start()
pauseItForAMoment = false
  1. Change few of the spinner config values to:
		StopCharacter:   &quot; &quot;,
// StopMessage:       &quot;done&quot;,

Note it, your goroutines are not exactly correct to print them in the order, which you know already, hope you will fix that.

Output looks like this:

你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

Here is the code if something is not clear https://go.dev/play/p/0lOza3aapf2

I prefer to show the output like this below & the code for that is https://go.dev/play/p/W8Y1kwNqphl (just my 2cents 你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。 ignore it if you don't need it)

你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

答案2

得分: 0

这是你要翻译的内容:

这是库的作者。根据你在你打开的GitHub问题中分享的代码片段,我认为你只需要将StopCharacter的值设置为空字符串,而不是&quot; &quot;。这样对我来说就可以渲染成这样...

你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

这是代码在Go Playground上的链接,因为我太笨了,不知道如何在这里复制和粘贴而不丢失格式。

希望这可以帮到你!

英文:

Author of the library here. Based on the snippet you shared in the GitHub issue you opened, I think you just need to set the StopCharacter value to an empty string instead of &quot; &quot;. It made it render like this for me...

你可以在使用goroutine的同时使用spinner,以避免输出互相覆盖。

Here is the code on the Go Playground, since I am too dumb to figure out how to copy and paste it here without losing formatting.

I hope this helps!

huangapple
  • 本文由 发表于 2022年4月19日 07:54:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/71918215.html
匿名

发表评论

匿名网友

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

确定