diff --git a/cli/account_command.go b/cli/account_command.go index 7ce52112..fe90c0b1 100644 --- a/cli/account_command.go +++ b/cli/account_command.go @@ -234,7 +234,7 @@ func (c *actCmd) reportServerStats(_ *fisk.ParseContext) error { return fmt.Errorf("did not get results from any servers") } - table := newTableWriter("Server Statistics") + table := iu.NewTableWriter(opts(), "Server Statistics") table.AddHeaders("Server", "Cluster", "Version", "Tags", "Connections", "Subscriptions", "Leafnodes", "Sent Bytes", "Sent Messages", "Received Bytes", "Received Messages", "Slow Consumers") var ( diff --git a/cli/auth_account_command.go b/cli/auth_account_command.go index 78642055..957da611 100644 --- a/cli/auth_account_command.go +++ b/cli/auth_account_command.go @@ -23,7 +23,7 @@ import ( "time" au "github.com/nats-io/natscli/internal/auth" - "github.com/nats-io/natscli/internal/util" + iu "github.com/nats-io/natscli/internal/util" "github.com/AlecAivazis/survey/v2" "github.com/choria-io/fisk" @@ -492,7 +492,7 @@ func (c *authAccountCommand) skAddAction(_ *fisk.ParseContext) error { } if c.skRole == "" { - err := util.AskOne(&survey.Input{ + err := iu.AskOne(&survey.Input{ Message: "Role Name", Help: "The role to associate with this key", }, &c.skRole, survey.WithValidator(survey.Required)) @@ -601,10 +601,10 @@ func (c *authAccountCommand) skListAction(_ *fisk.ParseContext) error { return err } - var table *tbl + var table *iu.Table if len(acct.ScopedSigningKeys().List()) > 0 { - table = newTableWriter("Scoped Signing Keys") + table = iu.NewTableWriter(opts(), "Scoped Signing Keys") table.AddHeaders("Role", "Key", "Description", "Max Subscriptions", "Pub Perms", "Sub Perms") for _, sk := range acct.ScopedSigningKeys().List() { scope, _ := acct.ScopedSigningKeys().GetScope(sk) @@ -765,7 +765,7 @@ func (c *authAccountCommand) lsAction(_ *fisk.ParseContext) error { return nil } - table := newTableWriter("Accounts") + table := iu.NewTableWriter(opts(), "Accounts") table.AddHeaders("Name", "Subject", "Users", "JetStream", "System") for _, acct := range list { system := "" @@ -894,7 +894,7 @@ func (c *authAccountCommand) addAction(_ *fisk.ParseContext) error { } if c.accountName == "" { - err := util.AskOne(&survey.Input{ + err := iu.AskOne(&survey.Input{ Message: "Account Name", Help: "A unique name for the Account being added", }, &c.accountName, survey.WithValidator(survey.Required)) diff --git a/cli/auth_account_exports.go b/cli/auth_account_exports.go index 50640bed..bea44e42 100644 --- a/cli/auth_account_exports.go +++ b/cli/auth_account_exports.go @@ -355,7 +355,7 @@ func (c *authAccountCommand) exportLsAction(_ *fisk.ParseContext) error { exports := c.exportBySubject(acct) - tbl := newTableWriter("Exports for account %s", acct.Name()) + tbl := util.NewTableWriter(opts(), "Exports for account %s", acct.Name()) tbl.AddHeaders("Name", "Kind", "Subject", "Activation Required", "Advertised", "Token Position", "Revocations") for _, e := range exports { diff --git a/cli/auth_account_imports.go b/cli/auth_account_imports.go index 555240d6..8983cb99 100644 --- a/cli/auth_account_imports.go +++ b/cli/auth_account_imports.go @@ -176,7 +176,7 @@ func (c *authAccountCommand) importLsAction(_ *fisk.ParseContext) error { imports := c.importsBySubject(acct) - tbl := newTableWriter("Imports for account %s", acct.Name()) + tbl := util.NewTableWriter(opts(), "Imports for account %s", acct.Name()) tbl.AddHeaders("Name", "Kind", "Source", "Local Subject", "Remote Subject", "Allows Tracing", "Sharing Connection Info") for _, i := range imports { diff --git a/cli/auth_operator_command.go b/cli/auth_operator_command.go index 03a51dd2..97f94831 100644 --- a/cli/auth_operator_command.go +++ b/cli/auth_operator_command.go @@ -446,7 +446,7 @@ func (c *authOperatorCommand) lsAction(_ *fisk.ParseContext) error { return nil } - table := newTableWriter("Operators") + table := iu.NewTableWriter(opts(), "Operators") table.AddHeaders("Name", "Subject", "Accounts", "Account Server", "Signing Keys") for _, op := range list { table.AddRow(op.Name(), op.Subject(), len(op.Accounts().List()), op.AccountServerURL(), len(op.SigningKeys().List())) diff --git a/cli/auth_user_command.go b/cli/auth_user_command.go index 08ea8436..ef529e1f 100644 --- a/cli/auth_user_command.go +++ b/cli/auth_user_command.go @@ -261,7 +261,7 @@ func (c *authUserCommand) lsAction(_ *fisk.ParseContext) error { return nil } - table := newTableWriter(fmt.Sprintf("Users in account %s", acct.Name())) + table := iu.NewTableWriter(opts(), fmt.Sprintf("Users in account %s", acct.Name())) table.AddHeaders("Name", "Subject", "Scoped", "Sub Perms", "Pub Perms", "Max Subscriptions") for _, user := range users { limits := ab.UserLimits(user) diff --git a/cli/consumer_command.go b/cli/consumer_command.go index adb87fee..b768dc63 100644 --- a/cli/consumer_command.go +++ b/cli/consumer_command.go @@ -946,7 +946,7 @@ func (c *consumerCmd) lsAction(pc *fisk.ParseContext) error { func (c *consumerCmd) renderConsumerAsTable(stream *jsm.Stream) (string, error) { var out bytes.Buffer - table := newTableWriter("Consumers") + table := iu.NewTableWriter(opts(), "Consumers") table.AddHeaders("Name", "Description", "Created", "Ack Pending", "Unprocessed", "Last Delivery") missing, err := stream.EachConsumer(func(cons *jsm.Consumer) { @@ -2305,7 +2305,7 @@ func (c *consumerCmd) reportAction(_ *fisk.ParseContext) error { leaders := make(map[string]*raftLeader) - table := newTableWriter(fmt.Sprintf("Consumer report for %s with %s consumers", c.stream, f(ss.Consumers))) + table := iu.NewTableWriter(opts(), fmt.Sprintf("Consumer report for %s with %s consumers", c.stream, f(ss.Consumers))) table.AddHeaders("Consumer", "Mode", "Ack Policy", "Ack Wait", "Ack Pending", "Redelivered", "Unprocessed", "Ack Floor", "Cluster") missing, err := s.EachConsumer(func(cons *jsm.Consumer) { cs, err := cons.LatestState() @@ -2372,7 +2372,7 @@ func (c *consumerCmd) renderMissing(out io.Writer, missing []string) { if len(missing) > 0 { fmt.Fprintln(out) sort.Strings(missing) - table := newTableWriter("Inaccessible Consumers") + table := iu.NewTableWriter(opts(), "Inaccessible Consumers") iu.SliceGroups(missing, 4, func(names []string) { table.AddRow(toany(names)...) }) diff --git a/cli/context_command.go b/cli/context_command.go index 355d810c..ba0eaaef 100644 --- a/cli/context_command.go +++ b/cli/context_command.go @@ -373,7 +373,7 @@ func (c *ctxCommand) renderListTable(current string, known []*natscontext.Contex return } - table := newTableWriter("Known Contexts") + table := iu.NewTableWriter(opts(), "Known Contexts") table.AddHeaders("Name", "Description") for _, nctx := range known { diff --git a/cli/errors_command.go b/cli/errors_command.go index dbc4314f..35fc3c28 100644 --- a/cli/errors_command.go +++ b/cli/errors_command.go @@ -112,7 +112,7 @@ func (c *errCmd) listAction(_ *fisk.ParseContext) error { } }) - table := newTableWriter("NATS Errors") + table := iu.NewTableWriter(opts(), "NATS Errors") table.AddHeaders("NATS Code", "HTTP Error Code", "Description", "Comment", "Go Constant") for _, v := range matched { table.AddRow(v.ErrCode, v.Code, v.Description, v.Comment, v.Constant) diff --git a/cli/kv_command.go b/cli/kv_command.go index c9104013..7f111fe1 100644 --- a/cli/kv_command.go +++ b/cli/kv_command.go @@ -252,7 +252,7 @@ func (c *kvCommand) displayKeyInfo(kv jetstream.KeyValue, keys jetstream.KeyList return found, errors.New("key value cannot be nil") } - table := newTableWriter(fmt.Sprintf("Contents for bucket '%s'", c.bucket)) + table := util.NewTableWriter(opts(), fmt.Sprintf("Contents for bucket '%s'", c.bucket)) if c.lsVerboseDisplayValue { table.AddHeaders("Key", "Created", "Delta", "Revision", "Value") @@ -325,7 +325,7 @@ func (c *kvCommand) lsBuckets() error { return info.State.Bytes < jnfo.State.Bytes }) - table := newTableWriter("Key-Value Buckets") + table := util.NewTableWriter(opts(), "Key-Value Buckets") table.AddHeaders("Bucket", "Description", "Created", "Size", "Values", "Last Update") for _, s := range found { nfo, _ := s.LatestInformation() @@ -400,7 +400,7 @@ func (c *kvCommand) historyAction(_ *fisk.ParseContext) error { return err } - table := newTableWriter(fmt.Sprintf("History for %s > %s", c.bucket, c.key)) + table := util.NewTableWriter(opts(), fmt.Sprintf("History for %s > %s", c.bucket, c.key)) table.AddHeaders("Key", "Revision", "Op", "Created", "Length", "Value") for _, r := range history { val := base64IfNotPrintable(r.Value()) diff --git a/cli/object_command.go b/cli/object_command.go index 9feb5a5b..c3ab3c99 100644 --- a/cli/object_command.go +++ b/cli/object_command.go @@ -17,6 +17,7 @@ import ( "context" "encoding/base64" "fmt" + "github.com/jedib0t/go-pretty/v6/progress" "io" "os" "path/filepath" @@ -25,13 +26,12 @@ import ( "time" "github.com/nats-io/nats.go/jetstream" - "github.com/nats-io/natscli/internal/util" + iu "github.com/nats-io/natscli/internal/util" "github.com/AlecAivazis/survey/v2" "github.com/choria-io/fisk" "github.com/dustin/go-humanize" "github.com/fatih/color" - "github.com/gosuri/uiprogress" "github.com/nats-io/jsm.go" "github.com/nats-io/nats.go" ) @@ -401,7 +401,7 @@ func (c *objCommand) listBuckets() error { return info.State.Bytes < jnfo.State.Bytes }) - table := newTableWriter("Object Store Buckets") + table := iu.NewTableWriter(opts(), "Object Store Buckets") table.AddHeaders("Bucket", "Description", "Created", "Size", "Last Update") for _, s := range found { nfo, _ := s.LatestInformation() @@ -444,7 +444,7 @@ func (c *objCommand) lsAction(_ *fisk.ParseContext) error { return nil } - table := newTableWriter("Bucket Contents") + table := iu.NewTableWriter(opts(), "Bucket Contents") table.AddHeaders("Name", "Size", "Time") for _, i := range contents { @@ -484,6 +484,7 @@ func (c *objCommand) putAction(_ *fisk.ParseContext) error { if !ok { return nil } + fmt.Println() } hdr, err := parseStringsToHeader(c.hdrs, 0) @@ -519,28 +520,35 @@ func (c *objCommand) putAction(_ *fisk.ParseContext) error { Headers: hdr, } - var progress *uiprogress.Bar + var progbar progress.Writer + var tracker *progress.Tracker + stop := func() {} if !opts().Trace && c.progress && stat != nil && stat.Size() > 20480 { - hs := humanize.IBytes(uint64(stat.Size())) - progress = uiprogress.AddBar(int(stat.Size())).PrependFunc(func(b *uiprogress.Bar) string { - return fmt.Sprintf("%s / %s", humanize.IBytes(uint64(b.Current())), hs) + progbar, tracker, err = iu.NewProgress(opts(), &progress.Tracker{ + Total: stat.Size(), + Units: iu.ProgressUnitsIBytes, }) - progress.Width = util.ProgressWidth() + if err != nil { + return err + } - fmt.Println() - uiprogress.Start() - stop = func() { uiprogress.Stop(); fmt.Println() } - pr = &progressRW{p: progress, r: pr} + stop = func() { + time.Sleep(300 * time.Millisecond) + progbar.Stop() + fmt.Println() + } + pr = &progressRW{p: progbar, t: tracker, r: pr} } - nfo, err = obj.Put(ctx, meta, pr) + nfo, err = obj.Put(context.TODO(), meta, pr) stop() if err != nil { return err } + fmt.Println() c.showObjectInfo(nfo) return nil @@ -552,10 +560,7 @@ func (c *objCommand) getAction(_ *fisk.ParseContext) error { return err } - ctx, cancel := context.WithTimeout(ctx, opts().Timeout) - defer cancel() - - res, err := obj.Get(ctx, c.file) + res, err := obj.Get(context.Background(), c.file) if err != nil { return err } @@ -597,21 +602,28 @@ func (c *objCommand) getAction(_ *fisk.ParseContext) error { } defer of.Close() - var progress *uiprogress.Bar + var progbar progress.Writer + var tracker *progress.Tracker + pw := io.Writer(of) stop := func() {} if !opts().Trace && c.progress && nfo.Size > 20480 { - hs := humanize.IBytes(nfo.Size) - progress = uiprogress.AddBar(int(nfo.Size)).PrependFunc(func(b *uiprogress.Bar) string { - return fmt.Sprintf("%s / %s", humanize.IBytes(uint64(b.Current())), hs) + fmt.Println() + progbar, tracker, err = iu.NewProgress(opts(), &progress.Tracker{ + Total: int64(nfo.Size), + Units: iu.ProgressUnitsIBytes, }) - progress.Width = util.ProgressWidth() + if err != nil { + return err + } + stop = func() { + time.Sleep(300 * time.Millisecond) + progbar.Stop() + fmt.Println() + } - fmt.Println() - uiprogress.Start() - stop = func() { uiprogress.Stop(); fmt.Println() } - pw = &progressRW{p: progress, w: of} + pw = &progressRW{p: progbar, t: tracker, w: of} } start := time.Now() @@ -693,10 +705,10 @@ func (c *objCommand) loadBucket() (*nats.Conn, jetstream.JetStream, jetstream.Ob return nil, nil, nil, fmt.Errorf("no Object buckets found") } - err = util.AskOne(&survey.Select{ + err = iu.AskOne(&survey.Select{ Message: "Select a Bucket", Options: known, - PageSize: util.SelectPageSize(len(known)), + PageSize: iu.SelectPageSize(len(known)), }, &c.bucket) if err != nil { return nil, nil, nil, err diff --git a/cli/pub_command.go b/cli/pub_command.go index dde7be85..7d5d4af9 100644 --- a/cli/pub_command.go +++ b/cli/pub_command.go @@ -15,16 +15,16 @@ package cli import ( "fmt" + iu "github.com/nats-io/natscli/internal/util" "io" "math" "os" "time" "github.com/choria-io/fisk" - "github.com/gosuri/uiprogress" + "github.com/jedib0t/go-pretty/v6/progress" "github.com/nats-io/jsm.go" "github.com/nats-io/nats.go" - iu "github.com/nats-io/natscli/internal/util" terminal "golang.org/x/term" ) @@ -124,7 +124,7 @@ func (c *pubCmd) prepareMsg(body []byte, seq int) (*nats.Msg, error) { return msg, parseStringsToMsgHeader(c.hdrs, seq, msg) } -func (c *pubCmd) doReq(nc *nats.Conn, progress *uiprogress.Bar) error { +func (c *pubCmd) doReq(nc *nats.Conn, progress *progress.Tracker) error { logOutput := !c.raw && progress == nil for i := 1; i <= c.cnt; i++ { @@ -155,7 +155,7 @@ func (c *pubCmd) doReq(nc *nats.Conn, progress *uiprogress.Bar) error { } if progress != nil { - progress.Incr() + progress.Increment(1) } // loop through the reply count. @@ -233,7 +233,7 @@ func (c *pubCmd) doReq(nc *nats.Conn, progress *uiprogress.Bar) error { return nil } -func (c *pubCmd) doJetstream(nc *nats.Conn, progress *uiprogress.Bar) error { +func (c *pubCmd) doJetstream(nc *nats.Conn, progress *progress.Tracker) error { for i := 1; i <= c.cnt; i++ { start := time.Now() body, err := pubReplyBodyTemplate(c.body, "", i) @@ -263,7 +263,7 @@ func (c *pubCmd) doJetstream(nc *nats.Conn, progress *uiprogress.Bar) error { } if progress != nil { - progress.Incr() + progress.Increment(1) } fmt.Printf(">>> Stream: %s Sequence: %s", ack.Stream, f(ack.Sequence)) @@ -307,26 +307,29 @@ func (c *pubCmd) publish(_ *fisk.ParseContext) error { c.body = string(body) } - var progress *uiprogress.Bar + var tracker *progress.Tracker + var progbar progress.Writer + if c.cnt > 20 && !c.raw { - progressFormat := fmt.Sprintf("%%%dd / %%d", len(fmt.Sprintf("%d", c.cnt))) - progress = uiprogress.AddBar(c.cnt).PrependFunc(func(b *uiprogress.Bar) string { - return fmt.Sprintf(progressFormat, b.Current(), c.cnt) - }).AppendElapsed() - progress.Width = iu.ProgressWidth() + progbar, tracker, err = iu.NewProgress(opts(), &progress.Tracker{ + Total: int64(c.cnt), + }) + if err != nil { + return err + } - fmt.Println() - uiprogress.Start() - uiprogress.RefreshInterval = 100 * time.Millisecond - defer func() { uiprogress.Stop(); fmt.Println() }() + defer func() { + progbar.Stop() + time.Sleep(300 * time.Millisecond) + }() } if c.jetstream { - return c.doJetstream(nc, progress) + return c.doJetstream(nc, tracker) } if c.req || c.replyCount >= 1 { - return c.doReq(nc, progress) + return c.doReq(nc, tracker) } for i := 1; i <= c.cnt; i++ { @@ -355,10 +358,10 @@ func (c *pubCmd) publish(_ *fisk.ParseContext) error { time.Sleep(c.sleep) } - if progress == nil { + if progbar == nil { log.Printf("Published %d bytes to %q\n", len(body), c.subject) } else { - progress.Incr() + tracker.Increment(1) } } diff --git a/cli/server_consumer_check.go b/cli/server_consumer_check.go index c9d43039..0cd4a36f 100644 --- a/cli/server_consumer_check.go +++ b/cli/server_consumer_check.go @@ -15,6 +15,7 @@ package cli import ( "fmt" + "github.com/nats-io/natscli/internal/util" "sort" "strings" "time" @@ -174,7 +175,7 @@ func (c *ConsumerCheckCmd) consumerCheck(_ *fisk.ParseContext) error { sort.Strings(keys) fmt.Printf("Consumers: %d\n", len(keys)) - table := newTableWriter("Consumers") + table := util.NewTableWriter(opts(), "Consumers") if c.health { table.AddHeaders("Consumer", "Stream", "Raft", "Account", "Account ID", "Node", "Delivered (S,C)", "ACK Floor (S,C)", "Counters", "Status", "Leader", "Stream Cluster Leader", "Peers", "Health") diff --git a/cli/server_list_command.go b/cli/server_list_command.go index e6cebcd8..10f90bfd 100644 --- a/cli/server_list_command.go +++ b/cli/server_list_command.go @@ -195,7 +195,7 @@ func (c *SrvLsCmd) list(_ *fisk.ParseContext) error { } }) - table := newTableWriter("Server Overview") + table := iu.NewTableWriter(opts(), "Server Overview") table.AddHeaders("Name", "Cluster", "Host", "Version", "JS", "Conns", "Subs", "Routes", "GWs", "Mem", "CPU %", "Cores", "Slow", "Uptime", "RTT") // here so its after the sort @@ -316,7 +316,7 @@ func (c *SrvLsCmd) list(_ *fisk.ParseContext) error { func (c *SrvLsCmd) showClusters(cl map[string]*srvListCluster) { fmt.Println() - table := newTableWriter("Cluster Overview") + table := iu.NewTableWriter(opts(), "Cluster Overview") table.AddHeaders("Cluster", "Node Count", "Outgoing Gateways", "Incoming Gateways", "Connections") var clusters []*srvListCluster diff --git a/cli/server_report_command.go b/cli/server_report_command.go index 876b48d5..ac9bf010 100644 --- a/cli/server_report_command.go +++ b/cli/server_report_command.go @@ -274,11 +274,11 @@ func (c *SrvReportCmd) reportJetStream(_ *fisk.ParseContext) error { cNames = names } - var table *tbl + var table *iu.Table if c.account != "" { - table = newTableWriter(fmt.Sprintf("JetStream Summary for Account %s", c.account)) + table = iu.NewTableWriter(opts(), fmt.Sprintf("JetStream Summary for Account %s", c.account)) } else { - table = newTableWriter("JetStream Summary") + table = iu.NewTableWriter(opts(), "JetStream Summary") } hdrs := []any{"Server", "Cluster"} @@ -418,7 +418,7 @@ func (c *SrvReportCmd) reportJetStream(_ *fisk.ParseContext) error { cNames = names } - table := newTableWriter("RAFT Meta Group Information") + table := iu.NewTableWriter(opts(), "RAFT Meta Group Information") table.AddHeaders("Connection Name", "ID", "Leader", "Current", "Online", "Active", "Lag") for i, replica := range cluster.Replicas { leader := "" @@ -516,7 +516,7 @@ func (c *SrvReportCmd) reportAccount(_ *fisk.ParseContext) error { return nil } - table := newTableWriter(fmt.Sprintf("%d Accounts Overview", len(accounts))) + table := iu.NewTableWriter(opts(), fmt.Sprintf("%d Accounts Overview", len(accounts))) table.AddHeaders("Account", "Connections", "In Msgs", "Out Msgs", "In Bytes", "Out Bytes", "Subs") for _, acct := range accounts { @@ -634,7 +634,7 @@ func (c *SrvReportCmd) renderConnections(report []connInfo) { limit = c.topk } - table := newTableWriter(fmt.Sprintf("Top %d Connections out of %s by %s", limit, f(total), c.sort)) + table := iu.NewTableWriter(opts(), fmt.Sprintf("Top %d Connections out of %s by %s", limit, f(total), c.sort)) showReason := c.stateFilter == "closed" || c.stateFilter == "all" headers := []any{"CID", "Name", "Server", "Cluster", "IP", "Account", "Uptime", "In Msgs", "Out Msgs", "In Bytes", "Out Bytes", "Subs"} if showReason { @@ -718,7 +718,7 @@ func (c *SrvReportCmd) renderConnections(report []connInfo) { return servers[serverNames[i]].conns < servers[serverNames[j]].conns }) - table := newTableWriter("Connections per server") + table := iu.NewTableWriter(opts(), "Connections per server") table.AddHeaders("Server", "Cluster", "Connections") sort.Slice(serverNames, func(i, j int) bool { return servers[serverNames[i]].conns < servers[serverNames[j]].conns diff --git a/cli/server_stream_check.go b/cli/server_stream_check.go index 2ebcf382..5e3a79e3 100644 --- a/cli/server_stream_check.go +++ b/cli/server_stream_check.go @@ -15,6 +15,7 @@ package cli import ( "fmt" + "github.com/nats-io/natscli/internal/util" "sort" "strings" "time" @@ -122,7 +123,7 @@ func (c *StreamCheckCmd) streamCheck(_ *fisk.ParseContext) error { fmt.Printf("Streams: %d\n", len(keys)) - table := newTableWriter("Streams") + table := util.NewTableWriter(opts(), "Streams") if c.health { table.AddHeaders("Stream Replica", "Raft", "Account", "Account ID", "Node", "Messages", "Bytes", "Subjects", "Deleted", "Consumers", "First", "Last", "Status", "Leader", "Peers", "Health") } else { diff --git a/cli/server_watch_acct_command.go b/cli/server_watch_acct_command.go index 6520c350..afcc8e47 100644 --- a/cli/server_watch_acct_command.go +++ b/cli/server_watch_acct_command.go @@ -184,7 +184,7 @@ func (c *SrvWatchAccountCmd) redraw() error { tc = fmt.Sprintf("%d / %d", c.topCount, len(accounts)) } - table := newTableWriter(fmt.Sprintf("Top %s Account activity by %s at %s", tc, c.sortNames[c.sort], c.lastMsg.Format(time.DateTime))) + table := iu.NewTableWriter(opts(), fmt.Sprintf("Top %s Account activity by %s at %s", tc, c.sortNames[c.sort], c.lastMsg.Format(time.DateTime))) table.AddHeaders("Account", "Servers", "Connections", "Leafnodes", "Subscriptions", "Slow", "Sent", "Received") var matched []*server.AccountStat diff --git a/cli/server_watch_js_command.go b/cli/server_watch_js_command.go index 7cc99784..93f2cf40 100644 --- a/cli/server_watch_js_command.go +++ b/cli/server_watch_js_command.go @@ -212,7 +212,7 @@ func (c *SrvWatchJSCmd) redraw(drawPending bool) error { tc = fmt.Sprintf("%d / %d", c.topCount, len(servers)) } - table := newTableWriter(fmt.Sprintf("Top %s Server activity by %s at %s", tc, c.sortNames[c.sort], c.lastMsg.Format(time.DateTime))) + table := iu.NewTableWriter(opts(), fmt.Sprintf("Top %s Server activity by %s at %s", tc, c.sortNames[c.sort], c.lastMsg.Format(time.DateTime))) if drawPending { table.AddHeaders("Server", "HA Assets", "Memory", "File", "API", "API Errors", "API Pending") diff --git a/cli/server_watch_srv_command.go b/cli/server_watch_srv_command.go index 357a61ee..4b7a2e23 100644 --- a/cli/server_watch_srv_command.go +++ b/cli/server_watch_srv_command.go @@ -228,7 +228,7 @@ func (c *SrvWatchServerCmd) redraw() error { c.lastMsg = time.Now() } - table := newTableWriter(fmt.Sprintf("Top %s Server activity by %s at %s", tc, c.sortNames[c.sort], c.lastMsg.Format(time.DateTime))) + table := iu.NewTableWriter(opts(), fmt.Sprintf("Top %s Server activity by %s at %s", tc, c.sortNames[c.sort], c.lastMsg.Format(time.DateTime))) table.AddHeaders("Server", "Connections", "Subscription", "Slow", "Memory", "CPU", "Cores", "Routes", "Gateways", "Sent", "Received") var matched []*server.ServerStatsMsg diff --git a/cli/service_command.go b/cli/service_command.go index 83bec8bc..d45978df 100644 --- a/cli/service_command.go +++ b/cli/service_command.go @@ -321,7 +321,7 @@ func (c *serviceCmd) statsAction(_ *fisk.ParseContext) error { return nil } - table := newTableWriter(fmt.Sprintf("%s Service Statistics", c.name)) + table := iu.NewTableWriter(opts(), fmt.Sprintf("%s Service Statistics", c.name)) table.AddHeaders("ID", "Endpoint", "Requests", "Queue Group", "Errors", "Processing Time", "Average Time") var requests, errors int @@ -460,11 +460,11 @@ func (c *serviceCmd) listAction(_ *fisk.ParseContext) error { return nil } - var table *tbl + var table *iu.Table if c.name == "" { - table = newTableWriter("All Services") + table = iu.NewTableWriter(opts(), "All Services") } else { - table = newTableWriter(fmt.Sprintf("%s Service Instances", c.name)) + table = iu.NewTableWriter(opts(), fmt.Sprintf("%s Service Instances", c.name)) } table.AddHeaders("Name", "Version", "ID", "Description") var pd, pv, pn string diff --git a/cli/stream_command.go b/cli/stream_command.go index 8569323e..2b142fd2 100644 --- a/cli/stream_command.go +++ b/cli/stream_command.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/jedib0t/go-pretty/v6/progress" "io" "math" "os" @@ -39,7 +40,6 @@ import ( "github.com/dustin/go-humanize" "github.com/emicklei/dot" "github.com/google/go-cmp/cmp" - "github.com/gosuri/uiprogress" "github.com/nats-io/jsm.go" "github.com/nats-io/jsm.go/api" "github.com/nats-io/nats.go" @@ -582,7 +582,9 @@ func (c *streamCmd) detectGaps(_ *fisk.ParseContext) error { c.showProgress = false } - var progress *uiprogress.Bar + var progbar progress.Writer + var tracker *progress.Tracker + var gaps [][2]uint64 var cnt int @@ -590,18 +592,18 @@ func (c *streamCmd) detectGaps(_ *fisk.ParseContext) error { if !c.showProgress { return } - if progress == nil { - progress = uiprogress.AddBar(int(pending)).AppendCompleted().PrependFunc(func(b *uiprogress.Bar) string { - return fmt.Sprintf("%s / %s", f(b.Current()), f(b.Total)) + if tracker == nil { + progbar, tracker, err = iu.NewProgress(opts(), &progress.Tracker{ + Total: int64(pending), }) - uiprogress.Start() } cnt++ - progress.Set(int(seq)) + tracker.SetValue(int64(seq)) if pending == 0 { - progress.Set(progress.Total) + tracker.SetValue(tracker.Total) + tracker.MarkAsDone() } } @@ -610,9 +612,9 @@ func (c *streamCmd) detectGaps(_ *fisk.ParseContext) error { } err = stream.DetectGaps(ctx, progressCb, gapCb) - if progress != nil { + if tracker != nil { time.Sleep(250 * time.Millisecond) // let it draw - uiprogress.Stop() + progbar.Stop() fmt.Println() } if err != nil { @@ -629,11 +631,11 @@ func (c *streamCmd) detectGaps(_ *fisk.ParseContext) error { return nil } - var table *tbl + var table *iu.Table if len(gaps) == 1 { - table = newTableWriter(fmt.Sprintf("1 gap found in Stream %s", c.stream)) + table = iu.NewTableWriter(opts(), fmt.Sprintf("1 gap found in Stream %s", c.stream)) } else { - table = newTableWriter(fmt.Sprintf("%s gaps found in Stream %s", f(len(gaps)), c.stream)) + table = iu.NewTableWriter(opts(), fmt.Sprintf("%s gaps found in Stream %s", f(len(gaps)), c.stream)) } table.AddHeaders("First Message", "Last Message") @@ -683,7 +685,7 @@ func (c *streamCmd) subjectsAction(_ *fisk.ParseContext) (err error) { cols := 1 countWidth := len(f(most)) - table := newTableWriter(fmt.Sprintf("%d Subjects in stream %s", len(names), c.stream)) + table := iu.NewTableWriter(opts(), fmt.Sprintf("%d Subjects in stream %s", len(names), c.stream)) switch { case longest+countWidth < 20: @@ -1087,13 +1089,11 @@ func (c *streamCmd) restoreAction(_ *fisk.ParseContext) error { fisk.Fatalf("Stream %q already exist", bm.Config.Name) } - var progress *uiprogress.Bar - var bps uint64 + var progbar progress.Writer + var tracker *progress.Tracker var prevMsg time.Time cb := func(p jsm.RestoreProgress) { - bps = p.BytesPerSecond() - if opts().Trace && (p.ChunksSent()%100 == 0 || time.Since(prevMsg) > 500*time.Millisecond) { fmt.Printf("Sent %v chunk %v / %v at %v / s\n", fiBytes(uint64(p.ChunkSize())), p.ChunksSent(), p.ChunksToSend(), fiBytes(p.BytesPerSecond())) return @@ -1101,23 +1101,19 @@ func (c *streamCmd) restoreAction(_ *fisk.ParseContext) error { prevMsg = time.Now() - if progress == nil { - progress = uiprogress.AddBar(p.ChunksToSend()).AppendCompleted().PrependFunc(func(b *uiprogress.Bar) string { - return humanize.IBytes(bps) + "/s" + if progbar == nil { + progbar, tracker, _ = iu.NewProgress(opts(), &progress.Tracker{ + Total: int64(p.ChunksToSend() * p.ChunkSize()), + Units: progress.UnitsBytes, }) - progress.Width = iu.ProgressWidth() } - progress.Set(int(p.ChunksSent())) + tracker.SetValue(int64(p.ChunksSent() * uint32(p.ChunkSize()))) } var ropts []jsm.SnapshotOption if c.showProgress { - if !opts().Trace { - uiprogress.Start() - } - ropts = append(ropts, jsm.RestoreNotify(cb)) } else { ropts = append(ropts, jsm.SnapshotDebug()) @@ -1150,15 +1146,18 @@ func (c *streamCmd) restoreAction(_ *fisk.ParseContext) error { cfg.Replicas = int(c.replicas) } - ropts = append(ropts, jsm.RestoreConfiguration(*cfg)) + if cfg != nil { + ropts = append(ropts, jsm.RestoreConfiguration(*cfg)) + } fmt.Printf("Starting restore of Stream %q from file %q\n\n", bm.Config.Name, c.backupDirectory) fp, _, err := mgr.RestoreSnapshotFromDirectory(ctx, bm.Config.Name, c.backupDirectory, ropts...) fisk.FatalIfError(err, "restore failed") if c.showProgress { - progress.Set(int(fp.ChunksSent())) - uiprogress.Stop() + tracker.SetValue(int64(fp.ChunksSent() * uint32(fp.ChunkSize()))) + time.Sleep(300 * time.Millisecond) + progbar.Stop() } fmt.Println() @@ -1175,13 +1174,13 @@ func (c *streamCmd) restoreAction(_ *fisk.ParseContext) error { func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check bool, target string, chunkSize int) error { first := true - inprogress := true pmu := sync.Mutex{} - var bar *uiprogress.Bar - var bps uint64 - var progress *uiprogress.Progress expected := 1 timedOut := false + + var progbar progress.Writer + var tracker *progress.Tracker + var err error var prevMsg time.Time ctx, cancel := context.WithCancel(ctx) @@ -1200,14 +1199,14 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b var received uint32 cb := func(p jsm.SnapshotProgress) { - if bar == nil && showProgress { + if tracker == nil && showProgress { if p.BytesExpected() > 0 { expected = int(p.BytesExpected()) } - bar = progress.AddBar(expected).AppendCompleted().PrependFunc(func(b *uiprogress.Bar) string { - return humanize.IBytes(bps) + "/s" + progbar, tracker, err = iu.NewProgress(opts(), &progress.Tracker{ + Total: int64(expected), + Units: iu.ProgressUnitsIBytes, }) - bar.Width = iu.ProgressWidth() } if first { @@ -1223,8 +1222,6 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b first = false } - bps = p.BytesPerSecond() - if opts().Trace { if first { fmt.Printf("Received %s chunk %s\n", fiBytes(uint64(p.ChunkSize())), f(p.ChunksReceived())) @@ -1238,20 +1235,8 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b received = p.ChunksReceived() } - if showProgress { - bar.Set(int(p.BytesReceived())) - } - - if p.Finished() { - pmu.Lock() - if inprogress { - if showProgress { - progress.Stop() - } - - inprogress = false - } - pmu.Unlock() + if tracker != nil { + tracker.SetValue(int64(p.UncompressedBytesReceived())) } prevMsg = time.Now() @@ -1259,6 +1244,7 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b sopts := []jsm.SnapshotOption{ jsm.SnapshotChunkSize(chunkSize), + jsm.SnapshotNotify(cb), } if consumers { @@ -1270,13 +1256,6 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b showProgress = false } - if showProgress { - progress = uiprogress.New() - progress.Start() - } - - sopts = append(sopts, jsm.SnapshotNotify(cb)) - if check { sopts = append(sopts, jsm.SnapshotHealthCheck()) } @@ -1287,10 +1266,11 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b } pmu.Lock() - if showProgress && inprogress { - bar.Set(int(fp.BytesReceived())) - uiprogress.Stop() - inprogress = false + if tracker != nil { + tracker.SetValue(int64(expected)) + tracker.MarkAsDone() + time.Sleep(300 * time.Millisecond) + progbar.Stop() } pmu.Unlock() @@ -1300,7 +1280,7 @@ func backupStream(stream *jsm.Stream, showProgress bool, consumers bool, check b return fmt.Errorf("backup timed out after receiving no data for a long period") } - fmt.Printf("Received %s compressed data in %s chunks for stream %q in %v, %s uncompressed \n", humanize.IBytes(fp.BytesReceived()), f(fp.ChunksReceived()), stream.Name(), fp.EndTime().Sub(fp.StartTime()).Round(time.Millisecond), humanize.IBytes(fp.UncompressedBytesReceived())) + fmt.Printf("Received %s compressed data in %s chunks for stream %q in %v, %s uncompressed \n", humanize.IBytes(fp.BytesReceived()), f(fp.ChunksReceived()), stream.Name(), fp.EndTime().Sub(fp.StartTime()).Round(time.Millisecond), fiBytes(fp.UncompressedBytesReceived())) return nil } @@ -1476,8 +1456,8 @@ func (c *streamCmd) reportAction(_ *fisk.ParseContext) error { } func (c *streamCmd) renderReplication(stats []streamStat) { - table := newTableWriter("Replication Report") - table.AddHeaders("Stream", "Kind", "API Prefix", "Source Stream", "Filters and Transforms", "Seen", "Lag", "Error") + table := iu.NewTableWriter(opts(), "Replication Report") + table.AddHeaders("Stream", "Kind", "API Prefix", "Source Stream", "Filters and Transforms", "Active", "Lag", "Error") for _, s := range stats { if len(s.Sources) == 0 && s.Mirror == nil { @@ -1535,7 +1515,7 @@ func (c *streamCmd) renderReplication(stats []streamStat) { } func (c *streamCmd) renderStreams(stats []streamStat) { - table := newTableWriter("Stream Report") + table := iu.NewTableWriter(opts(), "Stream Report") table.AddHeaders("Stream", "Storage", "Placement", "Consumers", "Messages", "Bytes", "Lost", "Deleted", "Replicas") for _, s := range stats { @@ -3150,11 +3130,11 @@ func (c *streamCmd) renderStreamsAsTable(streams []*jsm.Stream, missing []string }) var out bytes.Buffer - var table *tbl + var table *iu.Table if c.filterSubject == "" { - table = newTableWriter("Streams") + table = iu.NewTableWriter(opts(), "Streams") } else { - table = newTableWriter(fmt.Sprintf("Streams matching %s", c.filterSubject)) + table = iu.NewTableWriter(opts(), fmt.Sprintf("Streams matching %s", c.filterSubject)) } table.AddHeaders("Name", "Description", "Created", "Messages", "Size", "Last Message") @@ -3181,7 +3161,7 @@ func (c *streamCmd) renderMissing(out io.Writer, missing []string) { if len(missing) > 0 { fmt.Fprintln(out) sort.Strings(missing) - table := newTableWriter("Inaccessible Streams") + table := iu.NewTableWriter(opts(), "Inaccessible Streams") iu.SliceGroups(missing, 4, func(names []string) { table.AddRow(toany(names)...) }) diff --git a/cli/sub_command.go b/cli/sub_command.go index cd070581..a45f72c5 100644 --- a/cli/sub_command.go +++ b/cli/sub_command.go @@ -258,7 +258,7 @@ func (c *subCmd) startSubjectReporting(ctx context.Context, subjMu *sync.Mutex, } else { tableHeaderString = fmt.Sprintf("Top %d Active Subjects Report", subjCount) } - table := newTableWriter(tableHeaderString) + table := iu.NewTableWriter(opts(), tableHeaderString) table.AddHeaders("Subject", "Message Count", "Bytes") table.AddFooter("Totals", f(totalCount), humanize.IBytes(uint64(totalBytes))) for i := range subjectRows { diff --git a/cli/traffic_command.go b/cli/traffic_command.go index eed4439b..c676c282 100644 --- a/cli/traffic_command.go +++ b/cli/traffic_command.go @@ -15,6 +15,7 @@ package cli import ( "fmt" + "github.com/nats-io/natscli/internal/util" "runtime" "strings" "sync" @@ -179,7 +180,7 @@ func (c *trafficCmd) monitor(_ *fisk.ParseContext) error { } raftRows = append(raftRows, []any{c.raftVote.Comma(), c.raftAppend.Comma(), c.raftReply.Comma(), c.raftRemovePeer.Comma(), c.raftProp.Comma(), c.raftC.Comma()}) - table := newTableWriter("Raft Traffic") + table := util.NewTableWriter(opts(), "Raft Traffic") table.AddHeaders("Vote", "Append", "Reply", "Remove Peer", "Proposal", "Total Messages") for i := range raftRows { table.AddRow(raftRows[i]...) @@ -193,7 +194,7 @@ func (c *trafficCmd) monitor(_ *fisk.ParseContext) error { } clusterRows = append(clusterRows, []any{c.clusterJSAUpdate.Comma(), c.clusterStreamInfo.Comma(), c.clusterConsumerInfo.Comma(), c.clusterStreamSync.Comma(), c.clusterReply.Comma(), c.clusterC.Comma()}) - table := newTableWriter("Cluster Traffic") + table := util.NewTableWriter(opts(), "Cluster Traffic") table.AddHeaders("JSA Update", "Stream Info", "Consumer Info", "Stream Sync", "Reply", "Total Messages") for i := range clusterRows { table.AddRow(clusterRows[i]...) @@ -206,7 +207,7 @@ func (c *trafficCmd) monitor(_ *fisk.ParseContext) error { } genRows = append(genRows, []any{c.requests.Comma(), c.jsAPI.Comma(), c.jsAck.Comma(), c.systemMsg.Comma(), c.msgs.Comma(), c.size.IBytes(), c.genC.Comma()}) - table := newTableWriter("General Traffic") + table := util.NewTableWriter(opts(), "General Traffic") table.AddHeaders("Requests", "JS API", "JS ACK", "System", "Rest", "Total Bytes", "Total Messages") for i := range genRows { table.AddRow(genRows[i]...) diff --git a/cli/util.go b/cli/util.go index 0817c852..671c2e43 100644 --- a/cli/util.go +++ b/cli/util.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jedib0t/go-pretty/v6/progress" "io" "math" "math/rand" @@ -44,11 +45,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/choria-io/fisk" "github.com/google/shlex" - "github.com/gosuri/uiprogress" - "github.com/jedib0t/go-pretty/v6/table" - "github.com/jedib0t/go-pretty/v6/text" "github.com/klauspost/compress/s2" - "github.com/mattn/go-isatty" "github.com/nats-io/jsm.go" "github.com/nats-io/jsm.go/api" "github.com/nats-io/jsm.go/natscontext" @@ -945,7 +942,7 @@ type raftLeader struct { } func renderRaftLeaders(leaders map[string]*raftLeader, grpTitle string) { - table := newTableWriter("RAFT Leader Report") + table := iu.NewTableWriter(opts(), "RAFT Leader Report") table.AddHeaders("Server", "Cluster", grpTitle, "Distribution") var llist []*raftLeader @@ -1032,33 +1029,6 @@ func compactStrings(source []string) []string { return result } -func newTableWriter(format string, a ...any) *tbl { - tbl := &tbl{ - writer: table.NewWriter(), - } - - tbl.writer.SetStyle(styles["rounded"]) - - if isatty.IsTerminal(os.Stdout.Fd()) { - if opts().Config != nil { - style, ok := styles[opts().Config.ColorScheme()] - if ok { - tbl.writer.SetStyle(style) - } - } - } - - tbl.writer.Style().Title.Align = text.AlignCenter - tbl.writer.Style().Format.Header = text.FormatDefault - tbl.writer.Style().Format.Footer = text.FormatDefault - - if format != "" { - tbl.writer.SetTitle(fmt.Sprintf(format, a...)) - } - - return tbl -} - func isPrintable(s string) bool { for _, r := range s { if r > unicode.MaxASCII || !unicode.IsPrint(r) { @@ -1080,19 +1050,20 @@ func base64IfNotPrintable(val []byte) string { type progressRW struct { r io.Reader w io.Writer - p *uiprogress.Bar + p progress.Writer + t *progress.Tracker } func (pr *progressRW) Read(p []byte) (n int, err error) { n, err = pr.r.Read(p) - pr.p.Set(pr.p.Current() + n) + pr.t.Increment(int64(n)) return n, err } func (pr *progressRW) Write(p []byte) (n int, err error) { n, err = pr.w.Write(p) - pr.p.Set(pr.p.Current() + n) + pr.t.Increment(int64(n)) return n, err } diff --git a/go.mod b/go.mod index cee9cbfd..e54825bc 100644 --- a/go.mod +++ b/go.mod @@ -8,20 +8,20 @@ require ( github.com/choria-io/fisk v0.6.4 github.com/choria-io/scaffold v0.0.2 github.com/dustin/go-humanize v1.0.1 - github.com/emicklei/dot v1.6.2 + github.com/emicklei/dot v1.6.3 github.com/expr-lang/expr v1.16.9 github.com/fatih/color v1.18.0 github.com/ghodss/yaml v1.0.0 github.com/google/go-cmp v0.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gosuri/uiprogress v0.0.1 - github.com/jedib0t/go-pretty/v6 v6.6.1 + github.com/jedib0t/go-pretty/v6 v6.6.2 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.17.11 github.com/mattn/go-isatty v0.0.20 - github.com/nats-io/jsm.go v0.1.1-0.20241119145046-e1d638961b90 + github.com/nats-io/jsm.go v0.1.1-0.20241126113501-b6095211b00b github.com/nats-io/jwt/v2 v2.7.2 - github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20241119025009-2dadd723643c + github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20241126010352-fa50c751ab3c github.com/nats-io/nats.go v1.37.0 github.com/nats-io/nkeys v0.4.7 github.com/nats-io/nuid v1.0.1 @@ -30,18 +30,19 @@ require ( github.com/synadia-io/jwt-auth-builder.go v0.0.0-20240829124321-43722a8ce3ce github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f golang.org/x/crypto v0.29.0 - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f golang.org/x/term v0.26.0 gopkg.in/gizak/termui.v1 v1.0.0-20151021151108-e62b5929642a gopkg.in/yaml.v3 v3.0.1 ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/google/go-tpm v0.9.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gosuri/uilive v0.0.4 // indirect @@ -54,7 +55,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nats-io/nsc/v2 v2.8.6 // indirect + github.com/nats-io/nsc/v2 v2.8.6-0.20231220104935-3f89317df670 // indirect github.com/nsf/termbox-go v1.1.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.1 // indirect @@ -66,6 +67,6 @@ require ( golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.8.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0e7c9c3a..b5f36302 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= @@ -9,8 +9,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -30,8 +30,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= -github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/emicklei/dot v1.6.3 h1:MW9eLeJWaN+QZVSPlrargGd/l92IA6d4fmo39/YD2cQ= +github.com/emicklei/dot v1.6.3/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -46,6 +46,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -66,8 +68,8 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= -github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM= +github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= @@ -104,18 +106,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nats-io/jsm.go v0.1.1-0.20241119145046-e1d638961b90 h1:TWc4Yc/ZpS+LK9LTU1c24xRRrUyte6WSSynJZdqplfE= -github.com/nats-io/jsm.go v0.1.1-0.20241119145046-e1d638961b90/go.mod h1:XYx1eATSGIODQY8jhQQBPnB1w1DcqYO7gxb/kLC5mqk= +github.com/nats-io/jsm.go v0.1.1-0.20241126113501-b6095211b00b h1:/cZAXRQjncFpqWWE5uCn1FN8eGu60F6HeYj+zTfX1l8= +github.com/nats-io/jsm.go v0.1.1-0.20241126113501-b6095211b00b/go.mod h1:u1bYBgRIiahmheRyOc+bLpZUkbMTkfmdsaPhs2FzS9c= github.com/nats-io/jwt/v2 v2.7.2 h1:SCRjfDLJ2q8naXp8YlGJJS5/yj3wGSODFYVi4nnwVMw= github.com/nats-io/jwt/v2 v2.7.2/go.mod h1:kB6QUmqHG6Wdrzj0KP2L+OX4xiTPBeV+NHVstFaATXU= -github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20241119025009-2dadd723643c h1:lcnWooJpdDKowjjijNW5LCTGRjInR4rpAecFVXxoOZQ= -github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20241119025009-2dadd723643c/go.mod h1:UMry3yQXAiKBN3yh82BS4HxJWF9ht4sbRQyZ1qXODqc= +github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20241126010352-fa50c751ab3c h1:Lq24aOAbP9Pr4E5Sqvghr6NNxV+FUGMM3ojHFzno8Js= +github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20241126010352-fa50c751ab3c/go.mod h1:nI8h87Ryi/zcaQcDLSSVbMfomhyck+0oDqxR7NoZX0Y= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= -github.com/nats-io/nsc/v2 v2.8.6 h1:ytf5F2mb+BXx8DjXImPyqOZrFFozt9umQGuQ+6m9eXs= -github.com/nats-io/nsc/v2 v2.8.6/go.mod h1:jHj6s7VspjVwl0NRoWjVN+gqVvhA+NdkTpAk/WZg5yk= +github.com/nats-io/nsc/v2 v2.8.6-0.20231220104935-3f89317df670 h1:NQzs7g/+Z4kC4XsYsKCQlwRcM4Hk0VyKuz7F4zUgjvQ= +github.com/nats-io/nsc/v2 v2.8.6-0.20231220104935-3f89317df670/go.mod h1:Z2+aDD1PzpXk8kF1ro17cfGBzmBoWPbtvGW8hBssAdA= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -166,8 +168,8 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -213,8 +215,8 @@ golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -223,8 +225,8 @@ gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/util/progress.go b/internal/util/progress.go new file mode 100644 index 00000000..14b2e330 --- /dev/null +++ b/internal/util/progress.go @@ -0,0 +1,58 @@ +// Copyright 2024 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "github.com/dustin/go-humanize" + "github.com/jedib0t/go-pretty/v6/progress" + "github.com/jedib0t/go-pretty/v6/text" + "github.com/nats-io/natscli/options" + "time" +) + +var ProgressUnitsIBytes = progress.Units{ + Notation: "", + NotationPosition: progress.UnitsNotationPositionBefore, + Formatter: func(v int64) string { return humanize.IBytes(uint64(v)) }, +} + +func NewProgress(opts *options.Options, tracker *progress.Tracker) (progress.Writer, *progress.Tracker, error) { + progbar := progress.NewWriter() + natsStyle := progress.StyleBlocks + natsStyle.Visibility.ETA = false + natsStyle.Visibility.Speed = true + natsStyle.Colors.Tracker = contextColor(opts) + natsStyle.Options.Separator = " " + natsStyle.Options.TimeInProgressPrecision = time.Millisecond + progbar.SetStyle(natsStyle) + progbar.SetAutoStop(true) + pw := ProgressWidth() + progbar.SetTrackerLength(pw / 2) + progbar.SetNumTrackersExpected(1) + progbar.SetUpdateFrequency(250 * time.Millisecond) + + progbar.AppendTracker(tracker) + go progbar.Render() + + return progbar, tracker, nil +} + +func contextColor(opts *options.Options) text.Colors { + cs := opts.Config.ColorScheme() + s, ok := styles[cs] + if !ok { + return text.Colors{text.FgWhite} + } + return s.Color.Border +} diff --git a/cli/tables.go b/internal/util/tables.go similarity index 64% rename from cli/tables.go rename to internal/util/tables.go index e3aaa500..b8eea592 100644 --- a/cli/tables.go +++ b/internal/util/tables.go @@ -1,4 +1,4 @@ -// Copyright 2023 The NATS Authors +// Copyright 2024 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -11,17 +11,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cli +package util import ( "fmt" - "sort" - "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" + "github.com/mattn/go-isatty" + "github.com/nats-io/natscli/options" + "os" + "sort" ) -type tbl struct { +type Table struct { writer table.Writer } @@ -63,22 +65,49 @@ func ValidStyles() []string { return res } -func (t *tbl) AddHeaders(items ...any) { +func (t *Table) AddHeaders(items ...any) { t.writer.AppendHeader(items) } -func (t *tbl) AddFooter(items ...any) { +func (t *Table) AddFooter(items ...any) { t.writer.AppendFooter(items) } -func (t *tbl) AddSeparator() { +func (t *Table) AddSeparator() { t.writer.AppendSeparator() } -func (t *tbl) AddRow(items ...any) { +func (t *Table) AddRow(items ...any) { t.writer.AppendRow(items) } -func (t *tbl) Render() string { +func (t *Table) Render() string { return fmt.Sprintln(t.writer.Render()) } + +func NewTableWriter(opts *options.Options, format string, a ...any) *Table { + tbl := &Table{ + writer: table.NewWriter(), + } + + tbl.writer.SetStyle(styles["rounded"]) + + if isatty.IsTerminal(os.Stdout.Fd()) { + if opts.Config != nil { + style, ok := styles[opts.Config.ColorScheme()] + if ok { + tbl.writer.SetStyle(style) + } + } + } + + tbl.writer.Style().Title.Align = text.AlignCenter + tbl.writer.Style().Format.Header = text.FormatDefault + tbl.writer.Style().Format.Footer = text.FormatDefault + + if format != "" { + tbl.writer.SetTitle(fmt.Sprintf(format, a...)) + } + + return tbl +} diff --git a/nats/main.go b/nats/main.go index f961702f..ad61306b 100644 --- a/nats/main.go +++ b/nats/main.go @@ -14,6 +14,7 @@ package main import ( + iu "github.com/nats-io/natscli/internal/util" "log" "os" "runtime" @@ -70,7 +71,7 @@ See 'nats cheat' for a quick cheatsheet of commands` ncli.Flag("js-domain", "JetStream domain to access").PlaceHolder("DOMAIN").StringVar(&opts.JsDomain) ncli.Flag("inbox-prefix", "Custom inbox prefix to use for inboxes").PlaceHolder("PREFIX").StringVar(&opts.InboxPrefix) ncli.Flag("domain", "JetStream domain to access").PlaceHolder("DOMAIN").Hidden().StringVar(&opts.JsDomain) - ncli.Flag("colors", "Sets a color scheme to use").PlaceHolder("SCHEME").Envar("NATS_COLOR").EnumVar(&opts.ColorScheme, cli.ValidStyles()...) + ncli.Flag("colors", "Sets a color scheme to use").PlaceHolder("SCHEME").Envar("NATS_COLOR").EnumVar(&opts.ColorScheme, iu.ValidStyles()...) ncli.Flag("context", "Configuration context").Envar("NATS_CONTEXT").PlaceHolder("NAME").StringVar(&opts.CfgCtx) ncli.Flag("trace", "Trace API interactions").UnNegatableBoolVar(&opts.Trace) ncli.Flag("no-context", "Disable the selected context").UnNegatableBoolVar(&cli.SkipContexts)