From 530a84a10cd3fbd358bad697cdf02b5e2e3865a9 Mon Sep 17 00:00:00 2001 From: Jonathan Conder Date: Thu, 9 Jul 2026 15:50:41 +1200 Subject: [PATCH 1/2] WIP: switch to ansiterm/tabwriter --- cmd/internal/cmdutil/util_test.go | 2 +- cmd/sdk/find.go | 2 +- cmd/sdk/info.go | 2 +- cmd/sdk/list.go | 2 +- cmd/workshop/info.go | 2 +- cmd/workshop/list.go | 2 +- docs/coding-style-guide.md | 2 +- go.mod | 6 ++++++ go.sum | 15 +++++++++++++++ 9 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cmd/internal/cmdutil/util_test.go b/cmd/internal/cmdutil/util_test.go index a7d938764..f540f6cb4 100644 --- a/cmd/internal/cmdutil/util_test.go +++ b/cmd/internal/cmdutil/util_test.go @@ -22,9 +22,9 @@ import ( "reflect" "strings" "testing" - "text/tabwriter" "unicode/utf8" + "github.com/juju/ansiterm/tabwriter" "gopkg.in/check.v1" "gopkg.in/yaml.v3" ) diff --git a/cmd/sdk/find.go b/cmd/sdk/find.go index d6fe76293..e87d8d581 100644 --- a/cmd/sdk/find.go +++ b/cmd/sdk/find.go @@ -19,8 +19,8 @@ import ( "fmt" "slices" "strings" - "text/tabwriter" + "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "github.com/canonical/workshop/client" diff --git a/cmd/sdk/info.go b/cmd/sdk/info.go index c3d35d216..3b08aa955 100644 --- a/cmd/sdk/info.go +++ b/cmd/sdk/info.go @@ -20,10 +20,10 @@ import ( "net/url" "slices" "strings" - "text/tabwriter" "time" "github.com/canonical/lxd/shared/units" + "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "github.com/canonical/workshop/client" diff --git a/cmd/sdk/list.go b/cmd/sdk/list.go index 134739dd5..723950266 100644 --- a/cmd/sdk/list.go +++ b/cmd/sdk/list.go @@ -18,9 +18,9 @@ import ( "cmp" "fmt" "slices" - "text/tabwriter" "github.com/canonical/lxd/shared/units" + "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "github.com/canonical/workshop/client" diff --git a/cmd/workshop/info.go b/cmd/workshop/info.go index bfab11ea3..888d1f8b8 100644 --- a/cmd/workshop/info.go +++ b/cmd/workshop/info.go @@ -24,9 +24,9 @@ import ( "slices" "strconv" "strings" - "text/tabwriter" "time" + "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "github.com/canonical/workshop/client" diff --git a/cmd/workshop/list.go b/cmd/workshop/list.go index 3bd5445fd..fe1dbc821 100644 --- a/cmd/workshop/list.go +++ b/cmd/workshop/list.go @@ -19,8 +19,8 @@ import ( "fmt" "slices" "strings" - "text/tabwriter" + "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "github.com/canonical/workshop/client" diff --git a/docs/coding-style-guide.md b/docs/coding-style-guide.md index 0756e2a65..15466bd27 100644 --- a/docs/coding-style-guide.md +++ b/docs/coding-style-guide.md @@ -801,7 +801,7 @@ Long: `This command will launch a new workshop. It will create the workshop base ```go // Good: Use tabwriter for consistent table formatting -import "text/tabwriter" +import "github.com/juju/ansiterm/tabwriter" func tabWriter() *tabwriter.Writer { return tabwriter.NewWriter(Stdout, 4, 3, 2, ' ', tabwriter.StripEscape) diff --git a/go.mod b/go.mod index e2ba555b5..03bf18b85 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 github.com/jessevdk/go-flags v1.6.1 + github.com/juju/ansiterm v1.0.0 github.com/juju/clock v1.1.1 github.com/juju/retry v1.0.1 github.com/pkg/sftp v1.13.10 @@ -34,6 +35,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -46,6 +48,8 @@ require ( github.com/kr/fs v0.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lunixbochs/vtclean v1.0.0 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/pkg/xattr v0.4.12 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -64,3 +68,5 @@ require ( ) tool github.com/canonical/workshop/tools/try + +replace github.com/juju/ansiterm => github.com/jonathan-conder/ansiterm v0.0.0-20260709033836-7b24c91012ac diff --git a/go.sum b/go.sum index a8a10e539..04845122c 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,9 @@ github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVE github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -54,6 +57,8 @@ github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4w github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/jonathan-conder/ansiterm v0.0.0-20260709033836-7b24c91012ac h1:1YpK/tD7/L/3EwYLeOhD2t+k4IlDmqjrilOTs8KEwm0= +github.com/jonathan-conder/ansiterm v0.0.0-20260709033836-7b24c91012ac/go.mod h1:eU3/6wzyK7n7padpmWMNAKlw7C18Buln+ofDLVPgSMM= github.com/juju/clock v1.1.1 h1:NvgHG9DQmOpBevgt6gzkyimdWBooLXDy1cQn89qJzBI= github.com/juju/clock v1.1.1/go.mod h1:HIBvJ8kiV/n7UHwKuCkdYL4l/MDECztHR2sAvWDxxf0= github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a h1:d7eZO8OS/ZXxdP0uq3E8CdoA1qNFaecAv90UxrxaY2k= @@ -87,6 +92,13 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= @@ -157,8 +169,11 @@ golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= From 1149b9dd153396f5c022a2b62ce26374a93f7d9f Mon Sep 17 00:00:00 2001 From: Jonathan Conder Date: Thu, 9 Jul 2026 16:22:35 +1200 Subject: [PATCH 2/2] Embolden table headings --- cmd/sdk/find.go | 2 +- cmd/sdk/info.go | 14 +++++++------- cmd/sdk/list.go | 4 +++- cmd/workshop/changes.go | 5 ++++- cmd/workshop/connections.go | 4 +++- cmd/workshop/list.go | 7 +++++-- cmd/workshop/sketch.go | 4 +++- cmd/workshop/tasks.go | 5 ++++- 8 files changed, 30 insertions(+), 15 deletions(-) diff --git a/cmd/sdk/find.go b/cmd/sdk/find.go index e87d8d581..606e3a51a 100644 --- a/cmd/sdk/find.go +++ b/cmd/sdk/find.go @@ -92,7 +92,7 @@ func (c *CmdFind) Run(cmd *cobra.Command, av []string) error { esc := c.GetEscapes() w := tabwriter.NewWriter(Stdout, 4, 3, 2, ' ', tabwriter.StripEscape) if !c.noHeaders { - fmt.Fprintln(w, "NAME\tVERSION\tPUBLISHER\tSUMMARY") + fmt.Fprintf(w, "%sNAME\tVERSION\tPUBLISHER\tSUMMARY%s\n", esc.Bold, esc.End) } for _, sdk := range sdks { version := cmdutil.EmptyDash(sdk.Version) diff --git a/cmd/sdk/info.go b/cmd/sdk/info.go index 3b08aa955..53e450074 100644 --- a/cmd/sdk/info.go +++ b/cmd/sdk/info.go @@ -146,11 +146,11 @@ func (c *CmdInfo) Run(cmd *cobra.Command, av []string) error { if c.Arch != "all" { archTpl = "%.0s" } - tpl := " %s\t%s\t%s\t" + baseTpl + archTpl + "%*s\t%*s\n" + tpl := " %s%s\t%s\t%s\t" + baseTpl + archTpl + "%*s\t%*s%s\n" if len(tracks) > 0 { fmt.Fprintln(w) fmt.Fprintf(w, "%s%s%s\n", esc.Bold, "CHANNELS", esc.End) - fmt.Fprintf(w, tpl, "CHANNEL", "VERSION", "BUILD", "BASE", "ARCH", maxRev, "REV", maxSize, "SIZE") + fmt.Fprintf(w, tpl, esc.Bold, "CHANNEL", "VERSION", "BUILD", "BASE", "ARCH", maxRev, "REV", maxSize, "SIZE", esc.End) } for _, track := range tracks { closedSign := "-" @@ -158,7 +158,7 @@ func (c *CmdInfo) Run(cmd *cobra.Command, av []string) error { channel := track + "/" + risk revs := revsByChannel[channel] if len(revs) == 0 { - fmt.Fprintf(w, tpl, channel, closedSign, "", "", "", 0, "", 0, "") + fmt.Fprintf(w, tpl, "", channel, closedSign, "", "", "", 0, "", 0, "", "") continue } closedSign = esc.UpArrow @@ -169,7 +169,7 @@ func (c *CmdInfo) Run(cmd *cobra.Command, av []string) error { prev = rev size := units.GetByteSizeString(rev.DownloadSize, 2) - fmt.Fprintf(w, tpl, channel, version, build, base, rev.Arch, maxRev, rev.Revision, maxSize, size) + fmt.Fprintf(w, tpl, "", channel, version, build, base, rev.Arch, maxRev, rev.Revision, maxSize, size, "") } } } @@ -178,11 +178,11 @@ func (c *CmdInfo) Run(cmd *cobra.Command, av []string) error { for _, it := range installed { maxRev = max(maxRev, len(it.Revision)) } - tpl = " %s\t%s\t%s\t%s\t" + baseTpl + archTpl + "%*s\n" + tpl = " %s%s\t%s\t%s\t%s\t" + baseTpl + archTpl + "%*s%s\n" if len(installed) > 0 { fmt.Fprintln(w) fmt.Fprintf(w, "%s%s%s\n", esc.Bold, "INSTALLED", esc.End) - fmt.Fprintf(w, tpl, "PROJECT", "WORKSHOP", "CHANNEL", "VERSION", "BASE", "ARCH", maxRev, "REV") + fmt.Fprintf(w, tpl, esc.Bold, "PROJECT", "WORKSHOP", "CHANNEL", "VERSION", "BASE", "ARCH", maxRev, "REV", esc.End) } for _, it := range installed { project := cmdutil.ContractHome(it.ProjectPath) @@ -191,7 +191,7 @@ func (c *CmdInfo) Run(cmd *cobra.Command, av []string) error { if base == "" { base = "all" } - fmt.Fprintf(w, tpl, project, it.Workshop, channel, it.Version, base, it.Arch, maxRev, it.Revision) + fmt.Fprintf(w, tpl, "", project, it.Workshop, channel, it.Version, base, it.Arch, maxRev, it.Revision, "") } w.Flush() diff --git a/cmd/sdk/list.go b/cmd/sdk/list.go index 723950266..033aa8276 100644 --- a/cmd/sdk/list.go +++ b/cmd/sdk/list.go @@ -29,6 +29,7 @@ import ( ) type CmdList struct { + cmdutil.ColorMixin root *CmdRoot noHeaders bool } @@ -105,7 +106,8 @@ func (c *CmdList) Run(cmd *cobra.Command, _ []string) error { maxSize = max(maxSize, len(units.GetByteSizeString(sdk.Size, 2))) } if !c.noHeaders { - fmt.Fprintf(w, "NAME\tVERSION\t%*s\t%*s\n", maxRev, "REV", maxSize, "SIZE") + esc := c.GetEscapes() + fmt.Fprintf(w, "%sNAME\tVERSION\t%*s\t%*s%s\n", esc.Bold, maxRev, "REV", maxSize, "SIZE", esc.End) } for _, sdk := range sdks { version := cmdutil.EmptyDash(sdk.Version) diff --git a/cmd/workshop/changes.go b/cmd/workshop/changes.go index b3dbc9060..76beab698 100644 --- a/cmd/workshop/changes.go +++ b/cmd/workshop/changes.go @@ -21,10 +21,12 @@ import ( "github.com/spf13/cobra" "github.com/canonical/workshop/client" + "github.com/canonical/workshop/cmd/internal/cmdutil" "github.com/canonical/workshop/internal/timeutil" ) type CmdChanges struct { + cmdutil.ColorMixin root *CmdRoot noHeaders bool } @@ -89,7 +91,8 @@ func (c *CmdChanges) Run(cmd *cobra.Command, av []string) error { w := tabWriter() if !c.noHeaders { - fmt.Fprintf(w, "ID\tSTATUS\tSPAWN\tREADY\tSUMMARY\n") + esc := c.GetEscapes() + fmt.Fprintf(w, "%sID\tSTATUS\tSPAWN\tREADY\tSUMMARY%s\n", esc.Bold, esc.End) } for _, chg := range chngs { spawnTime := timeutil.Human(chg.SpawnTime) diff --git a/cmd/workshop/connections.go b/cmd/workshop/connections.go index d1641b291..7939aac0a 100644 --- a/cmd/workshop/connections.go +++ b/cmd/workshop/connections.go @@ -29,6 +29,7 @@ import ( ) type CmdConnections struct { + cmdutil.ColorMixin root *CmdRoot all bool noHeaders bool @@ -220,7 +221,8 @@ func (c *CmdConnections) Run(cmd *cobra.Command, av []string) error { w := tabWriter() if !c.noHeaders { - fmt.Fprintln(w, i18n.G("INTERFACE\tPLUG\tSLOT\tNOTES")) + esc := c.GetEscapes() + fmt.Fprintln(w, esc.Bold+i18n.G("INTERFACE\tPLUG\tSLOT\tNOTES")+esc.End) } for _, note := range annotatedConns { // find the plug that the current connection is bound to diff --git a/cmd/workshop/list.go b/cmd/workshop/list.go index fe1dbc821..173758a1f 100644 --- a/cmd/workshop/list.go +++ b/cmd/workshop/list.go @@ -28,6 +28,7 @@ import ( ) type CmdList struct { + cmdutil.ColorMixin root *CmdRoot global bool noHeaders bool @@ -112,7 +113,8 @@ func (c *CmdList) listProject() error { w := tabWriter() if !c.noHeaders { - fmt.Fprintf(w, "WORKSHOP\tSTATUS\tNOTES\n") + esc := c.GetEscapes() + fmt.Fprintf(w, "%sWORKSHOP\tSTATUS\tNOTES%s\n", esc.Bold, esc.End) } for _, wp := range merged { fmt.Fprintf(w, "%s\t%s\t%s\n", wp.name, wp.status, wp.notes) @@ -151,7 +153,8 @@ func (c *CmdList) listGlobal() error { } if printHeaders { - fmt.Fprintf(w, "PROJECT\tWORKSHOP\tSTATUS\tNOTES\n") + esc := c.GetEscapes() + fmt.Fprintf(w, "%sPROJECT\tWORKSHOP\tSTATUS\tNOTES%s\n", esc.Bold, esc.End) printHeaders = false } project := cmdutil.ContractHome(p.Path) diff --git a/cmd/workshop/sketch.go b/cmd/workshop/sketch.go index dbb0e1dd4..169e52fa9 100644 --- a/cmd/workshop/sketch.go +++ b/cmd/workshop/sketch.go @@ -630,6 +630,7 @@ func writeHooks(sdkdir string, file sdk.SketchSDKYaml) error { } type CmdSketches struct { + cmdutil.ColorMixin root *CmdRoot noHeaders bool } @@ -702,7 +703,8 @@ func (c *CmdSketches) Run(cmd *cobra.Command, _ []string) error { maxRev = max(maxRev, len(entry.rev)) } if !c.noHeaders && len(entries) > 0 { - fmt.Fprintf(w, "WORKSHOP\t%*s\tNOTES\n", maxRev, "REV") + esc := c.GetEscapes() + fmt.Fprintf(w, "%sWORKSHOP\t%*s\tNOTES%s\n", esc.Bold, maxRev, "REV", esc.End) } for _, entry := range entries { fmt.Fprintf(w, "%s\t%*s\t%s\n", entry.workshop, maxRev, entry.rev, entry.notes) diff --git a/cmd/workshop/tasks.go b/cmd/workshop/tasks.go index fd3b5d058..0cc8ecc9c 100644 --- a/cmd/workshop/tasks.go +++ b/cmd/workshop/tasks.go @@ -24,9 +24,11 @@ import ( "github.com/spf13/cobra" "github.com/canonical/workshop/client" + "github.com/canonical/workshop/cmd/internal/cmdutil" ) type CmdTasks struct { + cmdutil.ColorMixin root *CmdRoot noHeaders bool } @@ -126,7 +128,8 @@ func (c *CmdTasks) Run(cmd *cobra.Command, av []string) error { } w := tabWriter() if !c.noHeaders { - fmt.Fprintf(w, "STATUS\t%*s\tSUMMARY\n", maxDur, "DURATION") + esc := c.GetEscapes() + fmt.Fprintf(w, "%sSTATUS\t%*s\tSUMMARY%s\n", esc.Bold, maxDur, "DURATION", esc.End) } for _, tsk := range tasks { duration := tsk.DoingTime.Round(time.Millisecond).String()