diff --git a/nft/loading/loading.go b/nft/loading/loading.go new file mode 100644 index 0000000..3c14ad3 --- /dev/null +++ b/nft/loading/loading.go @@ -0,0 +1,123 @@ +package loading + +import ( + "context" + "fmt" + "time" +) + +type Type int + +const ( + TypeProcessing Type = iota + TypeInfo + TypeSuccess + TypeWarning + TypeError +) + +func (t Type) Symbol() string { + switch t { + case TypeSuccess: + return "✔️ " + case TypeWarning: + return "❗ " + case TypeError: + return "❌ " + case TypeInfo: + return "❕ " + default: + return "" + } +} + +type _msg struct { + msg string + t Type +} + +var frames = []string{"|", "/", "-", "\\"} + +func Do(ctx context.Context, fn func(ctx context.Context, print func(msg string, types ...Type)) error) (err error) { + start := time.Now() + ch := make(chan *_msg) + + go func() { + var ( + m *_msg + ok bool + processing string + ) + + defer func() { + fmt.Printf("\r\033[K") + }() + + for { + for _, frame := range frames { + select { + case <-ctx.Done(): + return + case m, ok = <-ch: + if !ok || m == nil { + return + } + + switch m.t { + case TypeProcessing: + if m.msg != "" { + processing = m.msg + } + case TypeInfo, + TypeSuccess, + TypeWarning, + TypeError: + // Clear the loading animation + fmt.Printf("\r\033[K") + fmt.Printf("%s%s\n", m.t.Symbol(), m.msg) + } + default: + elapsed := time.Since(start).Seconds() + if processing != "" { + fmt.Printf("\r\033[K%s %s (%.2fs)", frame, processing, elapsed) + } + time.Sleep(100 * time.Millisecond) + } + } + } + }() + + printFn := func(msg string, types ...Type) { + if msg == "" { + return + } + + m := &_msg{ + msg: msg, + t: TypeProcessing, + } + + if len(types) > 0 { + m.t = types[0] + } + + ch <- m + } + + done := make(chan struct{}) + go func() { + if err = fn(ctx, printFn); err != nil { + ch <- &_msg{msg: err.Error(), t: TypeError} + } + + close(ch) + done <- struct{}{} + }() + + select { + case <-ctx.Done(): + case <-done: + } + + return err +} diff --git a/nft/loading/loading_test.go b/nft/loading/loading_test.go new file mode 100644 index 0000000..de4d7e8 --- /dev/null +++ b/nft/loading/loading_test.go @@ -0,0 +1,25 @@ +package loading + +import ( + "context" + "os/signal" + "syscall" + "testing" + "time" +) + +func TestLoadingPrint(t *testing.T) { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer cancel() + + Do(ctx, func(ctx context.Context, print func(msg string, types ...Type)) error { + print("start task 1...") + time.Sleep(3 * time.Second) + + print("warning...1", TypeWarning) + + time.Sleep(2 * time.Second) + + return nil + }) +} diff --git a/nft/nfctl/internal/cmd/cmd.new.go b/nft/nfctl/internal/cmd/cmd.new.go index 0636396..b14bf8a 100644 --- a/nft/nfctl/internal/cmd/cmd.new.go +++ b/nft/nfctl/internal/cmd/cmd.new.go @@ -3,6 +3,7 @@ package cmd import ( "bufio" "bytes" + "context" "errors" "fmt" "os" @@ -11,9 +12,9 @@ import ( "strings" "text/template" + "github.com/loveuer/nf/nft/loading" "github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/nfctl/internal/opt" - "github.com/loveuer/nf/nft/nfctl/pkg/loading" "github.com/loveuer/nf/nft/tool" "github.com/spf13/cobra" ) @@ -32,7 +33,7 @@ func initNew() *cobra.Command { return newCmd } -func doNew(cmd *cobra.Command, args []string) error { +func doNew(cmd *cobra.Command, args []string) (err error) { if len(args) == 0 { return errors.New("必须提供 project 名称") } @@ -46,108 +47,103 @@ func doNew(cmd *cobra.Command, args []string) error { return errors.New("project 名称不能以 . 开头") } - ch := make(chan *loading.Loading) - defer close(ch) + return loading.Do(cmd.Context(), func(ctx context.Context, print func(msg string, types ...loading.Type)) error { + print("开始新建项目: "+args[0], loading.TypeInfo) - go loading.Print(cmd.Context(), ch) - ch <- &loading.Loading{Content: "开始新建项目: " + args[0], Type: loading.TypeInfo} - - pwd, err := os.Getwd() - if err != nil { - ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError} - return err - } - - moduleName := args[0] - pwd = path.Join(filepath.ToSlash(pwd), base) - - log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template) - - ch <- &loading.Loading{Content: "开始下载模板: " + opt.Cfg.New.Template, Type: loading.TypeProcessing} - - repo := opt.Cfg.New.Template - if v, ok := opt.TemplateMap[repo]; ok { - repo = v - } - - if err = tool.Clone(pwd, repo); err != nil { - ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError} - return err - } - - ch <- &loading.Loading{Content: "下载模板完成: " + opt.Cfg.New.Template, Type: loading.TypeSuccess} - - if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil { - ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeWarning} - } - - ch <- &loading.Loading{Content: "开始初始化项目: " + args[0], Type: loading.TypeProcessing} - - if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { + pwd, err := os.Getwd() if err != nil { return err } - if info.IsDir() { - return nil + moduleName := args[0] + pwd = path.Join(filepath.ToSlash(pwd), base) + + log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template) + + print("开始下载模板: "+opt.Cfg.New.Template, loading.TypeProcessing) + + repo := opt.Cfg.New.Template + if v, ok := opt.TemplateMap[repo]; ok { + repo = v } - if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") { - var content []byte - if content, err = os.ReadFile(path); err != nil { - ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning} - ch <- &loading.Loading{Content: "开始初始化项目: " + args[0], Type: loading.TypeProcessing} + if err = tool.Clone(pwd, repo); err != nil { + return err + } + + print("下载模板完成: "+opt.Cfg.New.Template, loading.TypeSuccess) + + if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil { + print(err.Error(), loading.TypeWarning) + } + + print("开始初始化项目: "+args[0], loading.TypeProcessing) + + if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { return nil } - scanner := bufio.NewScanner(bytes.NewReader(content)) - replaced := make([]string, 0, 16) - for scanner.Scan() { - line := scanner.Text() - // 操作 go.mod 文件时, 忽略 toolchain 行, 以更好的兼容 go1.20 - if strings.HasSuffix(path, "go.mod") && strings.HasPrefix(line, "toolchain") { - continue + if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") { + var content []byte + if content, err = os.ReadFile(path); err != nil { + print("初始化文件失败: "+err.Error(), loading.TypeWarning) + print("开始初始化项目: "+args[0], loading.TypeProcessing) + return nil + } + + scanner := bufio.NewScanner(bytes.NewReader(content)) + replaced := make([]string, 0, 16) + for scanner.Scan() { + line := scanner.Text() + // 操作 go.mod 文件时, 忽略 toolchain 行, 以更好的兼容 go1.20 + if strings.HasSuffix(path, "go.mod") && strings.HasPrefix(line, "toolchain") { + continue + } + replaced = append(replaced, strings.ReplaceAll(line, opt.Cfg.New.Template, moduleName)) + } + if err = os.WriteFile(path, []byte(strings.Join(replaced, "\n")), 0o644); err != nil { + return err } - replaced = append(replaced, strings.ReplaceAll(line, opt.Cfg.New.Template, moduleName)) - } - if err = os.WriteFile(path, []byte(strings.Join(replaced, "\n")), 0o644); err != nil { - return err } + + return nil + }); err != nil { + return err } + var ( + render *template.Template + rf *os.File + ) + + if render, err = template.New(base).Parse(opt.README); err != nil { + log.Debug("cmd.new: new text template err, err = %s", err.Error()) + print("生成 readme 失败", loading.TypeWarning) + goto END + } + + if rf, err = os.OpenFile(path.Join(pwd, "readme.md"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil { + log.Debug("cmd.new: new readme file err, err = %s", err.Error()) + print("生成 readme 失败", loading.TypeWarning) + goto END + } + defer rf.Close() + + if err = render.Execute(rf, map[string]any{ + "project_name": base, + }); err != nil { + log.Debug("cmd.new: template execute err, err = %s", err.Error()) + print("生成 readme 失败", loading.TypeWarning) + } + + END: + print(fmt.Sprintf("项目: %s 初始化成功", args[0]), loading.TypeSuccess) + return nil - }); err != nil { - ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning} - return err - } - - var ( - render *template.Template - rf *os.File - ) - - if render, err = template.New(base).Parse(opt.README); err != nil { - log.Debug("cmd.new: new text template err, err = %s", err.Error()) - ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning} - goto END - } - - if rf, err = os.OpenFile(path.Join(pwd, "readme.md"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil { - log.Debug("cmd.new: new readme file err, err = %s", err.Error()) - ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning} - goto END - } - defer rf.Close() - - if err = render.Execute(rf, map[string]any{ - "project_name": base, - }); err != nil { - log.Debug("cmd.new: template execute err, err = %s", err.Error()) - ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning} - } - -END: - ch <- &loading.Loading{Content: fmt.Sprintf("项目: %s 初始化成功", args[0]), Type: loading.TypeSuccess} - - return nil + }) } diff --git a/nft/nfctl/internal/cmd/cmd.update.go b/nft/nfctl/internal/cmd/cmd.update.go index 8734585..6e06110 100644 --- a/nft/nfctl/internal/cmd/cmd.update.go +++ b/nft/nfctl/internal/cmd/cmd.update.go @@ -9,16 +9,19 @@ import ( "time" resty "github.com/go-resty/resty/v2" + "github.com/loveuer/nf/nft/loading" "github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/nfctl/internal/opt" - "github.com/loveuer/nf/nft/nfctl/pkg/loading" + "github.com/loveuer/nf/nft/tool" "github.com/spf13/cobra" ) var updateCmd = &cobra.Command{ Use: "update", Short: "update nfctl self", - RunE: func(cmd *cobra.Command, args []string) error { return nil }, + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, } func initUpdate() *cobra.Command { @@ -26,61 +29,55 @@ func initUpdate() *cobra.Command { } func doUpdate(ctx context.Context) (err error) { - ch := make(chan *loading.Loading) - defer close(ch) + return loading.Do(tool.TimeoutCtx(ctx, 30), func(ctx context.Context, print func(msg string, types ...loading.Type)) error { + print("正在检查更新...") + tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master" + version := "" - go func() { - loading.Print(ctx, ch) - }() - - ch <- &loading.Loading{Content: "正在检查更新...", Type: loading.TypeProcessing} - tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@latest" - version := "" - - var rr *resty.Response - if rr, err = resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).R(). - SetContext(ctx). - Get(opt.VersionURL); err != nil { - err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip) - ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError} - return err - } - - log.Debug("cmd.update: url = %s, raw_response = %s", opt.VersionURL, rr.String()) - - if rr.StatusCode() != 200 { - err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip) - ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError} - return - } - - reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`) - for _, line := range strings.Split(rr.String(), "\n") { - if reg.MatchString(line) { - version = strings.TrimSpace(strings.TrimPrefix(line, "const Version = ")) - version = version[1 : len(version)-1] - break + var rr *resty.Response + if rr, err = resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).R(). + SetContext(ctx). + Get(opt.VersionURL); err != nil { + err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip) + return err } - } - if version == "" { - err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip) - ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError} - return err - } + log.Debug("cmd.update: url = %s, raw_response = %s", opt.VersionURL, rr.String()) - log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version) + if rr.StatusCode() != 200 { + err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip) + return err + } + + reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`) + for _, line := range strings.Split(rr.String(), "\n") { + if reg.MatchString(line) { + version = strings.TrimSpace(strings.TrimPrefix(line, "const Version = ")) + version = version[1 : len(version)-1] + break + } + } + + if version == "" { + err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip) + return err + } + + log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version) + + if version <= opt.Version { + print(fmt.Sprintf("已是最新版本: %s", opt.Version), loading.TypeSuccess) + return nil + } + + print(fmt.Sprintf("发现新版本: %s", version), loading.TypeInfo) + + print(fmt.Sprintf("正在更新到 %s ...", version)) + + time.Sleep(2 * time.Second) + + print("暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master", loading.TypeWarning) - if version <= opt.Version { - ch <- &loading.Loading{Content: fmt.Sprintf("已是最新版本: %s", opt.Version), Type: loading.TypeSuccess} return nil - } - - ch <- &loading.Loading{Content: fmt.Sprintf("发现新版本: %s", version), Type: loading.TypeInfo} - - ch <- &loading.Loading{Content: fmt.Sprintf("正在更新到 %s ...", version)} - - time.Sleep(2 * time.Second) - ch <- &loading.Loading{Content: "暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@latest", Type: loading.TypeWarning} - return nil + }) } diff --git a/nft/nfctl/internal/opt/version.go b/nft/nfctl/internal/opt/version.go index 137f315..1021386 100644 --- a/nft/nfctl/internal/opt/version.go +++ b/nft/nfctl/internal/opt/version.go @@ -1,6 +1,6 @@ package opt -const Version = "v24.12.27-r02" +const Version = "v24.12.27-r03" // const VersionURL = "https://github.com/loveuer/nf/nft/nfctl/internal/opt/version.go" diff --git a/nft/nfctl/pkg/loading/loading.go b/nft/nfctl/pkg/loading/loading.go deleted file mode 100644 index 0557353..0000000 --- a/nft/nfctl/pkg/loading/loading.go +++ /dev/null @@ -1,81 +0,0 @@ -package loading - -import ( - "context" - "fmt" - "time" -) - -type Type int - -const ( - TypeProcessing Type = iota - TypeInfo - TypeSuccess - TypeWarning - TypeError -) - -func (t Type) Symbol() string { - switch t { - case TypeSuccess: - return "✔️ " - case TypeWarning: - return "❗ " - case TypeError: - return "❌ " - case TypeInfo: - return "❕ " - default: - return "" - } -} - -type Loading struct { - Content string - Type Type -} - -func Print(ctx context.Context, ch <-chan *Loading) { - var ( - ok bool - frames = []string{"|", "/", "-", "\\"} - start = time.Now() - loading = &Loading{} - ) - - for { - for _, frame := range frames { - select { - case <-ctx.Done(): - return - case loading, ok = <-ch: - if !ok || loading == nil { - return - } - - if loading.Content == "" { - time.Sleep(100 * time.Millisecond) - continue - } - - switch loading.Type { - case TypeInfo, - TypeSuccess, - TypeWarning, - TypeError: - // Clear the loading animation - fmt.Printf("\r\033[K") - fmt.Printf("%s%s\n", loading.Type.Symbol(), loading.Content) - loading.Content = "" - } - default: - elapsed := time.Since(start).Seconds() - if loading.Content != "" { - fmt.Printf("\r\033[K%s %s (%.2fs)", frame, loading.Content, elapsed) - } - time.Sleep(100 * time.Millisecond) - } - } - } -} diff --git a/nft/nfctl/pkg/loading/loading_test.go b/nft/nfctl/pkg/loading/loading_test.go deleted file mode 100644 index 2c036a5..0000000 --- a/nft/nfctl/pkg/loading/loading_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package loading - -import ( - "context" - "testing" - "time" -) - -func TestLoadingPrint(t *testing.T) { - ch := make(chan *Loading) - - Print(context.TODO(), ch) - ch <- &Loading{Content: "处理中(1)..."} - - time.Sleep(3 * time.Second) - - ch <- &Loading{Content: "处理完成(1)", Type: TypeSuccess} - - ch <- &Loading{Content: "处理中(2)..."} - - time.Sleep(4 * time.Second) - - ch <- &Loading{Content: "处理失败(2)", Type: TypeError} - - time.Sleep(2 * time.Second) - close(ch) -}