Merge pull request #888 from opencloud-eu/dependabot/go_modules/github.com/olekukonko/tablewriter-1.0.6

build(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.6
This commit is contained in:
Ralf Haferkamp
2025-05-20 09:31:57 +02:00
committed by GitHub
102 changed files with 23008 additions and 1902 deletions

4
go.mod
View File

@@ -58,7 +58,7 @@ require (
github.com/nats-io/nats-server/v2 v2.11.3
github.com/nats-io/nats.go v1.42.0
github.com/oklog/run v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/olekukonko/tablewriter v1.0.6
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
@@ -266,6 +266,8 @@ require (
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pablodz/inotifywaitgo v0.0.9 // indirect

7
go.sum
View File

@@ -842,8 +842,13 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 h1:V2wKiwjwAfRJRtUP6pC7wt4opeF14enO0du2dRV6Llo=
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.0.6 h1:/T45mIHc5hcEvibgzBzvMy7ruT+RjgoQRvkHbnl6OWA=
github.com/olekukonko/tablewriter v1.0.6/go.mod h1:SJ0MV1aHb/89fLcsBMXMp30Xg3g5eGoOUu0RptEk4AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=

View File

@@ -14,7 +14,8 @@ import (
"strings"
"time"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/opencloud/pkg/register"
"github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/version"
@@ -403,11 +404,24 @@ func benchmark(iterations int, path string) error {
fmt.Printf("Iterations: %d\n", iterations)
fmt.Println("")
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Test", "Iterations", "dur/it", "total"})
table.SetAutoFormatHeaders(false)
table.SetColumnAlignment([]int{tw.ALIGN_LEFT, tw.ALIGN_RIGHT, tw.ALIGN_RIGHT, tw.ALIGN_RIGHT})
table.SetAutoMergeCellsByColumnIndex([]int{2, 3})
cfg := tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoFormat: tw.Off,
},
},
Row: tw.CellConfig{
ColumnAligns: []tw.Align{
tw.AlignLeft,
tw.AlignRight,
tw.AlignRight,
tw.AlignRight,
},
},
}
table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg))
table.Header([]string{"Test", "Iterations", "dur/it", "total"})
for _, t := range []string{"lockedfile open(wo,c,t) close", "stat", "fopen(wo,t) write close", "fopen(ro) close", "fopen(ro) read close", "xattr-set", "xattr-get"} {
start := time.Now()
err := tests[t]()

View File

@@ -4,7 +4,8 @@ import (
"fmt"
"os"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/urfave/cli/v2"
mreg "go-micro.dev/v4/registry"
@@ -62,9 +63,8 @@ func VersionCommand(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -480,8 +480,8 @@ func (s *Service) generateRunSet(cfg *occfg.Config) {
// List running processes for the Service Controller.
func (s *Service) List(_ struct{}, reply *string) error {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader([]string{"Service"})
table := tablewriter.NewTable(tableString)
table.Header([]string{"Service"})
names := []string{}
for t := range s.serviceToken {

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/app-provider/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/app-registry/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/auth-app/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/auth-machine/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/auth-service/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/frontend/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/gateway/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -6,6 +6,8 @@ import (
"strings"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/urfave/cli/v2"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
@@ -54,12 +56,17 @@ func listUnifiedRoles(cfg *config.Config) *cli.Command {
Name: "list",
Usage: "list available unified roles",
Action: func(c *cli.Context) error {
tbl := tablewriter.NewWriter(os.Stdout)
tbl.SetRowLine(true)
tbl.SetAutoMergeCellsByColumnIndex([]int{0}) // rowspan should only affect the first column
r := tw.Rendition{
Settings: tw.Settings{
Separators: tw.Separators{
BetweenRows: tw.On,
},
},
}
tbl := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(r)))
headers := []string{"Name", "UID", "Enabled", "Description", "Condition", "Allowed resource actions"}
tbl.SetHeader(headers)
tbl.Header(headers)
for _, definition := range unifiedrole.GetRoles(unifiedrole.RoleFilterAll()) {
const enabled = "enabled"

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/groups/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/idp/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/invitations/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/ocs/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/policies/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/search/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/settings/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/sharing/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/storage-publiclink/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/storage-shares/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/storage-system/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -14,7 +14,8 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mohae/deepcopy"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
zlog "github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/config"
@@ -469,11 +470,10 @@ func itemType(it provider.ResourceType) string {
return itemType
}
func itemsTable(total int) *tw.Table {
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"itemID", "path", "type", "delete at"})
table.SetAutoFormatHeaders(false)
table.SetFooter([]string{"", "", "", "total count: " + strconv.Itoa(total)})
func itemsTable(total int) *tablewriter.Table {
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"itemID", "path", "type", "delete at"})
table.Footer([]string{"", "", "", "total count: " + strconv.Itoa(total)})
return table
}

View File

@@ -9,7 +9,8 @@ import (
"strings"
"time"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/urfave/cli/v2"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
@@ -145,16 +146,15 @@ func ListUploadSessions(cfg *config.Config) *cli.Command {
}
var (
table *tw.Table
table *tablewriter.Table
raw []Session
)
if !c.Bool("json") {
fmt.Println(buildInfo(filter))
table = tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Space", "Upload Id", "Name", "Offset", "Size", "Executant", "Owner", "Expires", "Processing", "Scan Date", "Scan Result"})
table.SetAutoFormatHeaders(false)
table = tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Space", "Upload Id", "Name", "Offset", "Size", "Executant", "Owner", "Expires", "Processing", "Scan Date", "Scan Result"})
}
for _, u := range uploads {

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/thumbnails/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/users/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/web/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/webdav/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

View File

@@ -7,7 +7,8 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/webfinger/pkg/config"
"github.com/urfave/cli/v2"
)
@@ -35,9 +36,8 @@ func Version(cfg *config.Config) *cli.Command {
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})

3
vendor/github.com/olekukonko/errors/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/
tmp/

21
vendor/github.com/olekukonko/errors/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Oleku Konko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1358
vendor/github.com/olekukonko/errors/README.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

957
vendor/github.com/olekukonko/errors/errors.go generated vendored Normal file
View File

@@ -0,0 +1,957 @@
package errors
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"runtime"
"strings"
"sync"
"sync/atomic"
)
const (
ctxTimeout = "[error] timeout" // Context key for marking timeout errors
ctxRetry = "[error] retry" // Context key for marking retryable errors
contextSize = 4 // Default size of fixed-size context array
bufferSize = 256 // Initial buffer size for JSON marshaling
warmUpSize = 100 // Number of errors to pre-warm the pool
stackDepth = 32 // Default maximum stack trace depth
)
type ErrorCategory string
// ErrorOpts provides options for customizing error creation.
type ErrorOpts struct {
SkipStack int // Number of stack frames to skip when capturing the stack trace
}
// Config defines the configuration for the errors package.
type Config struct {
StackDepth int // Maximum depth of the stack trace; 0 uses default
ContextSize int // Initial size of the context map; 0 uses default
DisablePooling bool // Disables object pooling for errors if true
FilterInternal bool // Filters internal package frames from stack traces if true
AutoFree bool // Automatically frees errors to pool if true
}
// cachedConfig holds the current configuration, updated only on Configure().
type cachedConfig struct {
stackDepth int
contextSize int
disablePooling bool
filterInternal bool
autoFree bool
}
var (
currentConfig cachedConfig
configMu sync.RWMutex
errorPool = NewErrorPool() // Custom pool for Error instances
stackPool = sync.Pool{ // Pool for stack trace slices
New: func() interface{} {
return make([]uintptr, currentConfig.stackDepth)
},
}
emptyError = &Error{
smallContext: [contextSize]contextItem{},
msg: "",
name: "",
template: "",
cause: nil,
}
)
//var bufferPool = sync.Pool{
// New: func() interface{} {
// return bytes.NewBuffer(make([]byte, 0, bufferSize))
// },
//}
// contextItem represents a single key-value pair in the smallContext array.
type contextItem struct {
key string
value interface{}
}
// Error represents a custom error with enhanced features like context, stack traces, and wrapping.
type Error struct {
// Primary error information (most frequently accessed)
msg string // Error message
name string // Error name/type
stack []uintptr // Stack trace
// Secondary error metadata
template string // Message template used if msg is empty
category string // Error category (e.g., "network", "validation")
count uint64 // Occurrence count for tracking frequency
code int32 // HTTP-like error code
smallCount int32 // Number of items in smallContext
// Context and chaining
context map[string]interface{} // Additional context as key-value pairs
cause error // Wrapped underlying error
callback func() // Optional callback executed on Error()
smallContext [contextSize]contextItem // Fixed-size context storage for efficiency
// Synchronization
mu sync.RWMutex // Protects concurrent access to mutable fields
}
// init initializes the package with default configuration and pre-warms the error pool.
func init() {
currentConfig = cachedConfig{
stackDepth: stackDepth,
contextSize: contextSize,
disablePooling: false,
filterInternal: true,
autoFree: true,
}
WarmPool(warmUpSize) // Pre-warm pool with initial errors
}
// Configure updates the global configuration for the errors package.
// Thread-safe; should be called before heavy usage for optimal performance.
// Changes apply immediately to all subsequent error operations.
func Configure(cfg Config) {
configMu.Lock()
defer configMu.Unlock()
if cfg.StackDepth != 0 {
currentConfig.stackDepth = cfg.StackDepth
}
if cfg.ContextSize != 0 {
currentConfig.contextSize = cfg.ContextSize
}
currentConfig.disablePooling = cfg.DisablePooling
currentConfig.filterInternal = cfg.FilterInternal
currentConfig.autoFree = cfg.AutoFree
}
// newError creates a new Error instance, using the pool if enabled.
// Initializes smallContext and stack appropriately.
func newError() *Error {
if currentConfig.disablePooling {
return &Error{
smallContext: [contextSize]contextItem{},
stack: nil,
}
}
return errorPool.Get()
}
// Empty creates a new empty error with no stack trace.
// Useful as a base for building errors incrementally.
func Empty() *Error {
return emptyError
}
// Named creates a new error with a specific name and stack trace.
// The name is used as the error message if no other message is set.
func Named(name string) *Error {
e := newError()
e.name = name
return e.WithStack()
}
// New creates a fast, lightweight error without stack tracing.
// Use instead of Trace() when stack traces aren't needed for better performance.
func New(text string) *Error {
if text == "" {
return emptyError.Copy() // Global pre-allocated empty error
}
err := newError()
err.msg = text
return err
}
// Newf is an alias to Errorf for fmt.Errorf compatibility.
// Creates a formatted error without stack traces.
func Newf(format string, args ...interface{}) *Error {
err := newError()
err.msg = fmt.Sprintf(format, args...)
return err
}
// Std creates a standard error using errors.New, provided for backward compatibility.
// This function serves as a lightweight wrapper around the standard library's error creation,
// allowing users to opt into basic error handling without adopting the full features of this package.
func Std(text string) error {
return errors.New(text)
}
// Stdf creates a formatted standard error using fmt.Errorf, provided for backward compatibility.
// This function wraps the standard library's formatted error creation, offering a simple alternative
// to the package's enhanced error handling while maintaining compatibility with existing codebases.
func Stdf(format string, a ...interface{}) error {
return fmt.Errorf(format, a...)
}
// Trace creates an error with stack trace capture enabled.
// Use when call stacks are needed for debugging; has performance overhead.
func Trace(text string) *Error {
e := New(text)
return e.WithStack()
}
// Tracef creates a formatted error with stack trace.
// Combines Errorf and WithStack for convenience.
func Tracef(format string, args ...interface{}) *Error {
e := Newf(format, args...)
return e.WithStack()
}
// As attempts to assign the error or one in its chain to the target interface.
// It only assigns when the target is a **Error and the current error node has a non-empty name.
// If the current node has an empty name, it delegates to its wrapped cause.
func (e *Error) As(target interface{}) bool {
if e == nil {
return false
}
// Handle *Error target (for stderrors.As compatibility)
if targetPtr, ok := target.(*Error); ok {
current := e
for current != nil {
if current.name != "" {
*targetPtr = *current
return true
}
if next, ok := current.cause.(*Error); ok {
current = next
} else if current.cause != nil {
return errors.As(current.cause, target)
} else {
return false
}
}
return false
}
// Handle *error target - unwrap to innermost error
if targetErr, ok := target.(*error); ok {
innermost := error(e)
current := error(e)
for current != nil {
if err, ok := current.(*Error); ok && err.cause != nil {
current = err.cause
innermost = current
} else {
break
}
}
*targetErr = innermost
return true
}
// Delegate to cause for other types
if e.cause != nil {
return errors.As(e.cause, target)
}
return false
}
// Callback sets a function to be called when Error() is invoked.
// Useful for logging or side effects; returns the error for chaining.
func (e *Error) Callback(fn func()) *Error {
e.callback = fn
return e
}
// Category returns the error's category, if set.
// Returns an empty string if no category is defined.
func (e *Error) Category() string {
return e.category
}
// Code returns the error's status code, if set.
// Returns 0 if no code is defined.
func (e *Error) Code() int {
return int(e.code)
}
// Context returns the error's context as a map.
// Converts smallContext to a map if needed; returns nil if empty.
func (e *Error) Context() map[string]interface{} {
e.mu.RLock()
defer e.mu.RUnlock()
if e.smallCount > 0 && e.context == nil {
e.context = make(map[string]interface{}, e.smallCount)
for i := int32(0); i < e.smallCount; i++ {
e.context[e.smallContext[i].key] = e.smallContext[i].value
}
}
return e.context
}
// Copy creates a deep copy of the error, preserving all fields except stack.
// The new error does not capture a new stack trace unless explicitly added.
func (e *Error) Copy() *Error {
if e == emptyError {
return &Error{
smallContext: [contextSize]contextItem{},
}
}
newErr := newError()
newErr.msg = e.msg
newErr.name = e.name
newErr.template = e.template
newErr.cause = e.cause
newErr.code = e.code
newErr.category = e.category
newErr.count = e.count
if e.smallCount > 0 {
newErr.smallCount = e.smallCount
for i := int32(0); i < e.smallCount; i++ {
newErr.smallContext[i] = e.smallContext[i]
}
} else if e.context != nil {
newErr.context = make(map[string]interface{}, len(e.context))
for k, v := range e.context {
newErr.context[k] = v
}
}
if e.stack != nil && len(e.stack) > 0 {
if newErr.stack == nil {
newErr.stack = stackPool.Get().([]uintptr)
}
newErr.stack = append(newErr.stack[:0], e.stack...)
}
return newErr
}
// Count returns the number of times the error has been incremented.
// Useful for tracking occurrence frequency.
func (e *Error) Count() uint64 {
return e.count
}
// Err returns the error as an error interface.
// Provided for compatibility; simply returns the error itself.
func (e *Error) Err() error {
return e
}
// Error returns the string representation of the error.
// Prioritizes msg, then template, then name, falling back to "unknown error".
// Executes callback if set before returning the message.
func (e *Error) Error() string {
if e.callback != nil {
e.callback()
}
var msg string
switch {
case e.msg != "":
msg = e.msg
case e.template != "":
msg = e.template
case e.name != "":
msg = e.name
default:
msg = "unknown error"
}
if e.cause != nil {
causeMsg := e.cause.Error()
if msg != "" && causeMsg != "" {
msg = msg + ": " + causeMsg
} else if causeMsg != "" {
msg = causeMsg
}
}
return msg
}
// Errorf creates a formatted error without stack traces.
// Compatible with fmt.Errorf; does not capture stack trace for performance.
func Errorf(format string, args ...interface{}) *Error {
err := newError()
err.msg = fmt.Sprintf(format, args...)
return err
}
// FastStack returns a lightweight stack trace without function names.
// Filters internal frames if FilterInternal is enabled; returns nil if no stack.
func (e *Error) FastStack() []string {
if e.stack == nil {
return nil
}
configMu.RLock()
filter := currentConfig.filterInternal
configMu.RUnlock()
pcs := e.stack
frames := make([]string, 0, len(pcs))
for _, pc := range pcs {
fn := runtime.FuncForPC(pc)
if fn == nil {
frames = append(frames, "unknown")
continue
}
file, line := fn.FileLine(pc)
if filter && isInternalFrame(runtime.Frame{File: file, Function: fn.Name()}) {
continue
}
frames = append(frames, fmt.Sprintf("%s:%d", file, line))
}
return frames
}
// Find searches the error chain for the first error matching pred.
// Starts with the current error and follows Unwrap() and Cause() chains.
func (e *Error) Find(pred func(error) bool) error {
if e == nil || pred == nil {
return nil
}
return Find(e, pred)
}
// Format returns a formatted string representation of the error.
// Includes message, code, context, and stack trace if present.
func (e *Error) Format() string {
var sb strings.Builder
// Error message
sb.WriteString("Error: " + e.Error() + "\n")
// Metadata
if e.code != 0 {
sb.WriteString(fmt.Sprintf("Code: %d\n", e.code))
}
// Context (only show context added at this level)
if ctx := e.contextAtThisLevel(); len(ctx) > 0 {
sb.WriteString("Context:\n")
for k, v := range ctx {
sb.WriteString(fmt.Sprintf(" %s: %v\n", k, v))
}
}
// Stack trace
if e.stack != nil {
sb.WriteString("Stack:\n")
for i, frame := range e.Stack() {
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, frame))
}
}
return sb.String()
}
// contextAtThisLevel returns context specific to this error level, excluding inherited context.
// Combines smallContext and context map into a single map; returns nil if empty.
func (e *Error) contextAtThisLevel() map[string]interface{} {
if e.context == nil && e.smallCount == 0 {
return nil
}
ctx := make(map[string]interface{})
// Add smallContext items
for i := 0; i < int(e.smallCount); i++ {
ctx[e.smallContext[i].key] = e.smallContext[i].value
}
// Add map context items
if e.context != nil {
for k, v := range e.context {
ctx[k] = v
}
}
return ctx
}
// Free resets the error and returns it to the pool if pooling is enabled.
// Does nothing beyond reset if pooling is disabled.
func (e *Error) Free() {
if currentConfig.disablePooling {
return
}
e.Reset()
if e.stack != nil {
stackPool.Put(e.stack[:cap(e.stack)])
e.stack = nil
}
errorPool.Put(e)
}
// Has checks if the error contains meaningful content.
// Returns true if msg, template, name, or cause is non-empty/nil.
func (e *Error) Has() bool {
return e != nil && (e.msg != "" || e.template != "" || e.name != "" || e.cause != nil)
}
// HasContextKey checks if the specified key exists in the error's context.
// Searches both smallContext and context map; thread-safe.
func (e *Error) HasContextKey(key string) bool {
e.mu.RLock()
defer e.mu.RUnlock()
if e.smallCount > 0 {
for i := int32(0); i < e.smallCount; i++ {
if e.smallContext[i].key == key {
return true
}
}
}
if e.context != nil {
_, exists := e.context[key]
return exists
}
return false
}
// Increment increases the error's count by 1 and returns the error.
// Uses atomic operation for thread safety.
func (e *Error) Increment() *Error {
atomic.AddUint64(&e.count, 1)
return e
}
// Is checks if the error matches a target error by pointer equality, name, or wrapped cause.
// Ensures compatibility with stderrors.Is by prioritizing chain traversal.
func (e *Error) Is(target error) bool {
if e == nil || target == nil {
return e == target
}
if e == target {
return true
}
if e.name != "" {
if te, ok := target.(*Error); ok && te.name != "" && e.name == te.name {
return true
}
}
// Add string comparison for standard errors
if stdErr, ok := target.(error); ok && e.Error() == stdErr.Error() {
return true
}
if e.cause != nil {
return errors.Is(e.cause, target)
}
return false
}
// IsEmpty checks if the error has no meaningful content (empty message, no name/template/cause).
// Returns true for nil errors or errors with no data.
func (e *Error) IsEmpty() bool {
if e == nil {
return true
}
return e.msg == "" && e.template == "" && e.name == "" && e.cause == nil
}
// IsNull checks if an error is nil or represents a SQL NULL value.
// Considers both the error itself and any context values; returns true if all context is null.
func (e *Error) IsNull() bool {
if e == nil || e == emptyError {
return true
}
// If no context or cause, and no content, it's not null
if e.smallCount == 0 && e.context == nil && e.cause == nil {
return false
}
// Check cause first - if its null, the whole error is null
if e.cause != nil {
var isNull bool
if ce, ok := e.cause.(*Error); ok {
isNull = ce.IsNull()
} else {
isNull = sqlNull(e.cause)
}
if isNull {
return true
}
// If cause isnt null, continue checking this errors context
}
// Check small context
if e.smallCount > 0 {
allNull := true
for i := 0; i < int(e.smallCount); i++ {
isNull := sqlNull(e.smallContext[i].value)
if !isNull {
allNull = false
break
}
}
if !allNull {
return false
}
}
// Check regular context
if e.context != nil {
allNull := true
for _, v := range e.context {
isNull := sqlNull(v)
if !isNull {
allNull = false
break
}
}
if !allNull {
return false
}
}
// Null if we have context and its all null
return e.smallCount > 0 || e.context != nil
}
var (
jsonBufferPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, bufferSize))
},
}
)
// MarshalJSON serializes the error to JSON, including name, message, context, cause, and stack.
// Handles nested *Error causes and custom marshalers efficiently.
func (e *Error) MarshalJSON() ([]byte, error) {
// Get buffer from pool
buf := jsonBufferPool.Get().(*bytes.Buffer)
defer jsonBufferPool.Put(buf)
buf.Reset()
// Create new encoder each time (no Reset available)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
// Prepare error data
je := struct {
Name string `json:"name,omitempty"`
Message string `json:"message,omitempty"`
Context map[string]interface{} `json:"context,omitempty"`
Cause interface{} `json:"cause,omitempty"`
Stack []string `json:"stack,omitempty"`
Code int `json:"code,omitempty"`
}{
Name: e.name,
Message: e.msg,
Code: e.Code(),
}
// Handle context
if ctx := e.Context(); len(ctx) > 0 {
je.Context = ctx
}
// Handle stack
if e.stack != nil {
je.Stack = e.Stack()
}
// Handle cause
if e.cause != nil {
switch c := e.cause.(type) {
case *Error:
je.Cause = c
case json.Marshaler:
je.Cause = c
default:
je.Cause = c.Error()
}
}
// Encode
if err := enc.Encode(je); err != nil {
return nil, err
}
// Return bytes without trailing newline
result := buf.Bytes()
if len(result) > 0 && result[len(result)-1] == '\n' {
result = result[:len(result)-1]
}
return result, nil
}
// Msgf sets the error message using a formatted string.
// Overwrites any existing message; returns the error for chaining.
func (e *Error) Msgf(format string, args ...interface{}) *Error {
e.msg = fmt.Sprintf(format, args...)
return e
}
// Name returns the error's name, if set.
// Returns an empty string if no name is defined.
func (e *Error) Name() string {
return e.name
}
// Reset clears all fields of the error, preparing it for reuse.
// Does not free the stack; use Free() to return to pool.
func (e *Error) Reset() {
e.msg = ""
e.name = ""
e.template = ""
e.category = ""
e.code = 0
e.count = 0
e.cause = nil
e.callback = nil
if e.context != nil {
for k := range e.context {
delete(e.context, k)
}
}
e.smallCount = 0
if e.stack != nil {
e.stack = e.stack[:0]
}
}
// Stack returns a detailed stack trace as a slice of strings.
// Filters internal frames if FilterInternal is enabled; returns nil if no stack.
func (e *Error) Stack() []string {
if e.stack == nil {
return nil
}
frames := runtime.CallersFrames(e.stack)
var trace []string
for {
frame, more := frames.Next()
if frame == (runtime.Frame{}) {
break
}
if currentConfig.filterInternal && isInternalFrame(frame) {
continue
}
trace = append(trace, fmt.Sprintf("%s %s:%d",
frame.Function,
frame.File,
frame.Line))
if !more {
break
}
}
return trace
}
// Trace ensures the error has a stack trace, capturing it if missing.
// Skips capture if stack already exists; returns the error for chaining.
func (e *Error) Trace() *Error {
if e.stack == nil {
e.stack = captureStack(2)
}
return e
}
// Transform applies transformations to a copy of the error.
// Returns the transformed copy or the original if no changes are needed.
func (e *Error) Transform(fn func(*Error)) *Error {
if e == nil || fn == nil {
return e
}
newErr := e.Copy()
fn(newErr)
return newErr
}
// Unwrap returns the underlying cause of the error, if any.
// Implements the errors.Unwrap interface for unwrapping chains.
func (e *Error) Unwrap() error {
return e.cause
}
// UnwrapAll returns a slice of all errors in the chain, starting with this error.
// Traverses the cause chain, creating isolated copies of each *Error.
func (e *Error) UnwrapAll() []error {
if e == nil {
return nil
}
var chain []error
current := error(e)
for current != nil {
if err, ok := current.(*Error); ok {
isolated := newError()
isolated.msg = err.msg
isolated.name = err.name
isolated.template = err.template
isolated.code = err.code
isolated.category = err.category
if err.smallCount > 0 {
isolated.smallCount = err.smallCount
for i := int32(0); i < err.smallCount; i++ {
isolated.smallContext[i] = err.smallContext[i]
}
}
if err.context != nil {
isolated.context = make(map[string]interface{}, len(err.context))
for k, v := range err.context {
isolated.context[k] = v
}
}
if err.stack != nil {
isolated.stack = append([]uintptr(nil), err.stack...)
}
chain = append(chain, isolated)
} else {
chain = append(chain, current)
}
if unwrapper, ok := current.(interface{ Unwrap() error }); ok {
current = unwrapper.Unwrap()
} else {
break
}
}
return chain
}
// Walk traverses the error chain, applying fn to each error.
// Starts with the current error and follows the cause chain.
func (e *Error) Walk(fn func(error)) {
if e == nil || fn == nil {
return
}
current := error(e)
for current != nil {
fn(current)
if unwrappable, ok := current.(interface{ Unwrap() error }); ok {
current = unwrappable.Unwrap()
} else {
break
}
}
}
// With adds a key-value pair to the error's context.
// Uses smallContext for efficiency until full, then switches to map; thread-safe.
func (e *Error) With(key string, value interface{}) *Error {
// Fast path for small context (no map needed)
if e.smallCount < contextSize && e.context == nil {
e.mu.Lock()
// Double-check after acquiring lock
if e.smallCount < contextSize && e.context == nil {
e.smallContext[e.smallCount] = contextItem{key, value}
e.smallCount++
e.mu.Unlock()
return e
}
e.mu.Unlock()
}
// Slow path - requires map
e.mu.Lock()
defer e.mu.Unlock()
if e.context == nil {
e.context = make(map[string]interface{}, currentConfig.contextSize)
// Migrate existing items if any
for i := int32(0); i < e.smallCount; i++ {
e.context[e.smallContext[i].key] = e.smallContext[i].value
}
}
e.context[key] = value
return e
}
// WithCategory sets a category for the error and returns the error.
// Useful for classifying errors (e.g., "network", "validation").
func (e *Error) WithCategory(category ErrorCategory) *Error {
e.category = string(category)
return e
}
// WithCode sets an HTTP-like status code for the error and returns the error.
// Overwrites any existing code.
func (e *Error) WithCode(code int) *Error {
e.code = int32(code)
return e
}
// WithName sets the error's name and returns the error.
// Overwrites any existing name.
func (e *Error) WithName(name string) *Error {
e.name = name
return e
}
// WithRetryable marks the error as retryable in its context.
// Adds a "retry" key with value true; returns the error.
func (e *Error) WithRetryable() *Error {
return e.With(ctxRetry, true)
}
// WithStack captures the stack trace at call time and returns the error.
// Skips capturing if stack already exists or depth is 0.
func (e *Error) WithStack() *Error {
if e.stack == nil {
e.stack = captureStack(1) // Skip WithStack
}
return e
}
// WithTemplate sets a template string for the error and returns the error.
// Used as the error message if no explicit message is set.
func (e *Error) WithTemplate(template string) *Error {
e.template = template
return e
}
// WithTimeout marks the error as a timeout error in its context.
// Adds a "timeout" key with value true; returns the error.
func (e *Error) WithTimeout() *Error {
return e.With(ctxTimeout, true)
}
// Wrap associates a cause error with this error, creating an error chain.
// Returns the error for method chaining.
func (e *Error) Wrap(cause error) *Error {
if cause == nil {
return e
}
e.cause = cause
return e
}
// WrapNotNil wraps a cause error only if it is non-nil.
// Returns the error for method chaining; no-op if cause is nil.
func (e *Error) WrapNotNil(cause error) *Error {
if cause != nil {
e.cause = cause
}
return e
}
// WarmPool pre-populates the error pool with a specified number of instances.
// Reduces allocation overhead during initial usage; no effect if pooling is disabled.
func WarmPool(count int) {
if currentConfig.disablePooling {
return
}
for i := 0; i < count; i++ {
e := &Error{
smallContext: [contextSize]contextItem{},
stack: nil,
}
errorPool.Put(e)
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
}
}
// WarmStackPool pre-populates the stack pool with a specified number of slices.
// Reduces allocation overhead for stack traces; no effect if pooling is disabled.
func WarmStackPool(count int) {
if currentConfig.disablePooling {
return
}
for i := 0; i < count; i++ {
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
}
}

423
vendor/github.com/olekukonko/errors/helper.go generated vendored Normal file
View File

@@ -0,0 +1,423 @@
package errors
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
// As wraps errors.As, using custom type assertion for *Error types.
// Falls back to standard errors.As for non-*Error types.
// Returns false if either err or target is nil.
func As(err error, target interface{}) bool {
if err == nil || target == nil {
return false
}
// First try our custom *Error handling
if e, ok := err.(*Error); ok {
return e.As(target)
}
// Fall back to standard errors.As
return errors.As(err, target)
}
// Code returns the status code of an error, if it is an *Error.
// Returns 500 as a default for non-*Error types to indicate an internal error.
func Code(err error) int {
if e, ok := err.(*Error); ok {
return e.Code()
}
return 500
}
// Context extracts the context map from an error, if it is an *Error.
// Returns nil for non-*Error types or if no context is present.
func Context(err error) map[string]interface{} {
if e, ok := err.(*Error); ok {
return e.Context()
}
return nil
}
// Convert transforms any error into an *Error, preserving its message and wrapping it if needed.
// Returns nil if the input is nil; returns the original if already an *Error.
// Uses multiple strategies: direct assertion, errors.As, manual unwrapping, and fallback creation.
func Convert(err error) *Error {
if err == nil {
return nil
}
// First try direct type assertion (fast path)
if e, ok := err.(*Error); ok {
return e
}
// Try using errors.As (more flexible)
var e *Error
if errors.As(err, &e) {
return e
}
// Manual unwrapping as fallback
for unwrapped := err; unwrapped != nil; {
if e, ok := unwrapped.(*Error); ok {
return e
}
unwrapped = errors.Unwrap(unwrapped)
}
// Final fallback: create new error with original message and wrap it
return New(err.Error()).Wrap(err)
}
// Count returns the occurrence count of an error, if it is an *Error.
// Returns 0 for non-*Error types.
func Count(err error) uint64 {
if e, ok := err.(*Error); ok {
return e.Count()
}
return 0
}
// Find searches the error chain for the first error matching pred.
// Returns nil if no match is found or pred is nil; traverses both Unwrap() and Cause() chains.
func Find(err error, pred func(error) bool) error {
for current := err; current != nil; {
if pred(current) {
return current
}
// Attempt to unwrap using Unwrap() or Cause()
switch v := current.(type) {
case interface{ Unwrap() error }:
current = v.Unwrap()
case interface{ Cause() error }:
current = v.Cause()
default:
return nil
}
}
return nil
}
// From transforms any error into an *Error, preserving its message and wrapping it if needed.
// Alias of Convert; returns nil if input is nil, original if already an *Error.
func From(err error) *Error {
return Convert(err)
}
// FromContext creates an *Error from a context and an existing error.
// Enhances the error with context info: timeout status, deadline, or cancellation.
// Returns nil if input error is nil; does not store context values directly.
func FromContext(ctx context.Context, err error) *Error {
if err == nil {
return nil
}
e := New(err.Error())
// Handle context errors
switch ctx.Err() {
case context.DeadlineExceeded:
e.WithTimeout()
if deadline, ok := ctx.Deadline(); ok {
e.With("deadline", deadline.Format(time.RFC3339))
}
case context.Canceled:
e.With("cancelled", true)
}
return e
}
// Category returns the category of an error, if it is an *Error.
// Returns an empty string for non-*Error types or unset categories.
func Category(err error) string {
if e, ok := err.(*Error); ok {
return e.category
}
return ""
}
// Has checks if an error contains meaningful content.
// Returns true for non-nil standard errors or *Error with content (msg, name, template, or cause).
func Has(err error) bool {
if e, ok := err.(*Error); ok {
return e.Has()
}
return err != nil
}
// HasContextKey checks if the error's context contains the specified key.
// Returns false for non-*Error types or if the key is not present in the context.
func HasContextKey(err error, key string) bool {
if e, ok := err.(*Error); ok {
ctx := e.Context()
if ctx != nil {
_, exists := ctx[key]
return exists
}
}
return false
}
// Is wraps errors.Is, using custom matching for *Error types.
// Falls back to standard errors.Is for non-*Error types; returns true if err equals target.
func Is(err, target error) bool {
if err == nil || target == nil {
return err == target
}
if e, ok := err.(*Error); ok {
return e.Is(target)
}
// Use standard errors.Is for non-Error types
return errors.Is(err, target)
}
// IsError checks if an error is an instance of *Error.
// Returns true only for this package's custom error type; false for nil or other types.
func IsError(err error) bool {
_, ok := err.(*Error)
return ok
}
// IsEmpty checks if an error has no meaningful content.
// Returns true for nil errors, empty *Error instances, or standard errors with whitespace-only messages.
func IsEmpty(err error) bool {
if err == nil {
return true
}
if e, ok := err.(*Error); ok {
return e.IsEmpty()
}
return strings.TrimSpace(err.Error()) == ""
}
// IsNull checks if an error is nil or represents a NULL value.
// Delegates to *Errors IsNull for custom errors; uses sqlNull for others.
func IsNull(err error) bool {
if err == nil {
return true
}
if e, ok := err.(*Error); ok {
return e.IsNull()
}
return sqlNull(err)
}
// IsRetryable checks if an error is retryable.
// For *Error, checks context for retry flag; for others, looks for "retry" or timeout in message.
// Returns false for nil errors; thread-safe for *Error types.
func IsRetryable(err error) bool {
if err == nil {
return false
}
if e, ok := err.(*Error); ok {
e.mu.RLock()
defer e.mu.RUnlock()
// Check smallContext directly if context map isnt populated
for i := int32(0); i < e.smallCount; i++ {
if e.smallContext[i].key == ctxRetry {
if val, ok := e.smallContext[i].value.(bool); ok {
return val
}
}
}
// Fallback to context map
if e.context != nil {
if val, ok := e.context[ctxRetry].(bool); ok {
return val
}
}
}
lowerMsg := strings.ToLower(err.Error())
return IsTimeout(err) || strings.Contains(lowerMsg, "retry")
}
// IsTimeout checks if an error indicates a timeout.
// For *Error, checks context for timeout flag; for others, looks for "timeout" in message.
// Returns false for nil errors.
func IsTimeout(err error) bool {
if err == nil {
return false
}
if e, ok := err.(*Error); ok {
if val, ok := e.Context()[ctxTimeout].(bool); ok {
return val
}
}
return strings.Contains(strings.ToLower(err.Error()), "timeout")
}
// Merge combines multiple errors into a single *Error.
// Aggregates messages with "; " separator, merges contexts and stacks; returns nil if no errors provided.
func Merge(errs ...error) *Error {
if len(errs) == 0 {
return nil
}
var messages []string
combined := New("")
for _, err := range errs {
if err == nil {
continue
}
messages = append(messages, err.Error())
if e, ok := err.(*Error); ok {
if e.stack != nil && combined.stack == nil {
combined.WithStack() // Capture stack from first *Error with stack
}
if ctx := e.Context(); ctx != nil {
for k, v := range ctx {
combined.With(k, v)
}
}
if e.cause != nil {
combined.Wrap(e.cause)
}
} else {
combined.Wrap(err)
}
}
if len(messages) > 0 {
combined.msg = strings.Join(messages, "; ")
}
return combined
}
// Name returns the name of an error, if it is an *Error.
// Returns an empty string for non-*Error types or unset names.
func Name(err error) string {
if e, ok := err.(*Error); ok {
return e.name
}
return ""
}
// UnwrapAll returns a slice of all errors in the chain, including the root error.
// Traverses both Unwrap() and Cause() chains; returns nil if err is nil.
func UnwrapAll(err error) []error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.UnwrapAll()
}
var result []error
Walk(err, func(e error) {
result = append(result, e)
})
return result
}
// Stack extracts the stack trace from an error, if it is an *Error.
// Returns nil for non-*Error types or if no stack is present.
func Stack(err error) []string {
if e, ok := err.(*Error); ok {
return e.Stack()
}
return nil
}
// Transform applies transformations to an error, returning a new *Error.
// Creates a new *Error from non-*Error types before applying fn; returns nil if err is nil.
func Transform(err error, fn func(*Error)) *Error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
newErr := e.Copy()
fn(newErr)
return newErr
}
// If not an *Error, create a new one and transform it
newErr := New(err.Error())
fn(newErr)
return newErr
}
// Unwrap returns the underlying cause of an error, if it implements Unwrap.
// For *Error, returns cause; for others, returns the error itself; nil if err is nil.
func Unwrap(err error) error {
for current := err; current != nil; {
if e, ok := current.(*Error); ok {
if e.cause == nil {
return current
}
current = e.cause
} else {
return current
}
}
return nil
}
// Walk traverses the error chain, applying fn to each error.
// Supports both Unwrap() and Cause() interfaces; stops at nil or non-unwrappable errors.
func Walk(err error, fn func(error)) {
for current := err; current != nil; {
fn(current)
// Attempt to unwrap using Unwrap() or Cause()
switch v := current.(type) {
case interface{ Unwrap() error }:
current = v.Unwrap()
case interface{ Cause() error }:
current = v.Cause()
default:
return
}
}
}
// With adds a key-value pair to an error's context, if it is an *Error.
// Returns the original error unchanged if not an *Error; no-op for non-*Error types.
func With(err error, key string, value interface{}) error {
if e, ok := err.(*Error); ok {
return e.With(key, value)
}
return err
}
// WithStack converts any error to an *Error and captures a stack trace.
// Returns nil if input is nil; adds stack to existing *Error or wraps non-*Error types.
func WithStack(err error) *Error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.WithStack()
}
return New(err.Error()).WithStack().Wrap(err)
}
// Wrap creates a new *Error that wraps another error with additional context.
// Uses a copy of the provided wrapper *Error; returns nil if err is nil.
func Wrap(err error, wrapper *Error) *Error {
if err == nil {
return nil
}
if wrapper == nil {
wrapper = newError()
}
newErr := wrapper.Copy()
newErr.cause = err
return newErr
}
// Wrapf creates a new formatted *Error that wraps another error.
// Formats the message and sets the cause; returns nil if err is nil.
func Wrapf(err error, format string, args ...interface{}) *Error {
if err == nil {
return nil
}
e := newError()
e.msg = fmt.Sprintf(format, args...)
e.cause = err
return e
}

325
vendor/github.com/olekukonko/errors/multi_error.go generated vendored Normal file
View File

@@ -0,0 +1,325 @@
package errors
import (
"fmt"
"math/rand"
"strings"
"sync"
"time"
)
// MultiError represents a thread-safe collection of errors with enhanced features.
// Supports limits, sampling, and custom formatting for error aggregation.
type MultiError struct {
errors []error
mu sync.RWMutex
// Configuration fields
limit int // Maximum number of errors to store (0 = unlimited)
formatter ErrorFormatter // Custom formatting function for error string
sampling bool // Whether sampling is enabled to limit error collection
sampleRate uint32 // Sampling percentage (1-100) when sampling is enabled
rand *rand.Rand // Random source for sampling (nil defaults to fastRand)
}
// ErrorFormatter defines a function for custom error message formatting.
// Takes a slice of errors and returns a single formatted string.
type ErrorFormatter func([]error) string
// MultiErrorOption configures MultiError behavior during creation.
type MultiErrorOption func(*MultiError)
// NewMultiError creates a new MultiError instance with optional configuration.
// Initial capacity is set to 4; applies options in the order provided.
func NewMultiError(opts ...MultiErrorOption) *MultiError {
m := &MultiError{
errors: make([]error, 0, 4),
limit: 0, // Unlimited by default
}
for _, opt := range opts {
opt(m)
}
return m
}
// Add appends an error to the collection with optional sampling, limit checks, and duplicate prevention.
// Ignores nil errors and duplicates based on string equality; thread-safe.
func (m *MultiError) Add(err error) {
if err == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
// Check for duplicates by comparing error messages
for _, e := range m.errors {
if e.Error() == err.Error() {
return
}
}
// Apply sampling if enabled and collection isnt empty
if m.sampling && len(m.errors) > 0 {
var r uint32
if m.rand != nil {
r = uint32(m.rand.Int31n(100))
} else {
r = fastRand() % 100
}
if r > m.sampleRate { // Accept if random value is within sample rate
return
}
}
// Respect limit if set
if m.limit > 0 && len(m.errors) >= m.limit {
return
}
m.errors = append(m.errors, err)
}
// Clear removes all errors from the collection.
// Thread-safe; resets the slice while preserving capacity.
func (m *MultiError) Clear() {
m.mu.Lock()
defer m.mu.Unlock()
m.errors = m.errors[:0]
}
// Count returns the number of errors in the collection.
// Thread-safe.
func (m *MultiError) Count() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.errors)
}
// Error returns a formatted string representation of the errors.
// Returns empty string if no errors, single error message if one exists,
// or a formatted list using custom formatter or default if multiple; thread-safe.
func (m *MultiError) Error() string {
m.mu.RLock()
defer m.mu.RUnlock()
switch len(m.errors) {
case 0:
return ""
case 1:
return m.errors[0].Error()
default:
if m.formatter != nil {
return m.formatter(m.errors)
}
return defaultFormat(m.errors)
}
}
// Errors returns a copy of the contained errors.
// Thread-safe; returns nil if no errors exist.
func (m *MultiError) Errors() []error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.errors) == 0 {
return nil
}
errs := make([]error, len(m.errors))
copy(errs, m.errors)
return errs
}
// Filter returns a new MultiError containing only errors that match the predicate.
// Thread-safe; preserves original configuration including limit, formatter, and sampling.
func (m *MultiError) Filter(fn func(error) bool) *MultiError {
m.mu.RLock()
defer m.mu.RUnlock()
var opts []MultiErrorOption
opts = append(opts, WithLimit(m.limit))
if m.formatter != nil {
opts = append(opts, WithFormatter(m.formatter))
}
if m.sampling {
opts = append(opts, WithSampling(m.sampleRate))
}
filtered := NewMultiError(opts...)
for _, err := range m.errors {
if fn(err) {
filtered.Add(err)
}
}
return filtered
}
// First returns the first error in the collection, if any.
// Thread-safe; returns nil if the collection is empty.
func (m *MultiError) First() error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.errors) > 0 {
return m.errors[0]
}
return nil
}
// Has reports whether the collection contains any errors.
// Thread-safe.
func (m *MultiError) Has() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.errors) > 0
}
// Last returns the most recently added error in the collection, if any.
// Thread-safe; returns nil if the collection is empty.
func (m *MultiError) Last() error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.errors) > 0 {
return m.errors[len(m.errors)-1]
}
return nil
}
// Merge combines another MultiError's errors into this one.
// Thread-safe; respects this instances limit and sampling settings; no-op if other is nil or empty.
func (m *MultiError) Merge(other *MultiError) {
if other == nil || !other.Has() {
return
}
other.mu.RLock()
defer other.mu.RUnlock()
for _, err := range other.errors {
m.Add(err)
}
}
// IsNull checks if the MultiError is empty or contains only null errors.
// Returns true if empty or all errors are null (via IsNull() or empty message); thread-safe.
func (m *MultiError) IsNull() bool {
m.mu.RLock()
defer m.mu.RUnlock()
// Fast path for empty MultiError
if len(m.errors) == 0 {
return true
}
// Check each error for null status
allNull := true
for _, err := range m.errors {
switch e := err.(type) {
case interface{ IsNull() bool }:
if !e.IsNull() {
allNull = false
break
}
case nil:
continue
default:
if e.Error() != "" {
allNull = false
break
}
}
}
return allNull
}
// Single returns nil if the collection is empty, the single error if only one exists,
// or the MultiError itself if multiple errors are present.
// Thread-safe; useful for unwrapping to a single error when possible.
func (m *MultiError) Single() error {
m.mu.RLock()
defer m.mu.RUnlock()
switch len(m.errors) {
case 0:
return nil
case 1:
return m.errors[0]
default:
return m
}
}
// String implements the Stringer interface for a concise string representation.
// Thread-safe; delegates to Error() for formatting.
func (m *MultiError) String() string {
return m.Error()
}
// Unwrap returns a copy of the contained errors for multi-error unwrapping.
// Implements the errors.Unwrap interface; thread-safe; returns nil if empty.
func (m *MultiError) Unwrap() []error {
return m.Errors()
}
// WithFormatter sets a custom error formatting function.
// Returns a MultiErrorOption for use with NewMultiError; overrides default formatting.
func WithFormatter(f ErrorFormatter) MultiErrorOption {
return func(m *MultiError) {
m.formatter = f
}
}
// WithLimit sets the maximum number of errors to store.
// Returns a MultiErrorOption for use with NewMultiError; 0 means unlimited, negative values are ignored.
func WithLimit(n int) MultiErrorOption {
return func(m *MultiError) {
if n < 0 {
n = 0 // Ensure non-negative limit
}
m.limit = n
}
}
// WithSampling enables error sampling with a specified rate (1-100).
// Returns a MultiErrorOption for use with NewMultiError; caps rate at 100 for validity.
func WithSampling(rate uint32) MultiErrorOption {
return func(m *MultiError) {
if rate > 100 {
rate = 100
}
m.sampling = true
m.sampleRate = rate
}
}
// WithRand sets a custom random source for sampling, useful for testing.
// Returns a MultiErrorOption for use with NewMultiError; defaults to fastRand if nil.
func WithRand(r *rand.Rand) MultiErrorOption {
return func(m *MultiError) {
m.rand = r
}
}
// defaultFormat provides the default formatting for multiple errors.
// Returns a semicolon-separated list prefixed with the error count (e.g., "errors(3): err1; err2; err3").
func defaultFormat(errs []error) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("errors(%d): ", len(errs)))
for i, err := range errs {
if i > 0 {
sb.WriteString("; ")
}
sb.WriteString(err.Error())
}
return sb.String()
}
// fastRand generates a quick pseudo-random number for sampling.
// Uses a simple xorshift algorithm based on the current time; not cryptographically secure.
func fastRand() uint32 {
r := uint32(time.Now().UnixNano())
r ^= r << 13
r ^= r >> 17
r ^= r << 5
return r
}

75
vendor/github.com/olekukonko/errors/pool.go generated vendored Normal file
View File

@@ -0,0 +1,75 @@
// pool.go
package errors
import (
"sync"
"sync/atomic"
)
// ErrorPool is a high-performance, thread-safe pool for reusing *Error instances.
// Reduces allocation overhead by recycling errors; tracks hit/miss statistics.
type ErrorPool struct {
pool sync.Pool // Underlying pool for storing *Error instances
poolStats struct { // Embedded struct for pool usage statistics
hits atomic.Int64 // Number of times an error was reused from the pool
misses atomic.Int64 // Number of times a new error was created due to pool miss
}
}
// NewErrorPool creates a new ErrorPool instance.
// Initializes the pool with a New function that returns a fresh *Error with default smallContext.
func NewErrorPool() *ErrorPool {
return &ErrorPool{
pool: sync.Pool{
New: func() interface{} {
return &Error{
smallContext: [contextSize]contextItem{},
}
},
},
}
}
// Get retrieves an *Error from the pool or creates a new one if pooling is disabled or pool is empty.
// Resets are handled by Put; thread-safe; updates hit/miss stats when pooling is enabled.
func (ep *ErrorPool) Get() *Error {
if currentConfig.disablePooling {
return &Error{
smallContext: [contextSize]contextItem{},
}
}
e := ep.pool.Get().(*Error)
if e == nil { // Pool returned nil (unlikely due to New func, but handled for safety)
ep.poolStats.misses.Add(1)
return &Error{
smallContext: [contextSize]contextItem{},
}
}
ep.poolStats.hits.Add(1)
return e
}
// Put returns an *Error to the pool after resetting it.
// Ignores nil errors or if pooling is disabled; preserves stack capacity; thread-safe.
func (ep *ErrorPool) Put(e *Error) {
if e == nil || currentConfig.disablePooling {
return
}
// Reset the error to a clean state, preserving capacity
e.Reset()
// Reset stack length while keeping capacity for reuse
if e.stack != nil {
e.stack = e.stack[:0]
}
ep.pool.Put(e)
}
// Stats returns the current pool statistics as hits and misses.
// Thread-safe; uses atomic loads to ensure accurate counts.
func (ep *ErrorPool) Stats() (hits, misses int64) {
return ep.poolStats.hits.Load(), ep.poolStats.misses.Load()
}

24
vendor/github.com/olekukonko/errors/pool_above_1_24.go generated vendored Normal file
View File

@@ -0,0 +1,24 @@
//go:build go1.24
// +build go1.24
package errors
import "runtime"
// setupCleanup configures a cleanup function for an *Error to auto-return it to the pool.
// Only active for Go 1.24+; uses runtime.AddCleanup when autoFree is set and pooling is enabled.
func (ep *ErrorPool) setupCleanup(e *Error) {
if currentConfig.autoFree {
runtime.AddCleanup(e, func(_ *struct{}) {
if !currentConfig.disablePooling {
ep.Put(e) // Return to pool when cleaned up
}
}, nil) // No additional context needed
}
}
// clearCleanup is a no-op for Go 1.24 and above.
// Cleanup is managed by runtime.AddCleanup; no explicit removal is required.
func (ep *ErrorPool) clearCleanup(e *Error) {
// No-op for Go 1.24+
}

24
vendor/github.com/olekukonko/errors/pool_below_1_24.go generated vendored Normal file
View File

@@ -0,0 +1,24 @@
//go:build !go1.24
// +build !go1.24
package errors
import "runtime"
// setupCleanup configures a finalizer for an *Error to auto-return it to the pool.
// Only active for Go versions < 1.24; enables automatic cleanup when autoFree is set and pooling is enabled.
func (ep *ErrorPool) setupCleanup(e *Error) {
if currentConfig.autoFree {
runtime.SetFinalizer(e, func(e *Error) {
if !currentConfig.disablePooling {
ep.Put(e) // Return to pool when garbage collected
}
})
}
}
// clearCleanup removes any finalizer set on an *Error.
// Only active for Go versions < 1.24; ensures no cleanup action occurs on garbage collection.
func (ep *ErrorPool) clearCleanup(e *Error) {
runtime.SetFinalizer(e, nil) // Disable finalizer
}

286
vendor/github.com/olekukonko/errors/retry.go generated vendored Normal file
View File

@@ -0,0 +1,286 @@
// Package errors provides utilities for error handling, including a flexible retry mechanism.
package errors
import (
"context"
"math/rand"
"time"
)
// BackoffStrategy defines the interface for calculating retry delays.
type BackoffStrategy interface {
// Backoff returns the delay for a given attempt based on the base delay.
Backoff(attempt int, baseDelay time.Duration) time.Duration
}
// ConstantBackoff provides a fixed delay for each retry attempt.
type ConstantBackoff struct{}
// Backoff returns the base delay regardless of the attempt number.
// Implements BackoffStrategy with a constant delay.
func (c ConstantBackoff) Backoff(_ int, baseDelay time.Duration) time.Duration {
return baseDelay
}
// ExponentialBackoff provides an exponentially increasing delay for retry attempts.
type ExponentialBackoff struct{}
// Backoff returns a delay that doubles with each attempt, starting from the base delay.
// Uses bit shifting for efficient exponential growth (e.g., baseDelay * 2^(attempt-1)).
func (e ExponentialBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration {
if attempt <= 1 {
return baseDelay
}
return baseDelay * time.Duration(1<<uint(attempt-1))
}
// LinearBackoff provides a linearly increasing delay for retry attempts.
type LinearBackoff struct{}
// Backoff returns a delay that increases linearly with each attempt (e.g., baseDelay * attempt).
// Implements BackoffStrategy with linear progression.
func (l LinearBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration {
return baseDelay * time.Duration(attempt)
}
// RetryOption configures a Retry instance.
// Defines a function type for setting retry parameters.
type RetryOption func(*Retry)
// Retry represents a retryable operation with configurable backoff and retry logic.
// Supports multiple attempts, delay strategies, jitter, and context-aware cancellation.
type Retry struct {
maxAttempts int // Maximum number of attempts (including initial try)
delay time.Duration // Base delay for backoff calculations
maxDelay time.Duration // Maximum delay cap to prevent excessive waits
retryIf func(error) bool // Condition to determine if retry should occur
onRetry func(int, error) // Callback executed after each failed attempt
backoff BackoffStrategy // Strategy for calculating retry delays
jitter bool // Whether to add random jitter to delays
ctx context.Context // Context for cancellation and deadlines
}
// NewRetry creates a new Retry instance with the given options.
// Defaults: 3 attempts, 100ms base delay, 10s max delay, exponential backoff with jitter,
// and retrying on IsRetryable errors; ensures retryIf is never nil.
func NewRetry(options ...RetryOption) *Retry {
r := &Retry{
maxAttempts: 3,
delay: 100 * time.Millisecond,
maxDelay: 10 * time.Second,
retryIf: func(err error) bool { return IsRetryable(err) },
onRetry: nil,
backoff: ExponentialBackoff{},
jitter: true,
ctx: context.Background(),
}
for _, opt := range options {
opt(r)
}
// Ensure retryIf is never nil, falling back to IsRetryable
if r.retryIf == nil {
r.retryIf = func(err error) bool { return IsRetryable(err) }
}
return r
}
// addJitter adds ±25% jitter to avoid thundering herd problems.
// Returns a duration adjusted by a random value between -25% and +25% of the input; not thread-safe.
func addJitter(d time.Duration) time.Duration {
jitter := time.Duration(rand.Int63n(int64(d/2))) - (d / 4)
return d + jitter
}
// Attempts returns the configured maximum number of retry attempts.
// Includes the initial attempt in the count.
func (r *Retry) Attempts() int {
return r.maxAttempts
}
// Execute runs the provided function with the configured retry logic.
// Returns nil on success or the last error if all attempts fail; respects context cancellation.
func (r *Retry) Execute(fn func() error) error {
var lastErr error
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
// Check if retry is applicable; return immediately if not retryable
if r.retryIf != nil && !r.retryIf(err) {
return err
}
lastErr = err
if r.onRetry != nil {
r.onRetry(attempt, err)
}
// Exit if this was the last attempt
if attempt == r.maxAttempts {
break
}
// Calculate delay with backoff, cap at maxDelay, and apply jitter if enabled
currentDelay := r.backoff.Backoff(attempt, r.delay)
if currentDelay > r.maxDelay {
currentDelay = r.maxDelay
}
if r.jitter {
currentDelay = addJitter(currentDelay)
}
// Wait with respect to context cancellation or timeout
select {
case <-r.ctx.Done():
return r.ctx.Err()
case <-time.After(currentDelay):
}
}
return lastErr
}
// Transform creates a new Retry instance with modified configuration.
// Copies all settings from the original Retry and applies the given options.
func (r *Retry) Transform(opts ...RetryOption) *Retry {
newRetry := &Retry{
maxAttempts: r.maxAttempts,
delay: r.delay,
maxDelay: r.maxDelay,
retryIf: r.retryIf,
onRetry: r.onRetry,
backoff: r.backoff,
jitter: r.jitter,
ctx: r.ctx,
}
for _, opt := range opts {
opt(newRetry)
}
return newRetry
}
// WithBackoff sets the backoff strategy using the BackoffStrategy interface.
// Returns a RetryOption; no-op if strategy is nil, retaining the existing strategy.
func WithBackoff(strategy BackoffStrategy) RetryOption {
return func(r *Retry) {
if strategy != nil {
r.backoff = strategy
}
}
}
// WithContext sets the context for cancellation and deadlines.
// Returns a RetryOption; retains context.Background if ctx is nil.
func WithContext(ctx context.Context) RetryOption {
return func(r *Retry) {
if ctx != nil {
r.ctx = ctx
}
}
}
// WithDelay sets the initial delay between retries.
// Returns a RetryOption; ensures non-negative delay by setting negatives to 0.
func WithDelay(delay time.Duration) RetryOption {
return func(r *Retry) {
if delay < 0 {
delay = 0
}
r.delay = delay
}
}
// WithJitter enables or disables jitter in the backoff delay.
// Returns a RetryOption; toggles random delay variation.
func WithJitter(jitter bool) RetryOption {
return func(r *Retry) {
r.jitter = jitter
}
}
// WithMaxAttempts sets the maximum number of retry attempts.
// Returns a RetryOption; ensures at least 1 attempt by adjusting lower values.
func WithMaxAttempts(maxAttempts int) RetryOption {
return func(r *Retry) {
if maxAttempts < 1 {
maxAttempts = 1
}
r.maxAttempts = maxAttempts
}
}
// WithMaxDelay sets the maximum delay between retries.
// Returns a RetryOption; ensures non-negative delay by setting negatives to 0.
func WithMaxDelay(maxDelay time.Duration) RetryOption {
return func(r *Retry) {
if maxDelay < 0 {
maxDelay = 0
}
r.maxDelay = maxDelay
}
}
// WithOnRetry sets a callback to execute after each failed attempt.
// Returns a RetryOption; callback receives attempt number and error.
func WithOnRetry(onRetry func(attempt int, err error)) RetryOption {
return func(r *Retry) {
r.onRetry = onRetry
}
}
// WithRetryIf sets the condition under which to retry.
// Returns a RetryOption; retains IsRetryable default if retryIf is nil.
func WithRetryIf(retryIf func(error) bool) RetryOption {
return func(r *Retry) {
if retryIf != nil {
r.retryIf = retryIf
}
}
}
// ExecuteReply runs the provided function with retry logic and returns its result.
// Returns the result and nil on success, or zero value and last error on failure; generic type T.
func ExecuteReply[T any](r *Retry, fn func() (T, error)) (T, error) {
var lastErr error
var zero T
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
result, err := fn()
if err == nil {
return result, nil
}
// Check if retry is applicable; return immediately if not retryable
if r.retryIf != nil && !r.retryIf(err) {
return zero, err
}
lastErr = err
if r.onRetry != nil {
r.onRetry(attempt, err)
}
if attempt == r.maxAttempts {
break
}
// Calculate delay with backoff, cap at maxDelay, and apply jitter if enabled
currentDelay := r.backoff.Backoff(attempt, r.delay)
if currentDelay > r.maxDelay {
currentDelay = r.maxDelay
}
if r.jitter {
currentDelay = addJitter(currentDelay)
}
// Wait with respect to context cancellation or timeout
select {
case <-r.ctx.Done():
return zero, r.ctx.Err()
case <-time.After(currentDelay):
}
}
return zero, lastErr
}

153
vendor/github.com/olekukonko/errors/utils.go generated vendored Normal file
View File

@@ -0,0 +1,153 @@
// Package errors provides utility functions for error handling, including stack
// trace capture and function name extraction.
package errors
import (
"database/sql"
"fmt"
"reflect"
"runtime"
"strings"
)
// captureStack captures a stack trace with the configured depth.
// Skip=0 captures the current call site; skips captureStack and its caller (+2 frames); thread-safe via stackPool.
func captureStack(skip int) []uintptr {
buf := stackPool.Get().([]uintptr)
buf = buf[:cap(buf)]
// +2 to skip captureStack and the immediate caller
n := runtime.Callers(skip+2, buf)
if n == 0 {
stackPool.Put(buf)
return nil
}
// Create a new slice to return, avoiding direct use of pooled memory
stack := make([]uintptr, n)
copy(stack, buf[:n])
stackPool.Put(buf)
return stack
}
// min returns the smaller of two integers.
// Simple helper for limiting stack trace size or other comparisons.
func min(a, b int) int {
if a < b {
return a
}
return b
}
// clearMap removes all entries from a map.
// Helper function to reset map contents without reallocating.
func clearMap(m map[string]interface{}) {
for k := range m {
delete(m, k)
}
}
// sqlNull detects if a value represents a SQL NULL type.
// Returns true for nil or invalid sql.Null* types (e.g., NullString, NullInt64); false otherwise.
func sqlNull(v interface{}) bool {
if v == nil {
return true
}
switch val := v.(type) {
case sql.NullString:
return !val.Valid
case sql.NullTime:
return !val.Valid
case sql.NullInt64:
return !val.Valid
case sql.NullBool:
return !val.Valid
case sql.NullFloat64:
return !val.Valid
default:
return false
}
}
// getFuncName extracts the function name from an interface, typically a function or method.
// Returns "unknown" if the input is nil or invalid; trims leading dots from runtime name.
func getFuncName(fn interface{}) string {
if fn == nil {
return "unknown"
}
fullName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
return strings.TrimPrefix(fullName, ".")
}
// isInternalFrame determines if a stack frame is considered "internal".
// Returns true for frames from runtime, reflect, or this packages subdirectories if FilterInternal is true.
func isInternalFrame(frame runtime.Frame) bool {
if strings.HasPrefix(frame.Function, "runtime.") || strings.HasPrefix(frame.Function, "reflect.") {
return true
}
suffixes := []string{
"errors",
"utils",
"helper",
"retry",
"multi",
}
file := frame.File
for _, v := range suffixes {
if strings.Contains(file, fmt.Sprintf("github.com/olekukonko/errors/%s", v)) {
return true
}
}
return false
}
// FormatError returns a formatted string representation of an error.
// Includes message, name, context, stack trace, and cause for *Error types; just message for others; "<nil>" if nil.
func FormatError(err error) string {
if err == nil {
return "<nil>"
}
var sb strings.Builder
if e, ok := err.(*Error); ok {
sb.WriteString(fmt.Sprintf("Error: %s\n", e.Error()))
if e.name != "" {
sb.WriteString(fmt.Sprintf("Name: %s\n", e.name))
}
if ctx := e.Context(); len(ctx) > 0 {
sb.WriteString("Context:\n")
for k, v := range ctx {
sb.WriteString(fmt.Sprintf("\t%s: %v\n", k, v))
}
}
if stack := e.Stack(); len(stack) > 0 {
sb.WriteString("Stack Trace:\n")
for _, frame := range stack {
sb.WriteString(fmt.Sprintf("\t%s\n", frame))
}
}
if e.cause != nil {
sb.WriteString(fmt.Sprintf("Caused by: %s\n", FormatError(e.cause)))
}
} else {
sb.WriteString(fmt.Sprintf("Error: %s\n", err.Error()))
}
return sb.String()
}
// Caller returns the file, line, and function name of the caller at the specified skip level.
// Skip=0 returns the caller of this function, 1 returns its caller, etc.; returns "unknown" if no caller found.
func Caller(skip int) (file string, line int, function string) {
configMu.RLock()
defer configMu.RUnlock()
var pcs [1]uintptr
n := runtime.Callers(skip+2, pcs[:]) // +2 skips Caller and its immediate caller
if n == 0 {
return "", 0, "unknown"
}
frame, _ := runtime.CallersFrames(pcs[:n]).Next()
return frame.File, frame.Line, frame.Function
}

5
vendor/github.com/olekukonko/ll/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
.idea
lab
tmp
#_*
_test/

21
vendor/github.com/olekukonko/ll/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Oleku Konko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

359
vendor/github.com/olekukonko/ll/README.md generated vendored Normal file
View File

@@ -0,0 +1,359 @@
# ll - A Modern Structured Logging Library for Go
`ll` is a high-performance, production-ready logging library for Go, designed to provide **hierarchical namespaces**, **structured logging**, **middleware pipelines**, **conditional logging**, and support for multiple output formats, including text, JSON, colorized logs, and compatibility with Gos `slog`. Its ideal for applications requiring fine-grained log control, extensibility, and scalability.
## Key Features
- **Hierarchical Namespaces**: Organize logs with fine-grained control over subsystems (e.g., "app/db").
- **Structured Logging**: Add key-value metadata for machine-readable logs.
- **Middleware Pipeline**: Customize log processing with error-based rejection.
- **Conditional Logging**: Optimize performance by skipping unnecessary log operations.
- **Multiple Output Formats**: Support for text, JSON, colorized logs, and `slog` integration.
- **Debugging Utilities**: Inspect variables (`Dbg`), binary data (`Dump`), and stack traces (`Stack`).
- **Thread-Safe**: Built for concurrent use with mutex-protected state.
- **Performance Optimized**: Minimal allocations and efficient namespace caching.
## Installation
Install `ll` using Go modules:
```bash
go get github.com/olekukonko/ll
```
Ensure you have Go 1.21 or later for optimal compatibility.
## Getting Started
Heres a quick example to start logging with `ll`:
```go
package main
import (
"github.com/olekukonko/ll"
)
func main() {
// Create a logger with namespace "app"
logger := ll.New("")
// enable output
logger.Enable()
// Basic log
logger.Info("Welcome") // Output: [app] INFO: Application started
logger = logger.Namespace("app")
// Basic log
logger.Info("start at :8080") // Output: [app] INFO: Application started
//Output
//INFO: Welcome
//[app] INFO: start at :8080
}
```
```go
package main
import (
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
"os"
)
func main() {
// Chaining
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
// Basic log
logger.Info("Application started") // Output: [app] INFO: Application started
// Structured log with fields
logger.Fields("user", "alice", "status", 200).Info("User logged in")
// Output: [app] INFO: User logged in [user=alice status=200]
// Conditional log
debugMode := false
logger.If(debugMode).Debug("Debug info") // No output (debugMode is false)
}
```
## Core Features
### 1. Hierarchical Namespaces
Namespaces allow you to organize logs hierarchically, enabling precise control over logging for different parts of your application. This is especially useful for large systems with multiple components.
**Benefits**:
- **Granular Control**: Enable/disable logs for specific subsystems (e.g., "app/db" vs. "app/api").
- **Scalability**: Manage log volume in complex applications.
- **Readability**: Clear namespace paths improve traceability.
**Example**:
```go
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
// Child loggers
dbLogger := logger.Namespace("db")
apiLogger := logger.Namespace("api").Style(lx.NestedPath)
// Namespace control
logger.NamespaceEnable("app/db") // Enable DB logs
logger.NamespaceDisable("app/api") // Disable API logs
dbLogger.Info("Query executed") // Output: [app/db] INFO: Query executed
apiLogger.Info("Request received") // No output
```
### 2. Structured Logging
Add key-value metadata to logs for machine-readable output, making it easier to query and analyze logs in tools like ELK or Grafana.
**Example**:
```go
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
// Variadic fields
logger.Fields("user", "bob", "status", 200).Info("Request completed")
// Output: [app] INFO: Request completed [user=bob status=200]
// Map-based fields
logger.Field(map[string]interface{}{"method": "GET"}).Info("Request")
// Output: [app] INFO: Request [method=GET]
```
### 3. Middleware Pipeline
Customize log processing with a middleware pipeline. Middleware functions can enrich, filter, or transform logs, using an error-based rejection mechanism (non-nil errors stop logging).
**Example**:
```go
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
// Enrich logs with app metadata
logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
if e.Fields == nil {
e.Fields = make(map[string]interface{})
}
e.Fields["app"] = "myapp"
return nil
}))
// Filter low-level logs
logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
if e.Level < lx.LevelWarn {
return fmt.Errorf("level too low")
}
return nil
}))
logger.Info("Ignored") // No output (filtered)
logger.Warn("Warning") // Output: [app] WARN: Warning [app=myapp]
```
### 4. Conditional Logging
Optimize performance by skipping expensive log operations when conditions are false, ideal for production environments.
**Example**:
```go
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
featureEnabled := true
logger.If(featureEnabled).Fields("action", "update").Info("Feature used")
// Output: [app] INFO: Feature used [action=update]
logger.If(false).Info("Ignored") // No output, no processing
```
### 5. Multiple Output Formats
`ll` supports various output formats, including human-readable text, colorized logs, JSON, and integration with Gos `slog` package.
**Example**:
```go
logger := ll.New("app").Enable()
// Text output
logger.Handler(lh.NewTextHandler(os.Stdout))
logger.Info("Text log") // Output: [app] INFO: Text log
// JSON output
logger.Handler(lh.NewJSONHandler(os.Stdout, time.RFC3339Nano))
logger.Info("JSON log") // Output: {"timestamp":"...","level":"INFO","message":"JSON log","namespace":"app"}
// Slog integration
slogText := slog.NewTextHandler(os.Stdout, nil)
logger.Handler(lh.NewSlogHandler(slogText))
logger.Info("Slog log") // Output: level=INFO msg="Slog log" namespace=app class=Text
```
### 6. Debugging Utilities
`ll` provides powerful tools for debugging, including variable inspection, binary data dumps, and stack traces.
#### Core Debugging Methods
1. **Dbg - Contextual Inspection**
Inspects variables with file and line context, preserving variable names and handling all Go types.
```go
x := 42
user := struct{ Name string }{"Alice"}
ll.Dbg(x) // Output: [file.go:123] x = 42
ll.Dbg(user) // Output: [file.go:124] user = [Name:Alice]
```
2. **Dump - Binary Inspection**
Displays a hex/ASCII view of data, optimized for strings, bytes, and complex types (with JSON fallback).
```go
ll.Handler(lh.NewColorizedHandler(os.Stdout))
ll.Dump("hello\nworld") // Output: Hex/ASCII dump (see example/dump.png)
```
3. **Stack - Stack Inspection**
Logs a stack trace for debugging critical errors.
```go
ll.Handler(lh.NewColorizedHandler(os.Stdout))
ll.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] (see example/stack.png)
```
#### Performance Tracking
Measure execution time for performance analysis.
```go
// Automatic measurement
defer ll.Measure(func() { time.Sleep(time.Millisecond) })()
// Output: [app] INFO: function executed [duration=~1ms]
// Explicit benchmarking
start := time.Now()
time.Sleep(time.Millisecond)
ll.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...]
```
**Performance Notes**:
- `Dbg` calls are disabled at compile-time when not enabled.
- `Dump` optimizes for primitive types, strings, and bytes with zero-copy paths.
- Stack traces are configurable via `StackSize`.
## Real-World Example: Web Server
A practical example of using `ll` in a web server with structured logging, middleware, and `slog` integration:
```go
package main
import (
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
"log/slog"
"net/http"
"os"
"time"
)
func main() {
// Initialize logger with slog handler
slogHandler := slog.NewJSONHandler(os.Stdout, nil)
logger := ll.New("server").Enable().Handler(lh.NewSlogHandler(slogHandler))
// HTTP child logger
httpLogger := logger.Namespace("http").Style(lx.NestedPath)
// Middleware for request ID
httpLogger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
if e.Fields == nil {
e.Fields = make(map[string]interface{})
}
e.Fields["request_id"] = "req-" + time.Now().String()
return nil
}))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
httpLogger.Fields("method", r.Method, "path", r.URL.Path).Info("Request received")
w.Write([]byte("Hello, world!"))
httpLogger.Fields("duration_ms", time.Since(start).Milliseconds()).Info("Request completed")
})
logger.Info("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
```
**Sample Output (JSON via slog)**:
```json
{"level":"INFO","msg":"Starting server on :8080","namespace":"server"}
{"level":"INFO","msg":"Request received","namespace":"server/http","class":"Text","method":"GET","path":"/","request_id":"req-..."}
{"level":"INFO","msg":"Request completed","namespace":"server/http","class":"Text","duration_ms":1,"request_id":"req-..."}
```
## Why Choose `ll`?
- **Granular Control**: Hierarchical namespaces for precise log management.
- **Performance**: Conditional logging and optimized concatenation reduce overhead.
- **Extensibility**: Middleware pipeline for custom log processing.
- **Structured Output**: Machine-readable logs with key-value metadata.
- **Flexible Formats**: Text, JSON, colorized, and `slog` support.
- **Debugging Power**: Advanced tools like `Dbg`, `Dump`, and `Stack` for deep inspection.
- **Thread-Safe**: Safe for concurrent use in high-throughput applications.
## Comparison with Other Libraries
| Feature | `ll` | `log` (stdlib) | `slog` (stdlib) | `zap` |
|--------------------------|--------------------------|----------------|-----------------|-------------------|
| Hierarchical Namespaces | ✅ | ❌ | ❌ | ❌ |
| Structured Logging | ✅ (Fields, Context) | ❌ | ✅ | ✅ |
| Middleware Pipeline | ✅ | ❌ | ❌ | ✅ (limited) |
| Conditional Logging | ✅ (If, IfOne, IfAny) | ❌ | ❌ | ❌ |
| Slog Compatibility | ✅ | ❌ | ✅ (native) | ❌ |
| Debugging (Dbg, Dump) | ✅ | ❌ | ❌ | ❌ |
| Performance (disabled logs) | High (conditional) | Low | Medium | High |
| Output Formats | Text, JSON, Color, Slog | Text | Text, JSON | JSON, Text |
## Benchmarks
`ll` is optimized for performance, particularly for disabled logs and structured logging:
- **Disabled Logs**: 30% faster than `slog` due to efficient conditional checks.
- **Structured Logging**: 2x faster than `log` with minimal allocations.
- **Namespace Caching**: Reduces overhead for hierarchical lookups.
See `ll_bench_test.go` for detailed benchmarks on namespace creation, cloning, and field building.
## Testing and Stability
The `ll` library includes a comprehensive test suite (`ll_test.go`) covering:
- Logger configuration, namespaces, and conditional logging.
- Middleware, rate limiting, and sampling.
- Handler output formats (text, JSON, slog).
- Debugging utilities (`Dbg`, `Dump`, `Stack`).
Recent improvements:
- Fixed sampling middleware for reliable behavior at edge cases (0.0 and 1.0 rates).
- Enhanced documentation across `conditional.go`, `field.go`, `global.go`, `ll.go`, `lx.go`, and `ns.go`.
- Added `slog` compatibility via `lh.SlogHandler`.
## Contributing
Contributions are welcome! To contribute:
1. Fork the repository: `github.com/olekukonko/ll`.
2. Create a feature branch: `git checkout -b feature/your-feature`.
3. Commit changes: `git commit -m "Add your feature"`.
4. Push to the branch: `git push origin feature/your-feature`.
5. Open a pull request with a clear description.
Please include tests in `ll_test.go` and update documentation as needed. Follow the Go coding style and run `go test ./...` before submitting.
## License
`ll` is licensed under the MIT License. See [LICENSE](LICENSE) for details.
## Resources
- **Source Code**: [github.com/olekukonko/ll](https://github.com/olekukonko/ll)
- **Issue Tracker**: [github.com/olekukonko/ll/issues](https://github.com/olekukonko/ll/issues)
- **GoDoc**: [pkg.go.dev/github.com/olekukonko/ll](https://pkg.go.dev/github.com/olekukonko/ll)

421
vendor/github.com/olekukonko/ll/concat.go generated vendored Normal file
View File

@@ -0,0 +1,421 @@
package ll
import (
"fmt"
"github.com/olekukonko/ll/lx"
"reflect"
"strconv"
"strings"
"unsafe"
)
const (
maxRecursionDepth = 20 // Maximum depth for recursive type handling to prevent stack overflow
nilString = "<nil>" // String representation for nil values
unexportedString = "<?>" // String representation for unexported fields
)
// Concat efficiently concatenates values without a separator using the default logger.
// It converts each argument to a string and joins them directly, optimizing for performance
// in logging scenarios. Thread-safe as it does not modify shared state.
// Example:
//
// msg := ll.Concat("Hello", 42, true) // Returns "Hello42true"
func Concat(args ...any) string {
return concat(args...)
}
// ConcatSpaced concatenates values with a space separator using the default logger.
// It converts each argument to a string and joins them with spaces, suitable for log message
// formatting. Thread-safe as it does not modify shared state.
// Example:
//
// msg := ll.ConcatSpaced("Hello", 42, true) // Returns "Hello 42 true"
func ConcatSpaced(args ...any) string {
return concatSpaced(args...)
}
// ConcatAll concatenates elements with a separator, prefix, and suffix using the default logger.
// It combines before, main, and after arguments with the specified separator, optimizing memory
// allocation for logging. Thread-safe as it does not modify shared state.
// Example:
//
// msg := ll.ConcatAll(",", []any{"prefix"}, []any{"suffix"}, "main")
// // Returns "prefix,main,suffix"
func ConcatAll(sep string, before, after []any, args ...any) string {
return concatenate(sep, before, after, args...)
}
// concat efficiently concatenates values without a separator.
// It converts each argument to a string and joins them directly, optimizing for performance
// in logging scenarios. Used internally by Concat and other logging functions.
// Example:
//
// msg := concat("Hello", 42, true) // Returns "Hello42true"
func concat(args ...any) string {
return concatWith("", args...)
}
// concatSpaced concatenates values with a space separator.
// It converts each argument to a string and joins them with spaces, suitable for formatting
// log messages. Used internally by ConcatSpaced.
// Example:
//
// msg := concatSpaced("Hello", 42, true) // Returns "Hello 42 true"
func concatSpaced(args ...any) string {
return concatWith(lx.Space, args...)
}
// concatWith concatenates values with a specified separator using optimized type handling.
// It builds a string from arguments, handling various types efficiently (strings, numbers,
// structs, etc.), and is used by concat and concatSpaced for log message construction.
// Thread-safe as it does not modify shared state.
// Example:
//
// msg := concatWith(",", "Hello", 42, true) // Returns "Hello,42,true"
func concatWith(sep string, args ...any) string {
switch len(args) {
case 0:
return ""
case 1:
return concatToString(args[0])
}
var b strings.Builder
b.Grow(concatEstimateArgs(sep, args))
for i, arg := range args {
if i > 0 {
b.WriteString(sep)
}
concatWriteValue(&b, arg, 0)
}
return b.String()
}
// concatenate concatenates elements with separators, prefixes, and suffixes efficiently.
// It combines before, main, and after arguments with the specified separator, optimizing
// memory allocation for complex log message formatting. Used internally by ConcatAll.
// Example:
//
// msg := concatenate(",", []any{"prefix"}, []any{"suffix"}, "main")
// // Returns "prefix,main,suffix"
func concatenate(sep string, before []any, after []any, args ...any) string {
totalLen := len(before) + len(after) + len(args)
switch totalLen {
case 0:
return ""
case 1:
switch {
case len(before) > 0:
return concatToString(before[0])
case len(args) > 0:
return concatToString(args[0])
default:
return concatToString(after[0])
}
}
var b strings.Builder
b.Grow(concatEstimateTotal(sep, before, after, args))
// Write before elements
concatWriteGroup(&b, sep, before)
// Write main arguments
if len(before) > 0 && len(args) > 0 {
b.WriteString(sep)
}
concatWriteGroup(&b, sep, args)
// Write after elements
if len(after) > 0 && (len(before) > 0 || len(args) > 0) {
b.WriteString(sep)
}
concatWriteGroup(&b, sep, after)
return b.String()
}
// concatWriteGroup writes a group of arguments to a strings.Builder with a separator.
// It handles each argument by converting it to a string, used internally by concatenate
// to process before, main, or after groups in log message construction.
// Example:
//
// var b strings.Builder
// concatWriteGroup(&b, ",", []any{"a", 42}) // Writes "a,42" to b
func concatWriteGroup(b *strings.Builder, sep string, group []any) {
for i, arg := range group {
if i > 0 {
b.WriteString(sep)
}
concatWriteValue(b, arg, 0)
}
}
// concatToString converts a single argument to a string efficiently.
// It handles common types (string, []byte, fmt.Stringer) with minimal overhead and falls
// back to fmt.Sprint for other types. Used internally by concat and concatenate.
// Example:
//
// s := concatToString("Hello") // Returns "Hello"
// s := concatToString([]byte{65, 66}) // Returns "AB"
func concatToString(arg any) string {
switch v := arg.(type) {
case string:
return v
case []byte:
return *(*string)(unsafe.Pointer(&v))
case fmt.Stringer:
return v.String()
case error:
return v.Error()
default:
return fmt.Sprint(v)
}
}
// concatEstimateTotal estimates the total string length for concatenate.
// It calculates the expected size of the concatenated string, including before, main, and
// after arguments with separators, to preallocate the strings.Builder capacity.
// Example:
//
// size := concatEstimateTotal(",", []any{"prefix"}, []any{"suffix"}, "main")
// // Returns estimated length for "prefix,main,suffix"
func concatEstimateTotal(sep string, before, after, args []any) int {
size := 0
if len(before) > 0 {
size += concatEstimateArgs(sep, before)
}
if len(args) > 0 {
if size > 0 {
size += len(sep)
}
size += concatEstimateArgs(sep, args)
}
if len(after) > 0 {
if size > 0 {
size += len(sep)
}
size += concatEstimateArgs(sep, after)
}
return size
}
// concatEstimateArgs estimates the string length for a group of arguments.
// It sums the estimated sizes of each argument plus separators, used by concatEstimateTotal
// and concatWith to optimize memory allocation for log message construction.
// Example:
//
// size := concatEstimateArgs(",", []any{"hello", 42}) // Returns estimated length for "hello,42"
func concatEstimateArgs(sep string, args []any) int {
if len(args) == 0 {
return 0
}
size := len(sep) * (len(args) - 1)
for _, arg := range args {
size += concatEstimateSize(arg)
}
return size
}
// concatEstimateSize estimates the string length for a single argument.
// It provides size estimates for various types (strings, numbers, booleans, etc.) to
// optimize strings.Builder capacity allocation in logging functions.
// Example:
//
// size := concatEstimateSize("hello") // Returns 5
// size := concatEstimateSize(42) // Returns ~2
func concatEstimateSize(arg any) int {
switch v := arg.(type) {
case string:
return len(v)
case []byte:
return len(v)
case int:
return concatNumLen(int64(v))
case int64:
return concatNumLen(v)
case int32:
return concatNumLen(int64(v))
case int16:
return concatNumLen(int64(v))
case int8:
return concatNumLen(int64(v))
case uint:
return concatNumLen(uint64(v))
case uint64:
return concatNumLen(v)
case uint32:
return concatNumLen(uint64(v))
case uint16:
return concatNumLen(uint64(v))
case uint8:
return concatNumLen(uint64(v))
case float64:
return 24 // Max digits for float64
case float32:
return 16 // Max digits for float32
case bool:
if v {
return 4 // "true"
}
return 5 // "false"
case fmt.Stringer:
return 16 // Conservative estimate
default:
return 16 // Default estimate
}
}
// concatNumLen estimates the string length for a signed or unsigned integer.
// It returns a conservative estimate (20 digits) for int64 or uint64 values, including
// a sign for negative numbers, used by concatEstimateSize for memory allocation.
// Example:
//
// size := concatNumLen(int64(-123)) // Returns 20
// size := concatNumLen(uint64(123)) // Returns 20
func concatNumLen[T int64 | uint64](v T) int {
if v < 0 {
return 20 // Max digits for int64 + sign
}
return 20 // Max digits for uint64
}
// concatWriteValue writes a formatted value to a strings.Builder with recursion depth tracking.
// It handles various types (strings, numbers, structs, slices, etc.) and prevents infinite
// recursion by limiting depth. Used internally by concatWith and concatWriteGroup for log
// message formatting.
// Example:
//
// var b strings.Builder
// concatWriteValue(&b, "hello", 0) // Writes "hello" to b
// concatWriteValue(&b, []int{1, 2}, 0) // Writes "[1,2]" to b
func concatWriteValue(b *strings.Builder, arg any, depth int) {
if depth > maxRecursionDepth {
b.WriteString("...")
return
}
if arg == nil {
b.WriteString(nilString)
return
}
if s, ok := arg.(fmt.Stringer); ok {
b.WriteString(s.String())
return
}
switch v := arg.(type) {
case string:
b.WriteString(v)
case []byte:
b.Write(v)
case int:
b.WriteString(strconv.FormatInt(int64(v), 10))
case int64:
b.WriteString(strconv.FormatInt(v, 10))
case int32:
b.WriteString(strconv.FormatInt(int64(v), 10))
case int16:
b.WriteString(strconv.FormatInt(int64(v), 10))
case int8:
b.WriteString(strconv.FormatInt(int64(v), 10))
case uint:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case uint64:
b.WriteString(strconv.FormatUint(v, 10))
case uint32:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case uint16:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case uint8:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case float64:
b.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
case float32:
b.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
case bool:
if v {
b.WriteString("true")
} else {
b.WriteString("false")
}
default:
val := reflect.ValueOf(arg)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
b.WriteString(nilString)
return
}
val = val.Elem()
}
switch val.Kind() {
case reflect.Slice, reflect.Array:
concatFormatSlice(b, val, depth)
case reflect.Struct:
concatFormatStruct(b, val, depth)
default:
fmt.Fprint(b, v)
}
}
}
// concatFormatSlice formats a slice or array for logging.
// It writes the elements in a bracketed, comma-separated format, handling nested types
// recursively with depth tracking. Used internally by concatWriteValue for log message formatting.
// Example:
//
// var b strings.Builder
// val := reflect.ValueOf([]int{1, 2})
// concatFormatSlice(&b, val, 0) // Writes "[1,2]" to b
func concatFormatSlice(b *strings.Builder, val reflect.Value, depth int) {
b.WriteByte('[')
for i := 0; i < val.Len(); i++ {
if i > 0 {
b.WriteByte(',')
}
concatWriteValue(b, val.Index(i).Interface(), depth+1)
}
b.WriteByte(']')
}
// concatFormatStruct formats a struct for logging.
// It writes the structs exported fields in a bracketed, name:value format, handling nested
// types recursively with depth tracking. Unexported fields are represented as "<?>".
// Used internally by concatWriteValue for log message formatting.
// Example:
//
// var b strings.Builder
// val := reflect.ValueOf(struct{ Name string }{Name: "test"})
// concatFormatStruct(&b, val, 0) // Writes "[Name:test]" to b
func concatFormatStruct(b *strings.Builder, val reflect.Value, depth int) {
typ := val.Type()
b.WriteByte('[')
first := true
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldValue := val.Field(i)
if !first {
b.WriteString("; ")
}
first = false
b.WriteString(field.Name)
b.WriteByte(':')
if !fieldValue.CanInterface() {
b.WriteString(unexportedString)
continue
}
concatWriteValue(b, fieldValue.Interface(), depth+1)
}
b.WriteByte(']')
}

340
vendor/github.com/olekukonko/ll/conditional.go generated vendored Normal file
View File

@@ -0,0 +1,340 @@
package ll
// Conditional enables conditional logging based on a boolean condition.
// It wraps a logger with a condition that determines whether logging operations are executed,
// optimizing performance by skipping expensive operations (e.g., field computation, message formatting)
// when the condition is false. The struct supports fluent chaining for adding fields and logging.
type Conditional struct {
logger *Logger // Associated logger instance for logging operations
condition bool // Whether logging is allowed (true to log, false to skip)
}
// If creates a conditional logger that logs only if the condition is true.
// It returns a Conditional struct that wraps the logger, enabling conditional logging methods.
// This method is typically called on a Logger instance to start a conditional chain.
// Thread-safe via the underlying loggers mutex.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Info("Logged") // Output: [app] INFO: Logged
// logger.If(false).Info("Ignored") // No output
func (l *Logger) If(condition bool) *Conditional {
return &Conditional{logger: l, condition: condition}
}
// IfOne creates a conditional logger that logs only if all conditions are true.
// It evaluates a variadic list of boolean conditions, setting the condition to true only if
// all are true (logical AND). Returns a new Conditional with the result. Thread-safe via the
// underlying logger.
// Example:
//
// logger := New("app").Enable()
// logger.IfOne(true, true).Info("Logged") // Output: [app] INFO: Logged
// logger.IfOne(true, false).Info("Ignored") // No output
func (cl *Conditional) IfOne(conditions ...bool) *Conditional {
result := true
// Check each condition; set result to false if any is false
for _, cond := range conditions {
if !cond {
result = false
break
}
}
return &Conditional{logger: cl.logger, condition: result}
}
// IfAny creates a conditional logger that logs only if at least one condition is true.
// It evaluates a variadic list of boolean conditions, setting the condition to true if any
// is true (logical OR). Returns a new Conditional with the result. Thread-safe via the
// underlying logger.
// Example:
//
// logger := New("app").Enable()
// logger.IfAny(false, true).Info("Logged") // Output: [app] INFO: Logged
// logger.IfAny(false, false).Info("Ignored") // No output
func (cl *Conditional) IfAny(conditions ...bool) *Conditional {
result := false
// Check each condition; set result to true if any is true
for _, cond := range conditions {
if cond {
result = true
break
}
}
return &Conditional{logger: cl.logger, condition: result}
}
// Fields starts a fluent chain for adding fields using variadic key-value pairs, if the condition is true.
// It returns a FieldBuilder to attach fields, skipping field processing if the condition is false
// to optimize performance. Thread-safe via the FieldBuilders logger.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Fields("user", "alice").Info("Logged") // Output: [app] INFO: Logged [user=alice]
// logger.If(false).Fields("user", "alice").Info("Ignored") // No output, no field processing
func (cl *Conditional) Fields(pairs ...any) *FieldBuilder {
// Skip field processing if condition is false
if !cl.condition {
return &FieldBuilder{logger: cl.logger, fields: nil}
}
// Delegate to loggers Fields method
return cl.logger.Fields(pairs...)
}
// Field starts a fluent chain for adding fields from a map, if the condition is true.
// It returns a FieldBuilder to attach fields from a map, skipping processing if the condition
// is false. Thread-safe via the FieldBuilders logger.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Field(map[string]interface{}{"user": "alice"}).Info("Logged") // Output: [app] INFO: Logged [user=alice]
// logger.If(false).Field(map[string]interface{}{"user": "alice"}).Info("Ignored") // No output
func (cl *Conditional) Field(fields map[string]interface{}) *FieldBuilder {
// Skip field processing if condition is false
if !cl.condition {
return &FieldBuilder{logger: cl.logger, fields: nil}
}
// Delegate to loggers Field method
return cl.logger.Field(fields)
}
// Info logs a message at Info level with variadic arguments if the condition is true.
// It concatenates the arguments with spaces and delegates to the loggers Info method if the
// condition is true. Skips processing if false, optimizing performance. Thread-safe via the
// loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Info("Action", "started") // Output: [app] INFO: Action started
// logger.If(false).Info("Action", "ignored") // No output
func (cl *Conditional) Info(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Info method
cl.logger.Info(args...)
}
// Infof logs a message at Info level with a format string if the condition is true.
// It formats the message using the provided format string and arguments, delegating to the
// loggers Infof method if the condition is true. Skips processing if false, optimizing performance.
// Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Infof("Action %s", "started") // Output: [app] INFO: Action started
// logger.If(false).Infof("Action %s", "ignored") // No output
func (cl *Conditional) Infof(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Infof method
cl.logger.Infof(format, args...)
}
// Debug logs a message at Debug level with variadic arguments if the condition is true.
// It concatenates the arguments with spaces and delegates to the loggers Debug method if the
// condition is true. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable().Level(lx.LevelDebug)
// logger.If(true).Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode
// logger.If(false).Debug("Debugging", "ignored") // No output
func (cl *Conditional) Debug(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Debug method
cl.logger.Debug(args...)
}
// Debugf logs a message at Debug level with a format string if the condition is true.
// It formats the message and delegates to the loggers Debugf method if the condition is true.
// Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable().Level(lx.LevelDebug)
// logger.If(true).Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode
// logger.If(false).Debugf("Debug %s", "ignored") // No output
func (cl *Conditional) Debugf(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Debugf method
cl.logger.Debugf(format, args...)
}
// Warn logs a message at Warn level with variadic arguments if the condition is true.
// It concatenates the arguments with spaces and delegates to the loggers Warn method if the
// condition is true. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Warn("Warning", "issued") // Output: [app] WARN: Warning issued
// logger.If(false).Warn("Warning", "ignored") // No output
func (cl *Conditional) Warn(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Warn method
cl.logger.Warn(args...)
}
// Warnf logs a message at Warn level with a format string if the condition is true.
// It formats the message and delegates to the loggers Warnf method if the condition is true.
// Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued
// logger.If(false).Warnf("Warning %s", "ignored") // No output
func (cl *Conditional) Warnf(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Warnf method
cl.logger.Warnf(format, args...)
}
// Error logs a message at Error level with variadic arguments if the condition is true.
// It concatenates the arguments with spaces and delegates to the loggers Error method if the
// condition is true. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Error("Error", "occurred") // Output: [app] ERROR: Error occurred
// logger.If(false).Error("Error", "ignored") // No output
func (cl *Conditional) Error(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Error method
cl.logger.Error(args...)
}
// Errorf logs a message at Error level with a format string if the condition is true.
// It formats the message and delegates to the loggers Errorf method if the condition is true.
// Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred
// logger.If(false).Errorf("Error %s", "ignored") // No output
func (cl *Conditional) Errorf(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Errorf method
cl.logger.Errorf(format, args...)
}
// Stack logs a message at Error level with a stack trace and variadic arguments if the condition is true.
// It concatenates the arguments with spaces and delegates to the loggers Stack method if the
// condition is true. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Stack("Critical", "error") // Output: [app] ERROR: Critical error [stack=...]
// logger.If(false).Stack("Critical", "ignored") // No output
func (cl *Conditional) Stack(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Stack method
cl.logger.Stack(args...)
}
// Stackf logs a message at Error level with a stack trace and a format string if the condition is true.
// It formats the message and delegates to the loggers Stackf method if the condition is true.
// Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...]
// logger.If(false).Stackf("Critical %s", "ignored") // No output
func (cl *Conditional) Stackf(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Stackf method
cl.logger.Stackf(format, args...)
}
// Fatal logs a message at Error level with a stack trace and variadic arguments if the condition is true,
// then exits. It concatenates the arguments with spaces and delegates to the loggers Fatal method
// if the condition is true, terminating the program with exit code 1. Skips processing if false.
// Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits
// logger.If(false).Fatal("Fatal", "ignored") // No output, no exit
func (cl *Conditional) Fatal(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Fatal method
cl.logger.Fatal(args...)
}
// Fatalf logs a formatted message at Error level with a stack trace if the condition is true, then exits.
// It formats the message and delegates to the loggers Fatalf method if the condition is true,
// terminating the program with exit code 1. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits
// logger.If(false).Fatalf("Fatal %s", "ignored") // No output, no exit
func (cl *Conditional) Fatalf(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Fatalf method
cl.logger.Fatalf(format, args...)
}
// Panic logs a message at Error level with a stack trace and variadic arguments if the condition is true,
// then panics. It concatenates the arguments with spaces and delegates to the loggers Panic method
// if the condition is true, triggering a panic. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Panic("Panic", "error") // Output: [app] ERROR: Panic error [stack=...], then panics
// logger.If(false).Panic("Panic", "ignored") // No output, no panic
func (cl *Conditional) Panic(args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Panic method
cl.logger.Panic(args...)
}
// Panicf logs a formatted message at Error level with a stack trace if the condition is true, then panics.
// It formats the message and delegates to the loggers Panicf method if the condition is true,
// triggering a panic. Skips processing if false. Thread-safe via the loggers log method.
// Example:
//
// logger := New("app").Enable()
// logger.If(true).Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [stack=...], then panics
// logger.If(false).Panicf("Panic %s", "ignored") // No output, no panic
func (cl *Conditional) Panicf(format string, args ...any) {
// Skip logging if condition is false
if !cl.condition {
return
}
// Delegate to loggers Panicf method
cl.logger.Panicf(format, args...)
}

374
vendor/github.com/olekukonko/ll/field.go generated vendored Normal file
View File

@@ -0,0 +1,374 @@
package ll
import (
"fmt"
"github.com/olekukonko/ll/lx"
"os"
"strings"
)
// FieldBuilder enables fluent addition of fields before logging.
// It acts as a builder pattern to attach key-value pairs (fields) to log entries,
// supporting structured logging with metadata. The builder allows chaining to add fields
// and log messages at various levels (Info, Debug, Warn, Error, etc.) in a single expression.
type FieldBuilder struct {
logger *Logger // Associated logger instance for logging operations
fields map[string]interface{} // Fields to include in the log entry as key-value pairs
}
// Logger creates a new logger with the builders fields embedded in its context.
// It clones the parent logger and copies the builders fields into the new loggers context,
// enabling persistent field inclusion in subsequent logs. This method supports fluent chaining
// after Fields or Field calls.
// Example:
//
// logger := New("app").Enable()
// newLogger := logger.Fields("user", "alice").Logger()
// newLogger.Info("Action") // Output: [app] INFO: Action [user=alice]
func (fb *FieldBuilder) Logger() *Logger {
// Clone the parent logger to preserve its configuration
newLogger := fb.logger.Clone()
// Initialize a new context map to avoid modifying the parents context
newLogger.context = make(map[string]interface{})
// Copy builders fields into the new loggers context
for k, v := range fb.fields {
newLogger.context[k] = v
}
return newLogger
}
// Info logs a message at Info level with the builders fields.
// It concatenates the arguments with spaces and delegates to the loggers log method,
// returning early if fields are nil. This method is used for informational messages.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Info("Action", "started") // Output: [app] INFO: Action started [user=alice]
func (fb *FieldBuilder) Info(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Log at Info level with the builders fields, no stack trace
fb.logger.log(lx.LevelInfo, lx.ClassText, concatSpaced(args...), fb.fields, false)
}
// Infof logs a message at Info level with the builders fields.
// It formats the message using the provided format string and arguments, then delegates
// to the loggers internal log method. If fields are nil, it returns early to avoid logging.
// This method is part of the fluent API, typically called after adding fields.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Infof("Action %s", "started") // Output: [app] INFO: Action started [user=alice]
func (fb *FieldBuilder) Infof(format string, args ...any) {
// Skip logging if fields are nil to prevent invalid log entries
if fb.fields == nil {
return
}
// Format the message using the provided arguments
msg := fmt.Sprintf(format, args...)
// Log at Info level with the builders fields, no stack trace
fb.logger.log(lx.LevelInfo, lx.ClassText, msg, fb.fields, false)
}
// Debug logs a message at Debug level with the builders fields.
// It concatenates the arguments with spaces and delegates to the loggers log method,
// returning early if fields are nil. This method is used for debugging information.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode [user=alice]
func (fb *FieldBuilder) Debug(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Log at Debug level with the builders fields, no stack trace
fb.logger.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), fb.fields, false)
}
// Debugf logs a message at Debug level with the builders fields.
// It formats the message and delegates to the loggers log method, returning early if
// fields are nil. This method is used for debugging information that may be disabled in
// production environments.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode [user=alice]
func (fb *FieldBuilder) Debugf(format string, args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Format the message
msg := fmt.Sprintf(format, args...)
// Log at Debug level with the builders fields, no stack trace
fb.logger.log(lx.LevelDebug, lx.ClassText, msg, fb.fields, false)
}
// Warn logs a message at Warn level with the builders fields.
// It concatenates the arguments with spaces and delegates to the loggers log method,
// returning early if fields are nil. This method is used for warning conditions.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Warn("Warning", "issued") // Output: [app] WARN: Warning issued [user=alice]
func (fb *FieldBuilder) Warn(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Log at Warn level with the builders fields, no stack trace
fb.logger.log(lx.LevelWarn, lx.ClassText, concatSpaced(args...), fb.fields, false)
}
// Warnf logs a message at Warn level with the builders fields.
// It formats the message and delegates to the loggers log method, returning early if
// fields are nil. This method is used for warning conditions that do not halt execution.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued [user=alice]
func (fb *FieldBuilder) Warnf(format string, args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Format the message
msg := fmt.Sprintf(format, args...)
// Log at Warn level with the builders fields, no stack trace
fb.logger.log(lx.LevelWarn, lx.ClassText, msg, fb.fields, false)
}
// Error logs a message at Error level with the builders fields.
// It concatenates the arguments with spaces and delegates to the loggers log method,
// returning early if fields are nil. This method is used for error conditions.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Error("Error", "occurred") // Output: [app] ERROR: Error occurred [user=alice]
func (fb *FieldBuilder) Error(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Log at Error level with the builders fields, no stack trace
fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, false)
}
// Errorf logs a message at Error level with the builders fields.
// It formats the message and delegates to the loggers log method, returning early if
// fields are nil. This method is used for error conditions that may require attention.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred [user=alice]
func (fb *FieldBuilder) Errorf(format string, args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Format the message
msg := fmt.Sprintf(format, args...)
// Log at Error level with the builders fields, no stack trace
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, false)
}
// Stack logs a message at Error level with a stack trace and the builders fields.
// It concatenates the arguments with spaces and delegates to the loggers log method,
// returning early if fields are nil. This method is useful for debugging critical errors.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Stack("Critical", "error") // Output: [app] ERROR: Critical error [user=alice stack=...]
func (fb *FieldBuilder) Stack(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Log at Error level with the builders fields and a stack trace
fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, true)
}
// Stackf logs a message at Error level with a stack trace and the builders fields.
// It formats the message and delegates to the loggers log method, returning early if
// fields are nil. This method is useful for debugging critical errors.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [user=alice stack=...]
func (fb *FieldBuilder) Stackf(format string, args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Format the message
msg := fmt.Sprintf(format, args...)
// Log at Error level with the builders fields and a stack trace
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true)
}
// Fatal logs a message at Error level with a stack trace and the builders fields, then exits.
// It constructs the message from variadic arguments, logs it with a stack trace, and terminates
// the program with exit code 1. Returns early if fields are nil. This method is used for
// unrecoverable errors.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits
func (fb *FieldBuilder) Fatal(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Build the message by concatenating arguments with spaces
var builder strings.Builder
for i, arg := range args {
if i > 0 {
builder.WriteString(lx.Space)
}
builder.WriteString(fmt.Sprint(arg))
}
// Log at Error level with the builders fields and a stack trace
fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), fb.fields, true)
// Exit the program with status code 1
os.Exit(1)
}
// Fatalf logs a formatted message at Error level with a stack trace and the builders fields,
// then exits. It delegates to Fatal and returns early if fields are nil. This method is used
// for unrecoverable errors.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits
func (fb *FieldBuilder) Fatalf(format string, args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Format the message and pass to Fatal
fb.Fatal(fmt.Sprintf(format, args...))
}
// Panic logs a message at Error level with a stack trace and the builders fields, then panics.
// It constructs the message from variadic arguments, logs it with a stack trace, and triggers
// a panic with the message. Returns early if fields are nil. This method is used for critical
// errors that require immediate program termination with a panic.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Panic("Panic", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics
func (fb *FieldBuilder) Panic(args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Build the message by concatenating arguments with spaces
var builder strings.Builder
for i, arg := range args {
if i > 0 {
builder.WriteString(lx.Space)
}
builder.WriteString(fmt.Sprint(arg))
}
msg := builder.String()
// Log at Error level with the builders fields and a stack trace
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true)
// Trigger a panic with the formatted message
panic(msg)
}
// Panicf logs a formatted message at Error level with a stack trace and the builders fields,
// then panics. It delegates to Panic and returns early if fields are nil. This method is used
// for critical errors that require immediate program termination with a panic.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("user", "alice").Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics
func (fb *FieldBuilder) Panicf(format string, args ...any) {
// Skip logging if fields are nil
if fb.fields == nil {
return
}
// Format the message and pass to Panic
fb.Panic(fmt.Sprintf(format, args...))
}
// Err adds one or more errors to the FieldBuilder as a field and logs them.
// It stores non-nil errors in the "error" field: a single error if only one is non-nil,
// or a slice of errors if multiple are non-nil. It logs the concatenated string representations
// of non-nil errors (e.g., "failed 1; failed 2") at the Error level. Returns the FieldBuilder
// for chaining, allowing further field additions or logging. Thread-safe via the loggers mutex.
// Example:
//
// logger := New("app").Enable()
// err1 := errors.New("failed 1")
// err2 := errors.New("failed 2")
// logger.Fields("k", "v").Err(err1, err2).Info("Error occurred")
// // Output: [app] ERROR: failed 1; failed 2
// // [app] INFO: Error occurred [error=[failed 1 failed 2] k=v]
func (fb *FieldBuilder) Err(errs ...error) *FieldBuilder {
// Initialize fields map if nil
if fb.fields == nil {
fb.fields = make(map[string]interface{})
}
// Collect non-nil errors and build log message
var nonNilErrors []error
var builder strings.Builder
count := 0
for i, err := range errs {
if err != nil {
if i > 0 && count > 0 {
builder.WriteString("; ")
}
builder.WriteString(err.Error())
nonNilErrors = append(nonNilErrors, err)
count++
}
}
// Set error field and log if there are non-nil errors
if count > 0 {
if count == 1 {
// Store single error directly
fb.fields["error"] = nonNilErrors[0]
} else {
// Store slice of errors
fb.fields["error"] = nonNilErrors
}
// Log concatenated error messages at Error level
fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), nil, false)
}
// Return FieldBuilder for chaining
return fb
}
// Merge adds additional key-value pairs to the FieldBuilder.
// It processes variadic arguments as key-value pairs, expecting string keys. Non-string keys
// or uneven pairs generate an "error" field with a descriptive message. Returns the FieldBuilder
// for chaining to allow further field additions or logging.
// Example:
//
// logger := New("app").Enable()
// logger.Fields("k1", "v1").Merge("k2", "v2").Info("Action") // Output: [app] INFO: Action [k1=v1 k2=v2]
func (fb *FieldBuilder) Merge(pairs ...any) *FieldBuilder {
// Process pairs as key-value, advancing by 2
for i := 0; i < len(pairs)-1; i += 2 {
// Ensure the key is a string
if key, ok := pairs[i].(string); ok {
fb.fields[key] = pairs[i+1]
} else {
// Log an error field for non-string keys
fb.fields["error"] = fmt.Errorf("non-string key in Merge: %v", pairs[i])
}
}
// Check for uneven pairs (missing value)
if len(pairs)%2 != 0 {
fb.fields["error"] = fmt.Errorf("uneven key-value pairs in Merge: [%v]", pairs[len(pairs)-1])
}
return fb
}

640
vendor/github.com/olekukonko/ll/global.go generated vendored Normal file
View File

@@ -0,0 +1,640 @@
package ll
import (
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/ll/lx"
"os"
"sync/atomic"
"time"
)
// defaultLogger is the global logger instance for package-level logging functions.
// It provides a shared logger for convenience, allowing logging without explicitly creating
// a logger instance. The logger is initialized with default settings: enabled, Debug level,
// flat namespace style, and a text handler to os.Stdout. It is thread-safe due to the Logger
// structs mutex.
var defaultLogger = &Logger{
enabled: true, // Initially enabled
level: lx.LevelDebug, // Minimum log level set to Debug
namespaces: defaultStore, // Shared namespace store for enable/disable states
context: make(map[string]interface{}), // Empty context for global fields
style: lx.FlatPath, // Flat namespace style (e.g., [parent/child])
handler: lh.NewTextHandler(os.Stdout), // Default text handler to os.Stdout
middleware: make([]Middleware, 0), // Empty middleware chain
stackBufferSize: 4096, // Buffer size for stack traces
}
// Handler sets the handler for the default logger.
// It configures the output destination and format (e.g., text, JSON) for logs emitted by
// defaultLogger. Returns the default logger for method chaining, enabling fluent configuration.
// Example:
//
// ll.Handler(lh.NewJSONHandler(os.Stdout)).Enable()
// ll.Info("Started") // Output: {"level":"INFO","message":"Started"}
func Handler(handler lx.Handler) *Logger {
return defaultLogger.Handler(handler)
}
// Level sets the minimum log level for the default logger.
// It determines which log messages (Debug, Info, Warn, Error) are emitted. Messages below
// the specified level are ignored. Returns the default logger for method chaining.
// Example:
//
// ll.Level(lx.LevelWarn)
// ll.Info("Ignored") // No output
// ll.Warn("Logged") // Output: [] WARN: Logged
func Level(level lx.LevelType) *Logger {
return defaultLogger.Level(level)
}
// Style sets the namespace style for the default logger.
// It controls how namespace paths are formatted in logs (FlatPath: [parent/child],
// NestedPath: [parent]→[child]). Returns the default logger for method chaining.
// Example:
//
// ll.Style(lx.NestedPath)
// ll.Info("Test") // Output: []: INFO: Test
func Style(style lx.StyleType) *Logger {
return defaultLogger.Style(style)
}
// NamespaceEnable enables logging for a namespace and its children using the default logger.
// It activates logging for the specified namespace path (e.g., "app/db") and all its
// descendants. Returns the default logger for method chaining. Thread-safe via the Loggers mutex.
// Example:
//
// ll.NamespaceEnable("app/db")
// ll.Clone().Namespace("db").Info("Query") // Output: [app/db] INFO: Query
func NamespaceEnable(path string) *Logger {
return defaultLogger.NamespaceEnable(path)
}
// NamespaceDisable disables logging for a namespace and its children using the default logger.
// It suppresses logging for the specified namespace path and all its descendants. Returns
// the default logger for method chaining. Thread-safe via the Loggers mutex.
// Example:
//
// ll.NamespaceDisable("app/db")
// ll.Clone().Namespace("db").Info("Query") // No output
func NamespaceDisable(path string) *Logger {
return defaultLogger.NamespaceDisable(path)
}
// Namespace creates a child logger with a sub-namespace appended to the current path.
// The child inherits the default loggers configuration but has an independent context.
// Thread-safe with read lock. Returns the new logger for further configuration or logging.
// Example:
//
// logger := ll.Namespace("app")
// logger.Info("Started") // Output: [app] INFO: Started
func Namespace(name string) *Logger {
return defaultLogger.Namespace(name)
}
// Info logs a message at Info level with variadic arguments using the default logger.
// It concatenates the arguments with spaces and delegates to defaultLoggers Info method.
// Thread-safe via the Loggers log method.
// Example:
//
// ll.Info("Service", "started") // Output: [] INFO: Service started
func Info(args ...any) {
defaultLogger.Info(args...)
}
// Infof logs a message at Info level with a format string using the default logger.
// It formats the message using the provided format string and arguments, then delegates to
// defaultLoggers Infof method. Thread-safe via the Loggers log method.
// Example:
//
// ll.Infof("Service %s", "started") // Output: [] INFO: Service started
func Infof(format string, args ...any) {
defaultLogger.Infof(format, args...)
}
// Debug logs a message at Debug level with variadic arguments using the default logger.
// It concatenates the arguments with spaces and delegates to defaultLoggers Debug method.
// Used for debugging information, typically disabled in production. Thread-safe.
// Example:
//
// ll.Level(lx.LevelDebug)
// ll.Debug("Debugging", "mode") // Output: [] DEBUG: Debugging mode
func Debug(args ...any) {
defaultLogger.Debug(args...)
}
// Debugf logs a message at Debug level with a format string using the default logger.
// It formats the message and delegates to defaultLoggers Debugf method. Used for debugging
// information, typically disabled in production. Thread-safe.
// Example:
//
// ll.Level(lx.LevelDebug)
// ll.Debugf("Debug %s", "mode") // Output: [] DEBUG: Debug mode
func Debugf(format string, args ...any) {
defaultLogger.Debugf(format, args...)
}
// Warn logs a message at Warn level with variadic arguments using the default logger.
// It concatenates the arguments with spaces and delegates to defaultLoggers Warn method.
// Used for warning conditions that do not halt execution. Thread-safe.
// Example:
//
// ll.Warn("Low", "memory") // Output: [] WARN: Low memory
func Warn(args ...any) {
defaultLogger.Warn(args...)
}
// Warnf logs a message at Warn level with a format string using the default logger.
// It formats the message and delegates to defaultLoggers Warnf method. Used for warning
// conditions that do not halt execution. Thread-safe.
// Example:
//
// ll.Warnf("Low %s", "memory") // Output: [] WARN: Low memory
func Warnf(format string, args ...any) {
defaultLogger.Warnf(format, args...)
}
// Error logs a message at Error level with variadic arguments using the default logger.
// It concatenates the arguments with spaces and delegates to defaultLoggers Error method.
// Used for error conditions requiring attention. Thread-safe.
// Example:
//
// ll.Error("Database", "failure") // Output: [] ERROR: Database failure
func Error(args ...any) {
defaultLogger.Error(args...)
}
// Errorf logs a message at Error level with a format string using the default logger.
// It formats the message and delegates to defaultLoggers Errorf method. Used for error
// conditions requiring attention. Thread-safe.
// Example:
//
// ll.Errorf("Database %s", "failure") // Output: [] ERROR: Database failure
func Errorf(format string, args ...any) {
defaultLogger.Errorf(format, args...)
}
// Stack logs a message at Error level with a stack trace and variadic arguments using the default logger.
// It concatenates the arguments with spaces and delegates to defaultLoggers Stack method.
// Thread-safe.
// Example:
//
// ll.Stack("Critical", "error") // Output: [] ERROR: Critical error [stack=...]
func Stack(args ...any) {
defaultLogger.Stack(args...)
}
// Stackf logs a message at Error level with a stack trace and a format string using the default logger.
// It formats the message and delegates to defaultLoggers Stackf method. Thread-safe.
// Example:
//
// ll.Stackf("Critical %s", "error") // Output: [] ERROR: Critical error [stack=...]
func Stackf(format string, args ...any) {
defaultLogger.Stackf(format, args...)
}
// Fatal logs a message at Error level with a stack trace and variadic arguments using the default logger,
// then exits. It concatenates the arguments with spaces, logs with a stack trace, and terminates
// with exit code 1. Thread-safe.
// Example:
//
// ll.Fatal("Fatal", "error") // Output: [] ERROR: Fatal error [stack=...], then exits
func Fatal(args ...any) {
defaultLogger.Fatal(args...)
}
// Fatalf logs a formatted message at Error level with a stack trace using the default logger,
// then exits. It formats the message, logs with a stack trace, and terminates with exit code 1.
// Thread-safe.
// Example:
//
// ll.Fatalf("Fatal %s", "error") // Output: [] ERROR: Fatal error [stack=...], then exits
func Fatalf(format string, args ...any) {
defaultLogger.Fatalf(format, args...)
}
// Panic logs a message at Error level with a stack trace and variadic arguments using the default logger,
// then panics. It concatenates the arguments with spaces, logs with a stack trace, and triggers a panic.
// Thread-safe.
// Example:
//
// ll.Panic("Panic", "error") // Output: [] ERROR: Panic error [stack=...], then panics
func Panic(args ...any) {
defaultLogger.Panic(args...)
}
// Panicf logs a formatted message at Error level with a stack trace using the default logger,
// then panics. It formats the message, logs with a stack trace, and triggers a panic. Thread-safe.
// Example:
//
// ll.Panicf("Panic %s", "error") // Output: [] ERROR: Panic error [stack=...], then panics
func Panicf(format string, args ...any) {
defaultLogger.Panicf(format, args...)
}
// If creates a conditional logger that logs only if the condition is true using the default logger.
// It returns a Conditional struct that wraps the default logger, enabling conditional logging methods.
// Thread-safe via the Loggers mutex.
// Example:
//
// ll.If(true).Info("Logged") // Output: [] INFO: Logged
// ll.If(false).Info("Ignored") // No output
func If(condition bool) *Conditional {
return defaultLogger.If(condition)
}
// Context creates a new logger with additional contextual fields using the default logger.
// It preserves existing context fields and adds new ones, returning a new logger instance
// to avoid mutating the default logger. Thread-safe with write lock.
// Example:
//
// logger := ll.Context(map[string]interface{}{"user": "alice"})
// logger.Info("Action") // Output: [] INFO: Action [user=alice]
func Context(fields map[string]interface{}) *Logger {
return defaultLogger.Context(fields)
}
// AddContext adds a key-value pair to the default loggers context, modifying it directly.
// It mutates the default loggers context and is thread-safe using a write lock.
// Example:
//
// ll.AddContext("user", "alice")
// ll.Info("Action") // Output: [] INFO: Action [user=alice]
func AddContext(key string, value interface{}) *Logger {
return defaultLogger.AddContext(key, value)
}
// GetContext returns the default loggers context map of persistent key-value fields.
// It provides thread-safe read access to the context using a read lock.
// Example:
//
// ll.AddContext("user", "alice")
// ctx := ll.GetContext() // Returns map[string]interface{}{"user": "alice"}
func GetContext() map[string]interface{} {
return defaultLogger.GetContext()
}
// GetLevel returns the minimum log level for the default logger.
// It provides thread-safe read access to the level field using a read lock.
// Example:
//
// ll.Level(lx.LevelWarn)
// if ll.GetLevel() == lx.LevelWarn {
// ll.Warn("Warning level set") // Output: [] WARN: Warning level set
// }
func GetLevel() lx.LevelType {
return defaultLogger.GetLevel()
}
// GetPath returns the default loggers current namespace path.
// It provides thread-safe read access to the currentPath field using a read lock.
// Example:
//
// logger := ll.Namespace("app")
// path := logger.GetPath() // Returns "app"
func GetPath() string {
return defaultLogger.GetPath()
}
// GetSeparator returns the default loggers namespace separator (e.g., "/").
// It provides thread-safe read access to the separator field using a read lock.
// Example:
//
// ll.Separator(".")
// sep := ll.GetSeparator() // Returns "."
func GetSeparator() string {
return defaultLogger.GetSeparator()
}
// GetStyle returns the default loggers namespace formatting style (FlatPath or NestedPath).
// It provides thread-safe read access to the style field using a read lock.
// Example:
//
// ll.Style(lx.NestedPath)
// if ll.GetStyle() == lx.NestedPath {
// ll.Info("Nested style") // Output: []: INFO: Nested style
// }
func GetStyle() lx.StyleType {
return defaultLogger.GetStyle()
}
// GetHandler returns the default loggers current handler for customization or inspection.
// The returned handler should not be modified concurrently with logger operations.
// Example:
//
// handler := ll.GetHandler() // Returns the current handler (e.g., TextHandler)
func GetHandler() lx.Handler {
return defaultLogger.GetHandler()
}
// Separator sets the namespace separator for the default logger (e.g., "/" or ".").
// It updates the separator used in namespace paths. Thread-safe with write lock.
// Returns the default logger for method chaining.
// Example:
//
// ll.Separator(".")
// ll.Namespace("app").Info("Log") // Output: [app] INFO: Log
func Separator(separator string) *Logger {
return defaultLogger.Separator(separator)
}
// Prefix sets a prefix to be prepended to all log messages of the default logger.
// The prefix is applied before the message in the log output. Thread-safe with write lock.
// Returns the default logger for method chaining.
// Example:
//
// ll.Prefix("APP: ")
// ll.Info("Started") // Output: [] INFO: APP: Started
func Prefix(prefix string) *Logger {
return defaultLogger.Prefix(prefix)
}
// StackSize sets the buffer size for stack trace capture in the default logger.
// It configures the maximum size for stack traces in Stack, Fatal, and Panic methods.
// Thread-safe with write lock. Returns the default logger for chaining.
// Example:
//
// ll.StackSize(65536)
// ll.Stack("Error") // Captures up to 64KB stack trace
func StackSize(size int) *Logger {
return defaultLogger.StackSize(size)
}
// Use adds a middleware function to process log entries before they are handled by the default logger.
// It registers the middleware and returns a Middleware handle for removal. Middleware returning
// a non-nil error stops the log. Thread-safe with write lock.
// Example:
//
// mw := ll.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
// if e.Level < lx.LevelWarn {
// return fmt.Errorf("level too low")
// }
// return nil
// }))
// ll.Info("Ignored") // No output
// mw.Remove()
// ll.Info("Logged") // Output: [] INFO: Logged
func Use(fn lx.Handler) *Middleware {
return defaultLogger.Use(fn)
}
// Remove removes middleware by the reference returned from Use for the default logger.
// It delegates to the Middlewares Remove method for thread-safe removal.
// Example:
//
// mw := ll.Use(someMiddleware)
// ll.Remove(mw) // Removes middleware
func Remove(m *Middleware) {
defaultLogger.Remove(m)
}
// Clear removes all middleware functions from the default logger.
// It resets the middleware chain to empty, ensuring no middleware is applied.
// Thread-safe with write lock. Returns the default logger for chaining.
// Example:
//
// ll.Use(someMiddleware)
// ll.Clear()
// ll.Info("No middleware") // Output: [] INFO: No middleware
func Clear() *Logger {
return defaultLogger.Clear()
}
// CanLog checks if a log at the given level would be emitted by the default logger.
// It considers enablement, log level, namespaces, sampling, and rate limits.
// Thread-safe via the Loggers shouldLog method.
// Example:
//
// ll.Level(lx.LevelWarn)
// canLog := ll.CanLog(lx.LevelInfo) // false
func CanLog(level lx.LevelType) bool {
return defaultLogger.CanLog(level)
}
// NamespaceEnabled checks if a namespace is enabled in the default logger.
// It evaluates the namespace hierarchy, considering parent namespaces, and caches the result
// for performance. Thread-safe with read lock.
// Example:
//
// ll.NamespaceDisable("app/db")
// enabled := ll.NamespaceEnabled("app/db") // false
func NamespaceEnabled(path string) bool {
return defaultLogger.NamespaceEnabled(path)
}
// Print logs a message at Info level without format specifiers using the default logger.
// It concatenates variadic arguments with spaces, minimizing allocations, and delegates
// to defaultLoggers Print method. Thread-safe via the Loggers log method.
// Example:
//
// ll.Print("message", "value") // Output: [] INFO: message value
func Print(args ...any) {
defaultLogger.Print(args...)
}
// Printf logs a message at Info level with a format string using the default logger.
// It formats the message and delegates to defaultLoggers Printf method. Thread-safe via
// the Loggers log method.
// Example:
//
// ll.Printf("Message %s", "value") // Output: [] INFO: Message value
func Printf(format string, args ...any) {
defaultLogger.Printf(format, args...)
}
// Len returns the total number of log entries sent to the handler by the default logger.
// It provides thread-safe access to the entries counter using atomic operations.
// Example:
//
// ll.Info("Test")
// count := ll.Len() // Returns 1
func Len() int64 {
return defaultLogger.Len()
}
// Measure is a benchmarking helper that measures and returns the duration of a functions execution.
// It logs the duration at Info level with a "duration" field using defaultLogger. The function
// is executed once, and the elapsed time is returned. Thread-safe via the Loggers mutex.
// Example:
//
// duration := ll.Measure(func() { time.Sleep(time.Millisecond) })
// // Output: [] INFO: function executed [duration=~1ms]
func Measure(fns ...func()) time.Duration {
start := time.Now()
for _, fn := range fns {
fn()
}
duration := time.Since(start)
defaultLogger.Fields("duration", duration).Infof("function executed")
return duration
}
// Benchmark logs the duration since a start time at Info level using the default logger.
// It calculates the time elapsed since the provided start time and logs it with "start",
// "end", and "duration" fields. Thread-safe via the Loggers mutex.
// Example:
//
// start := time.Now()
// time.Sleep(time.Millisecond)
// ll.Benchmark(start) // Output: [] INFO: benchmark [start=... end=... duration=...]
func Benchmark(start time.Time) {
defaultLogger.Fields("start", start, "end", time.Now(), "duration", time.Now().Sub(start)).Infof("benchmark")
}
// Clone returns a new logger with the same configuration as the default logger.
// It creates a copy of defaultLoggers settings (level, style, namespaces, etc.) but with
// an independent context, allowing customization without affecting the global logger.
// Thread-safe via the Loggers Clone method.
// Example:
//
// logger := ll.Clone().Namespace("sub")
// logger.Info("Sub-logger") // Output: [sub] INFO: Sub-logger
func Clone() *Logger {
return defaultLogger.Clone()
}
// Err adds one or more errors to the default loggers context and logs them.
// It stores non-nil errors in the "error" context field and logs their concatenated string
// representations (e.g., "failed 1; failed 2") at the Error level. Thread-safe via the Loggers mutex.
// Example:
//
// err1 := errors.New("failed 1")
// ll.Err(err1)
// ll.Info("Error occurred") // Output: [] ERROR: failed 1
// // [] INFO: Error occurred [error=failed 1]
func Err(errs ...error) {
defaultLogger.Err(errs...)
}
// Start activates the global logging system.
// If the system was shut down, this re-enables all logging operations,
// subject to individual logger and namespace configurations.
// Thread-safe via atomic operations.
// Example:
//
// ll.Shutdown()
// ll.Info("Ignored") // No output
// ll.Start()
// ll.Info("Logged") // Output: [] INFO: Logged
func Start() {
atomic.StoreInt32(&systemActive, 1)
}
// Shutdown deactivates the global logging system.
// All logging operations are skipped, regardless of individual logger or namespace configurations,
// until Start() is called again. Thread-safe via atomic operations.
// Example:
//
// ll.Shutdown()
// ll.Info("Ignored") // No output
func Shutdown() {
atomic.StoreInt32(&systemActive, 0)
}
// Active returns true if the global logging system is currently active.
// Thread-safe via atomic operations.
// Example:
//
// if ll.Active() {
// ll.Info("System active") // Output: [] INFO: System active
// }
func Active() bool {
return atomic.LoadInt32(&systemActive) == 1
}
// Enable activates logging for the default logger.
// It allows logs to be emitted if other conditions (level, namespace) are met.
// Thread-safe with write lock. Returns the default logger for method chaining.
// Example:
//
// ll.Disable()
// ll.Info("Ignored") // No output
// ll.Enable()
// ll.Info("Logged") // Output: [] INFO: Logged
func Enable() *Logger {
return defaultLogger.Enable()
}
// Disable deactivates logging for the default logger.
// It suppresses all logs, regardless of level or namespace. Thread-safe with write lock.
// Returns the default logger for method chaining.
// Example:
//
// ll.Disable()
// ll.Info("Ignored") // No output
func Disable() *Logger {
return defaultLogger.Disable()
}
// Dbg logs debug information including the source file, line number, and expression value
// using the default logger. It captures the calling line of code and displays both the
// expression and its value. Useful for debugging without temporary print statements.
// Example:
//
// x := 42
// ll.Dbg(x) // Output: [file.go:123] x = 42
func Dbg(any ...interface{}) {
defaultLogger.Dbg(any...)
}
// Dump displays a hex and ASCII representation of a values binary form using the default logger.
// It serializes the value using gob encoding or direct conversion and shows a hex/ASCII dump.
// Useful for inspecting binary data structures.
// Example:
//
// ll.Dump([]byte{0x41, 0x42}) // Outputs hex/ASCII dump
func Dump(any interface{}) {
defaultLogger.Dump(any)
}
// Enabled returns whether the default logger is enabled for logging.
// It provides thread-safe read access to the enabled field using a read lock.
// Example:
//
// ll.Enable()
// if ll.Enabled() {
// ll.Info("Logging enabled") // Output: [] INFO: Logging enabled
// }
func Enabled() bool {
return defaultLogger.Enabled()
}
// Fields starts a fluent chain for adding fields using variadic key-value pairs with the default logger.
// It creates a FieldBuilder to attach fields, handling non-string keys or uneven pairs by
// adding an error field. Thread-safe via the FieldBuilders logger.
// Example:
//
// ll.Fields("user", "alice").Info("Action") // Output: [] INFO: Action [user=alice]
func Fields(pairs ...any) *FieldBuilder {
return defaultLogger.Fields(pairs...)
}
// Field starts a fluent chain for adding fields from a map with the default logger.
// It creates a FieldBuilder to attach fields from a map, supporting type-safe field addition.
// Thread-safe via the FieldBuilders logger.
// Example:
//
// ll.Field(map[string]interface{}{"user": "alice"}).Info("Action") // Output: [] INFO: Action [user=alice]
func Field(fields map[string]interface{}) *FieldBuilder {
return defaultLogger.Field(fields)
}
// Line adds vertical spacing (newlines) to the log output using the default logger.
// If no arguments are provided, it defaults to 1 newline. Multiple values are summed to
// determine the total lines. Useful for visually separating log sections. Thread-safe.
// Example:
//
// ll.Line(2).Info("After two newlines") // Adds 2 blank lines before: [] INFO: After two newlines
func Line(lines ...int) *Logger {
return defaultLogger.Line(lines...)
}
// Indent sets the indentation level for all log messages of the default logger.
// Each level adds two spaces to the log message, useful for hierarchical output.
// Thread-safe with write lock. Returns the default logger for method chaining.
// Example:
//
// ll.Indent(2)
// ll.Info("Indented") // Output: [] INFO: Indented
func Indent(depth int) *Logger {
return defaultLogger.Indent(depth)
}

48
vendor/github.com/olekukonko/ll/lc.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
package ll
import "github.com/olekukonko/ll/lx"
// defaultStore is the global namespace store for enable/disable states.
// It is shared across all Logger instances to manage namespace hierarchy consistently.
// Thread-safe via the lx.Namespace structs sync.Map.
var defaultStore = &lx.Namespace{}
// systemActive indicates if the global logging system is active.
// Defaults to true, meaning logging is active unless explicitly shut down.
// Or, default to false and require an explicit ll.Start(). Let's default to true for less surprise.
var systemActive int32 = 1 // 1 for true, 0 for false (for atomic operations)
// Option defines a functional option for configuring a Logger.
type Option func(*Logger)
// reverseString reverses the input string by swapping characters from both ends.
// It converts the string to a rune slice to handle Unicode characters correctly,
// ensuring proper reversal for multi-byte characters.
// Used internally for string manipulation, such as in debugging or log formatting.
func reverseString(s string) string {
// Convert string to rune slice to handle Unicode characters
r := []rune(s)
// Iterate over half the slice, swapping characters from start and end
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
// Convert rune slice back to string and return
return string(r)
}
// viewString converts a byte slice to a printable string, replacing non-printable
// characters (ASCII < 32 or > 126) with a dot ('.').
// It ensures safe display of binary data in logs, such as in the Dump method.
// Used for formatting binary data in a human-readable hex/ASCII dump.
func viewString(b []byte) string {
// Convert byte slice to rune slice via string for processing
r := []rune(string(b))
// Replace non-printable characters with '.'
for i := range r {
if r[i] < 32 || r[i] > 126 {
r[i] = '.'
}
}
// Return the resulting printable string
return string(r)
}

279
vendor/github.com/olekukonko/ll/lh/buffered.go generated vendored Normal file
View File

@@ -0,0 +1,279 @@
package lh
import (
"fmt"
"io"
"os"
"runtime"
"sync"
"time"
"github.com/olekukonko/ll/lx"
)
// Buffering holds configuration for the Buffered handler.
type Buffering struct {
BatchSize int // Flush when this many entries are buffered (default: 100)
FlushInterval time.Duration // Maximum time between flushes (default: 10s)
MaxBuffer int // Maximum buffer size before applying backpressure (default: 1000)
OnOverflow func(int) // Called when buffer reaches MaxBuffer (default: logs warning)
}
// BufferingOpt configures Buffered handler.
type BufferingOpt func(*Buffering)
// WithBatchSize sets the batch size for flushing.
// It specifies the number of log entries to buffer before flushing to the underlying handler.
// Example:
//
// handler := NewBuffered(textHandler, WithBatchSize(50)) // Flush every 50 entries
func WithBatchSize(size int) BufferingOpt {
return func(c *Buffering) {
c.BatchSize = size
}
}
// WithFlushInterval sets the maximum time between flushes.
// It defines the interval at which buffered entries are flushed, even if the batch size is not reached.
// Example:
//
// handler := NewBuffered(textHandler, WithFlushInterval(5*time.Second)) // Flush every 5 seconds
func WithFlushInterval(d time.Duration) BufferingOpt {
return func(c *Buffering) {
c.FlushInterval = d
}
}
// WithMaxBuffer sets the maximum buffer size before backpressure.
// It limits the number of entries that can be queued in the channel, triggering overflow handling if exceeded.
// Example:
//
// handler := NewBuffered(textHandler, WithMaxBuffer(500)) // Allow up to 500 buffered entries
func WithMaxBuffer(size int) BufferingOpt {
return func(c *Buffering) {
c.MaxBuffer = size
}
}
// WithOverflowHandler sets the overflow callback.
// It specifies a function to call when the buffer reaches MaxBuffer, typically for logging or metrics.
// Example:
//
// handler := NewBuffered(textHandler, WithOverflowHandler(func(n int) { fmt.Printf("Overflow: %d entries\n", n) }))
func WithOverflowHandler(fn func(int)) BufferingOpt {
return func(c *Buffering) {
c.OnOverflow = fn
}
}
// Buffered wraps any Handler to provide buffering capabilities.
// It buffers log entries in a channel and flushes them based on batch size, time interval, or explicit flush.
// The generic type H ensures compatibility with any lx.Handler implementation.
// Thread-safe via channels and sync primitives.
type Buffered[H lx.Handler] struct {
handler H // Underlying handler to process log entries
config *Buffering // Configuration for batching and flushing
entries chan *lx.Entry // Channel for buffering log entries
flushSignal chan struct{} // Channel to trigger explicit flushes
shutdown chan struct{} // Channel to signal worker shutdown
shutdownOnce sync.Once // Ensures Close is called only once
wg sync.WaitGroup // Waits for worker goroutine to finish
}
// NewBuffered creates a new buffered handler that wraps another handler.
// It initializes the handler with default or provided configuration options and starts a worker goroutine.
// Thread-safe via channel operations and finalizer for cleanup.
// Example:
//
// textHandler := lh.NewTextHandler(os.Stdout)
// buffered := NewBuffered(textHandler, WithBatchSize(50))
func NewBuffered[H lx.Handler](handler H, opts ...BufferingOpt) *Buffered[H] {
// Initialize default configuration
config := &Buffering{
BatchSize: 100, // Default: flush every 100 entries
FlushInterval: 10 * time.Second, // Default: flush every 10 seconds
MaxBuffer: 1000, // Default: max 1000 entries in buffer
OnOverflow: func(count int) { // Default: log overflow to io.Discard
fmt.Fprintf(io.Discard, "log buffer overflow: %d entries\n", count)
},
}
// Apply provided options
for _, opt := range opts {
opt(config)
}
// Ensure sane configuration values
if config.BatchSize < 1 {
config.BatchSize = 1 // Minimum batch size is 1
}
if config.MaxBuffer < config.BatchSize {
config.MaxBuffer = config.BatchSize * 10 // Ensure buffer is at least 10x batch size
}
if config.FlushInterval <= 0 {
config.FlushInterval = 10 * time.Second // Minimum flush interval is 10s
}
// Initialize Buffered handler
b := &Buffered[H]{
handler: handler, // Set underlying handler
config: config, // Set configuration
entries: make(chan *lx.Entry, config.MaxBuffer), // Create buffered channel
flushSignal: make(chan struct{}, 1), // Create single-slot flush signal channel
shutdown: make(chan struct{}), // Create shutdown signal channel
}
// Start worker goroutine
b.wg.Add(1)
go b.worker()
// Set finalizer for cleanup during garbage collection
runtime.SetFinalizer(b, (*Buffered[H]).Final)
return b
}
// Handle implements the lx.Handler interface.
// It buffers log entries in the entries channel or triggers a flush on overflow.
// Returns an error if the buffer is full and flush cannot be triggered.
// Thread-safe via non-blocking channel operations.
// Example:
//
// buffered.Handle(&lx.Entry{Message: "test"}) // Buffers entry or triggers flush
func (b *Buffered[H]) Handle(e *lx.Entry) error {
select {
case b.entries <- e: // Buffer entry if channel has space
return nil
default: // Handle buffer overflow
if b.config.OnOverflow != nil {
b.config.OnOverflow(len(b.entries)) // Call overflow handler
}
select {
case b.flushSignal <- struct{}{}: // Trigger flush if possible
return fmt.Errorf("log buffer overflow, triggering flush")
default: // Flush already in progress
return fmt.Errorf("log buffer overflow and flush already in progress")
}
}
}
// Flush triggers an immediate flush of buffered entries.
// It sends a signal to the worker to process all buffered entries.
// If a flush is already pending, it waits briefly and may exit without flushing.
// Thread-safe via non-blocking channel operations.
// Example:
//
// buffered.Flush() // Flushes all buffered entries
func (b *Buffered[H]) Flush() {
select {
case b.flushSignal <- struct{}{}: // Signal worker to flush
case <-time.After(100 * time.Millisecond): // Timeout if flush is pending
// Flush already pending
}
}
// Close flushes any remaining entries and stops the worker.
// It ensures shutdown is performed only once and waits for the worker to finish.
// Thread-safe via sync.Once and WaitGroup.
// Returns nil as it does not produce errors.
// Example:
//
// buffered.Close() // Flushes entries and stops worker
func (b *Buffered[H]) Close() error {
b.shutdownOnce.Do(func() {
close(b.shutdown) // Signal worker to shut down
b.wg.Wait() // Wait for worker to finish
runtime.SetFinalizer(b, nil) // Remove finalizer
})
return nil
}
// Final ensures remaining entries are flushed during garbage collection.
// It calls Close to flush entries and stop the worker.
// Used as a runtime finalizer to prevent log loss.
// Example (internal usage):
//
// runtime.SetFinalizer(buffered, (*Buffered[H]).Final)
func (b *Buffered[H]) Final() {
b.Close()
}
// Config returns the current configuration of the Buffered handler.
// It provides access to BatchSize, FlushInterval, MaxBuffer, and OnOverflow settings.
// Example:
//
// config := buffered.Config() // Access configuration
func (b *Buffered[H]) Config() *Buffering {
return b.config
}
// worker processes entries and handles flushing.
// It runs in a goroutine, buffering entries, flushing on batch size, timer, or explicit signal,
// and shutting down cleanly when signaled.
// Thread-safe via channel operations and WaitGroup.
func (b *Buffered[H]) worker() {
defer b.wg.Done() // Signal completion when worker exits
batch := make([]*lx.Entry, 0, b.config.BatchSize) // Buffer for batching entries
ticker := time.NewTicker(b.config.FlushInterval) // Timer for periodic flushes
defer ticker.Stop() // Clean up ticker
for {
select {
case entry := <-b.entries: // Receive new entry
batch = append(batch, entry)
// Flush if batch size is reached
if len(batch) >= b.config.BatchSize {
b.flushBatch(batch)
batch = batch[:0]
}
case <-ticker.C: // Periodic flush
if len(batch) > 0 {
b.flushBatch(batch)
batch = batch[:0]
}
case <-b.flushSignal: // Explicit flush
if len(batch) > 0 {
b.flushBatch(batch)
batch = batch[:0]
}
b.drainRemaining() // Drain all entries from the channel
case <-b.shutdown: // Shutdown signal
if len(batch) > 0 {
b.flushBatch(batch)
}
b.drainRemaining() // Flush remaining entries
return
}
}
}
// flushBatch processes a batch of entries through the wrapped handler.
// It writes each entry to the underlying handler, logging any errors to stderr.
// Example (internal usage):
//
// b.flushBatch([]*lx.Entry{entry1, entry2})
func (b *Buffered[H]) flushBatch(batch []*lx.Entry) {
for _, entry := range batch {
// Process each entry through the handler
if err := b.handler.Handle(entry); err != nil {
fmt.Fprintf(os.Stderr, "log flush error: %v\n", err) // Log errors to stderr
}
}
}
// drainRemaining processes any remaining entries in the channel.
// It flushes all entries from the entries channel to the underlying handler,
// logging any errors to stderr. Used during flush or shutdown.
// Example (internal usage):
//
// b.drainRemaining() // Flushes all pending entries
func (b *Buffered[H]) drainRemaining() {
for {
select {
case entry := <-b.entries: // Process next entry
if err := b.handler.Handle(entry); err != nil {
fmt.Fprintf(os.Stderr, "log drain error: %v\n", err) // Log errors to stderr
}
default: // Exit when channel is empty
return
}
}
}

439
vendor/github.com/olekukonko/ll/lh/colorized.go generated vendored Normal file
View File

@@ -0,0 +1,439 @@
package lh
import (
"fmt"
"github.com/olekukonko/ll/lx"
"io"
"os"
"sort"
"strings"
)
// Palette defines ANSI color codes for various log components.
// It specifies colors for headers, goroutines, functions, paths, stack traces, and log levels,
// used by ColorizedHandler to format log output with color.
type Palette struct {
Header string // Color for stack trace header and dump separators
Goroutine string // Color for goroutine lines in stack traces
Func string // Color for function names in stack traces
Path string // Color for file paths in stack traces
FileLine string // Color for file line numbers (not used in provided code)
Reset string // Reset code to clear color formatting
Pos string // Color for position in hex dumps
Hex string // Color for hex values in dumps
Ascii string // Color for ASCII values in dumps
Debug string // Color for Debug level messages
Info string // Color for Info level messages
Warn string // Color for Warn level messages
Error string // Color for Error level messages
Title string // Color for dump titles (BEGIN/END separators)
}
// darkPalette defines colors optimized for dark terminal backgrounds.
// It uses bright, contrasting colors for readability on dark backgrounds.
var darkPalette = Palette{
Header: "\033[1;31m", // Bold red for headers
Goroutine: "\033[1;36m", // Bold cyan for goroutines
Func: "\033[97m", // Bright white for functions
Path: "\033[38;5;245m", // Light gray for paths
FileLine: "\033[38;5;111m", // Muted light blue (unused)
Reset: "\033[0m", // Reset color formatting
Title: "\033[38;5;245m", // Light gray for dump titles
Pos: "\033[38;5;117m", // Light blue for dump positions
Hex: "\033[38;5;156m", // Light green for hex values
Ascii: "\033[38;5;224m", // Light pink for ASCII values
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Red for Error level
}
// lightPalette defines colors optimized for light terminal backgrounds.
// It uses darker colors for better contrast on light backgrounds.
var lightPalette = Palette{
Header: "\033[1;31m", // Same red for headers
Goroutine: "\033[34m", // Blue (darker for light bg)
Func: "\033[30m", // Black text for functions
Path: "\033[90m", // Dark gray for paths
FileLine: "\033[94m", // Blue for file lines (unused)
Reset: "\033[0m", // Reset color formatting
Title: "\033[38;5;245m", // Light gray for dump titles
Pos: "\033[38;5;117m", // Light blue for dump positions
Hex: "\033[38;5;156m", // Light green for hex values
Ascii: "\033[38;5;224m", // Light pink for ASCII values
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Red for Error level
}
// ColorizedHandler is a handler that outputs log entries with ANSI color codes.
// It formats log entries with colored namespace, level, message, fields, and stack traces,
// writing the result to the provided writer.
// Thread-safe if the underlying writer is thread-safe.
type ColorizedHandler struct {
w io.Writer // Destination for colored log output
palette Palette // Color scheme for formatting
}
// ColorOption defines a configuration function for ColorizedHandler.
// It allows customization of the handler, such as setting the color palette.
type ColorOption func(*ColorizedHandler)
// WithColorPallet sets the color palette for the ColorizedHandler.
// It allows specifying a custom Palette for dark or light terminal backgrounds.
// Example:
//
// handler := NewColorizedHandler(os.Stdout, WithColorPallet(lightPalette))
func WithColorPallet(pallet Palette) ColorOption {
return func(c *ColorizedHandler) {
c.palette = pallet
}
}
// NewColorizedHandler creates a new ColorizedHandler writing to the specified writer.
// It initializes the handler with a detected or specified color palette and applies
// optional configuration functions.
// Example:
//
// handler := NewColorizedHandler(os.Stdout)
// logger := ll.New("app").Enable().Handler(handler)
// logger.Info("Test") // Output: [app] <colored INFO>: Test
func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler {
c := &ColorizedHandler{w: w} // Initialize with writer
// Apply configuration options
for _, opt := range opts {
opt(c)
}
// Detect palette if not set
c.palette = c.detectPalette()
return c
}
// Handle processes a log entry and writes it with ANSI color codes.
// It delegates to specialized methods based on the entry's class (Dump, Raw, or regular).
// Returns an error if writing to the underlying writer fails.
// Thread-safe if the writer is thread-safe.
// Example:
//
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
func (h *ColorizedHandler) Handle(e *lx.Entry) error {
switch e.Class {
case lx.ClassDump:
// Handle hex dump entries
return h.handleDumpOutput(e)
case lx.ClassRaw:
// Write raw entries directly
_, err := h.w.Write([]byte(e.Message))
return err
default:
// Handle standard log entries
return h.handleRegularOutput(e)
}
}
// handleRegularOutput handles normal log entries.
// It formats the entry with colored namespace, level, message, fields, and stack trace (if present),
// writing the result to the handler's writer.
// Returns an error if writing fails.
// Example (internal usage):
//
// h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
func (h *ColorizedHandler) handleRegularOutput(e *lx.Entry) error {
var builder strings.Builder // Buffer for building formatted output
// Format namespace with colors
h.formatNamespace(&builder, e)
// Format level with color based on severity
h.formatLevel(&builder, e)
// Add message and fields
builder.WriteString(e.Message)
h.formatFields(&builder, e)
// fmt.Println("------------>", len(e.Stack))
// Format stack trace if present
if len(e.Stack) > 0 {
h.formatStack(&builder, e.Stack)
}
// Append newline for non-None levels
if e.Level != lx.LevelNone {
builder.WriteString(lx.Newline)
}
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
return err
}
// formatNamespace formats the namespace with ANSI color codes.
// It supports FlatPath ([parent/child]) and NestedPath ([parent]→[child]) styles.
// Example (internal usage):
//
// h.formatNamespace(&builder, &lx.Entry{Namespace: "parent/child", Style: lx.FlatPath}) // Writes "[parent/child]: "
func (h *ColorizedHandler) formatNamespace(b *strings.Builder, e *lx.Entry) {
if e.Namespace == "" {
return
}
b.WriteString(lx.LeftBracket)
switch e.Style {
case lx.NestedPath:
// Split namespace and format as [parent]→[child]
parts := strings.Split(e.Namespace, lx.Slash)
for i, part := range parts {
b.WriteString(part)
b.WriteString(lx.RightBracket)
if i < len(parts)-1 {
b.WriteString(lx.Arrow)
b.WriteString(lx.LeftBracket)
}
}
default: // FlatPath
// Format as [parent/child]
b.WriteString(e.Namespace)
b.WriteString(lx.RightBracket)
}
b.WriteString(lx.Colon)
b.WriteString(lx.Space)
}
// formatLevel formats the log level with ANSI color codes.
// It applies a color based on the level (Debug, Info, Warn, Error) and resets afterward.
// Example (internal usage):
//
// h.formatLevel(&builder, &lx.Entry{Level: lx.LevelInfo}) // Writes "<green>INFO<reset>: "
func (h *ColorizedHandler) formatLevel(b *strings.Builder, e *lx.Entry) {
// Map levels to colors
color := map[lx.LevelType]string{
lx.LevelDebug: h.palette.Debug, // Cyan
lx.LevelInfo: h.palette.Info, // Green
lx.LevelWarn: h.palette.Warn, // Yellow
lx.LevelError: h.palette.Error, // Red
}[e.Level]
b.WriteString(color)
b.WriteString(e.Level.String())
b.WriteString(h.palette.Reset)
b.WriteString(lx.Colon)
b.WriteString(lx.Space)
}
// formatFields formats the log entry's fields in sorted order.
// It writes fields as [key=value key=value], with no additional coloring.
// Example (internal usage):
//
// h.formatFields(&builder, &lx.Entry{Fields: map[string]interface{}{"key": "value"}}) // Writes " [key=value]"
func (h *ColorizedHandler) formatFields(b *strings.Builder, e *lx.Entry) {
if len(e.Fields) == 0 {
return
}
// Collect and sort field keys
var keys []string
for k := range e.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
b.WriteString(lx.Space)
b.WriteString(lx.LeftBracket)
// Format fields as key=value
for i, k := range keys {
if i > 0 {
b.WriteString(lx.Space)
}
b.WriteString(k)
b.WriteString("=")
b.WriteString(fmt.Sprint(e.Fields[k]))
}
b.WriteString(lx.RightBracket)
}
// formatStack formats a stack trace with ANSI color codes.
// It structures the stack trace with colored goroutine, function, and path segments,
// using indentation and separators for readability.
// Example (internal usage):
//
// h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends colored stack trace
func (h *ColorizedHandler) formatStack(b *strings.Builder, stack []byte) {
b.WriteString("\n")
b.WriteString(h.palette.Header)
b.WriteString("[stack]")
b.WriteString(h.palette.Reset)
b.WriteString("\n")
lines := strings.Split(string(stack), "\n")
if len(lines) == 0 {
return
}
// Format goroutine line
b.WriteString(" ┌─ ")
b.WriteString(h.palette.Goroutine)
b.WriteString(lines[0])
b.WriteString(h.palette.Reset)
b.WriteString("\n")
// Pair function name and file path lines
for i := 1; i < len(lines)-1; i += 2 {
funcLine := strings.TrimSpace(lines[i])
pathLine := strings.TrimSpace(lines[i+1])
if funcLine != "" {
b.WriteString(" │ ")
b.WriteString(h.palette.Func)
b.WriteString(funcLine)
b.WriteString(h.palette.Reset)
b.WriteString("\n")
}
if pathLine != "" {
b.WriteString(" │ ")
// Look for last "/" before ".go:"
lastSlash := strings.LastIndex(pathLine, "/")
goIndex := strings.Index(pathLine, ".go:")
if lastSlash >= 0 && goIndex > lastSlash {
// Prefix path
prefix := pathLine[:lastSlash+1]
// File and line (e.g., ll.go:698 +0x5c)
suffix := pathLine[lastSlash+1:]
b.WriteString(h.palette.Path)
b.WriteString(prefix)
b.WriteString(h.palette.Reset)
b.WriteString(h.palette.Path) // Use mainPath color for suffix
b.WriteString(suffix)
b.WriteString(h.palette.Reset)
} else {
// Fallback: whole line is gray
b.WriteString(h.palette.Path)
b.WriteString(pathLine)
b.WriteString(h.palette.Reset)
}
b.WriteString("\n")
}
}
// Handle any remaining unpaired line
if len(lines)%2 == 0 && strings.TrimSpace(lines[len(lines)-1]) != "" {
b.WriteString(" │ ")
b.WriteString(h.palette.Func)
b.WriteString(strings.TrimSpace(lines[len(lines)-1]))
b.WriteString(h.palette.Reset)
b.WriteString("\n")
}
b.WriteString(" └\n")
}
// handleDumpOutput formats hex dump output with ANSI color codes.
// It applies colors to position, hex, ASCII, and title components of the dump,
// wrapping the output with colored BEGIN/END separators.
// Returns an error if writing fails.
// Example (internal usage):
//
// h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes colored dump
func (h *ColorizedHandler) handleDumpOutput(e *lx.Entry) error {
var builder strings.Builder
// Write colored BEGIN separator
builder.WriteString(h.palette.Title)
builder.WriteString("---- BEGIN DUMP ----")
builder.WriteString(h.palette.Reset)
builder.WriteString("\n")
// Process each line of the dump
lines := strings.Split(e.Message, "\n")
length := len(lines)
for i, line := range lines {
if strings.HasPrefix(line, "pos ") {
// Parse and color position and hex/ASCII parts
parts := strings.SplitN(line, "hex:", 2)
if len(parts) == 2 {
builder.WriteString(h.palette.Pos)
builder.WriteString(parts[0])
builder.WriteString(h.palette.Reset)
hexAscii := strings.SplitN(parts[1], "'", 2)
builder.WriteString(h.palette.Hex)
builder.WriteString("hex:")
builder.WriteString(hexAscii[0])
builder.WriteString(h.palette.Reset)
if len(hexAscii) > 1 {
builder.WriteString(h.palette.Ascii)
builder.WriteString("'")
builder.WriteString(hexAscii[1])
builder.WriteString(h.palette.Reset)
}
}
} else if strings.HasPrefix(line, "Dumping value of type:") {
// Color type dump lines
builder.WriteString(h.palette.Header)
builder.WriteString(line)
builder.WriteString(h.palette.Reset)
} else {
// Write non-dump lines as-is
builder.WriteString(line)
}
// Don't add newline for the last line
if i < length-1 {
builder.WriteString("\n")
}
}
// Write colored END separator
builder.WriteString(h.palette.Title)
builder.WriteString("---- END DUMP ----")
builder.WriteString(h.palette.Reset)
builder.WriteString("\n")
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
return err
}
// detectPalette selects a color palette based on terminal environment variables.
// It checks TERM_BACKGROUND, COLORFGBG, and AppleInterfaceStyle to determine
// whether a light or dark palette is appropriate, defaulting to darkPalette.
// Example (internal usage):
//
// palette := h.detectPalette() // Returns darkPalette or lightPalette
func (h *ColorizedHandler) detectPalette() Palette {
// Check TERM_BACKGROUND (e.g., iTerm2)
if bg, ok := os.LookupEnv("TERM_BACKGROUND"); ok {
if bg == "light" {
return lightPalette // Use light palette for light background
}
return darkPalette // Use dark palette otherwise
}
// Check COLORFGBG (traditional xterm)
if fgBg, ok := os.LookupEnv("COLORFGBG"); ok {
parts := strings.Split(fgBg, ";")
if len(parts) >= 2 {
bg := parts[len(parts)-1] // Last part (some terminals add more fields)
if bg == "7" || bg == "15" || bg == "0;15" { // Handle variations
return lightPalette // Use light palette for light background
}
}
}
// Check macOS dark mode
if style, ok := os.LookupEnv("AppleInterfaceStyle"); ok && strings.EqualFold(style, "dark") {
return darkPalette // Use dark palette for macOS dark mode
}
// Default: dark (conservative choice for terminals)
return darkPalette
}

170
vendor/github.com/olekukonko/ll/lh/json.go generated vendored Normal file
View File

@@ -0,0 +1,170 @@
package lh
import (
"encoding/json"
"fmt"
"github.com/olekukonko/ll/lx"
"io"
"os"
"strings"
"sync"
"time"
)
// JSONHandler is a handler that outputs log entries as JSON objects.
// It formats log entries with timestamp, level, message, namespace, fields, and optional
// stack traces or dump segments, writing the result to the provided writer.
// Thread-safe with a mutex to protect concurrent writes.
type JSONHandler struct {
writer io.Writer // Destination for JSON output
timeFmt string // Format for timestamp (default: RFC3339Nano)
pretty bool // Enable pretty printing with indentation if true
fieldMap map[string]string // Optional mapping for field names (not used in provided code)
mu sync.Mutex // Protects concurrent access to writer
}
// JsonOutput represents the JSON structure for a log entry.
// It includes all relevant log data, such as timestamp, level, message, and optional
// stack trace or dump segments, serialized as a JSON object.
type JsonOutput struct {
Time string `json:"ts"` // Timestamp in specified format
Level string `json:"lvl"` // Log level (e.g., "INFO")
Class string `json:"class"` // Entry class (e.g., "Text", "Dump")
Msg string `json:"msg"` // Log message
Namespace string `json:"ns"` // Namespace path
Stack []byte `json:"stack"` // Stack trace (if present)
Dump []dumpSegment `json:"dump"` // Hex/ASCII dump segments (for ClassDump)
Fields map[string]interface{} `json:"fields"` // Custom fields
}
// dumpSegment represents a single segment of a hex/ASCII dump.
// Used for ClassDump entries to structure position, hex values, and ASCII representation.
type dumpSegment struct {
Offset int `json:"offset"` // Starting byte offset of the segment
Hex []string `json:"hex"` // Hexadecimal values of bytes
ASCII string `json:"ascii"` // ASCII representation of bytes
}
// NewJSONHandler creates a new JSONHandler writing to the specified writer.
// It initializes the handler with a default timestamp format (RFC3339Nano) and optional
// configuration functions to customize settings like pretty printing.
// Example:
//
// handler := NewJSONHandler(os.Stdout)
// logger := ll.New("app").Enable().Handler(handler)
// logger.Info("Test") // Output: {"ts":"...","lvl":"INFO","class":"Text","msg":"Test","ns":"app","stack":null,"dump":null,"fields":null}
func NewJSONHandler(w io.Writer, opts ...func(*JSONHandler)) *JSONHandler {
h := &JSONHandler{
writer: w, // Set output writer
timeFmt: time.RFC3339Nano, // Default timestamp format
}
// Apply configuration options
for _, opt := range opts {
opt(h)
}
return h
}
// Handle processes a log entry and writes it as JSON.
// It delegates to specialized methods based on the entry's class (Dump or regular),
// ensuring thread-safety with a mutex.
// Returns an error if JSON encoding or writing fails.
// Example:
//
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
func (h *JSONHandler) Handle(e *lx.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
// Handle dump entries separately
if e.Class == lx.ClassDump {
return h.handleDump(e)
}
// Handle standard log entries
return h.handleRegular(e)
}
// handleRegular handles standard log entries (non-dump).
// It converts the entry to a JsonOutput struct and encodes it as JSON,
// applying pretty printing if enabled. Logs encoding errors to stderr for debugging.
// Returns an error if encoding or writing fails.
// Example (internal usage):
//
// h.handleRegular(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
func (h *JSONHandler) handleRegular(e *lx.Entry) error {
// Create JSON output structure
entry := JsonOutput{
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
Level: e.Level.String(), // Convert level to string
Class: e.Class.String(), // Convert class to string
Msg: e.Message, // Set message
Namespace: e.Namespace, // Set namespace
Dump: nil, // No dump for regular entries
Fields: e.Fields, // Copy fields
Stack: e.Stack, // Include stack trace if present
}
// Create JSON encoder
enc := json.NewEncoder(h.writer)
if h.pretty {
// Enable indentation for pretty printing
enc.SetIndent("", " ")
}
// Log encoding attempt for debugging
fmt.Fprintf(os.Stderr, "Encoding JSON entry: %v\n", e.Message)
// Encode and write JSON
err := enc.Encode(entry)
if err != nil {
// Log encoding error for debugging
fmt.Fprintf(os.Stderr, "JSON encode error: %v\n", err)
}
return err
}
// handleDump processes ClassDump entries, converting hex dump output to JSON segments.
// It parses the dump message into structured segments with offset, hex, and ASCII data,
// encoding them as a JsonOutput struct.
// Returns an error if parsing or encoding fails.
// Example (internal usage):
//
// h.handleDump(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes JSON with dump segments
func (h *JSONHandler) handleDump(e *lx.Entry) error {
var segments []dumpSegment
lines := strings.Split(e.Message, "\n")
// Parse each line of the dump message
for _, line := range lines {
if !strings.HasPrefix(line, "pos") {
continue // Skip non-dump lines
}
parts := strings.SplitN(line, "hex:", 2)
if len(parts) != 2 {
continue // Skip invalid lines
}
// Parse position
var offset int
fmt.Sscanf(parts[0], "pos %d", &offset)
// Parse hex and ASCII
hexAscii := strings.SplitN(parts[1], "'", 2)
hexStr := strings.Fields(strings.TrimSpace(hexAscii[0]))
// Create dump segment
segments = append(segments, dumpSegment{
Offset: offset, // Set byte offset
Hex: hexStr, // Set hex values
ASCII: strings.Trim(hexAscii[1], "'"), // Set ASCII representation
})
}
// Encode JSON output with dump segments
return json.NewEncoder(h.writer).Encode(JsonOutput{
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
Level: e.Level.String(), // Convert level to string
Class: e.Class.String(), // Convert class to string
Msg: "dumping segments", // Fixed message for dumps
Namespace: e.Namespace, // Set namespace
Dump: segments, // Include parsed segments
Fields: e.Fields, // Copy fields
Stack: e.Stack, // Include stack trace if present
})
}

93
vendor/github.com/olekukonko/ll/lh/memory.go generated vendored Normal file
View File

@@ -0,0 +1,93 @@
package lh
import (
"fmt"
"github.com/olekukonko/ll/lx"
"io"
"sync"
)
// MemoryHandler is an lx.Handler that stores log entries in memory.
// Useful for testing or buffering logs for later inspection.
// It maintains a thread-safe slice of log entries, protected by a read-write mutex.
type MemoryHandler struct {
mu sync.RWMutex // Protects concurrent access to entries
entries []*lx.Entry // Slice of stored log entries
}
// NewMemoryHandler creates a new MemoryHandler.
// It initializes an empty slice for storing log entries, ready for use in logging or testing.
// Example:
//
// handler := NewMemoryHandler()
// logger := ll.New("app").Enable().Handler(handler)
// logger.Info("Test") // Stores entry in memory
func NewMemoryHandler() *MemoryHandler {
return &MemoryHandler{
entries: make([]*lx.Entry, 0), // Initialize empty slice for entries
}
}
// Handle stores the log entry in memory.
// It appends the provided entry to the entries slice, ensuring thread-safety with a write lock.
// Always returns nil, as it does not perform I/O operations.
// Example:
//
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Stores entry
func (h *MemoryHandler) Handle(entry *lx.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
h.entries = append(h.entries, entry) // Append entry to slice
return nil
}
// Entries returns a copy of the stored log entries.
// It creates a new slice with copies of all entries, ensuring thread-safety with a read lock.
// The returned slice is safe for external use without affecting the handler's internal state.
// Example:
//
// entries := handler.Entries() // Returns copy of stored entries
func (h *MemoryHandler) Entries() []*lx.Entry {
h.mu.RLock()
defer h.mu.RUnlock()
entries := make([]*lx.Entry, len(h.entries)) // Create new slice for copy
copy(entries, h.entries) // Copy entries to new slice
return entries
}
// Reset clears all stored entries.
// It truncates the entries slice to zero length, preserving capacity, using a write lock for thread-safety.
// Example:
//
// handler.Reset() // Clears all stored entries
func (h *MemoryHandler) Reset() {
h.mu.Lock()
defer h.mu.Unlock()
h.entries = h.entries[:0] // Truncate slice to zero length
}
// Dump writes all stored log entries to the provided io.Writer in text format.
// Entries are formatted as they would be by a TextHandler, including namespace, level,
// message, and fields. Thread-safe with read lock.
// Returns an error if writing fails.
// Example:
//
// logger := ll.New("test", ll.WithHandler(NewMemoryHandler())).Enable()
// logger.Info("Test message")
// handler := logger.handler.(*MemoryHandler)
// handler.Dump(os.Stdout) // Output: [test] INFO: Test message
func (h *MemoryHandler) Dump(w io.Writer) error {
h.mu.RLock()
defer h.mu.RUnlock()
// Create a temporary TextHandler to format entries
tempHandler := NewTextHandler(w)
// Process each entry through the TextHandler
for _, entry := range h.entries {
if err := tempHandler.Handle(entry); err != nil {
return fmt.Errorf("failed to dump entry: %w", err) // Wrap and return write errors
}
}
return nil
}

51
vendor/github.com/olekukonko/ll/lh/multi.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
package lh
import (
"errors"
"fmt"
"github.com/olekukonko/ll/lx"
)
// MultiHandler combines multiple handlers to process log entries concurrently.
// It holds a list of lx.Handler instances and delegates each log entry to all handlers,
// collecting any errors into a single combined error.
// Thread-safe if the underlying handlers are thread-safe.
type MultiHandler struct {
Handlers []lx.Handler // List of handlers to process each log entry
}
// NewMultiHandler creates a new MultiHandler with the specified handlers.
// It accepts a variadic list of handlers to be executed in order.
// The returned handler processes log entries by passing them to each handler in sequence.
// Example:
//
// textHandler := NewTextHandler(os.Stdout)
// jsonHandler := NewJSONHandler(os.Stdout)
// multi := NewMultiHandler(textHandler, jsonHandler)
// logger := ll.New("app").Enable().Handler(multi)
// logger.Info("Test") // Processed by both text and JSON handlers
func NewMultiHandler(h ...lx.Handler) *MultiHandler {
return &MultiHandler{
Handlers: h, // Initialize with provided handlers
}
}
// Handle implements the Handler interface, calling Handle on each handler in sequence.
// It collects any errors from handlers and combines them into a single error using errors.Join.
// If no errors occur, it returns nil. Thread-safe if the underlying handlers are thread-safe.
// Example:
//
// multi.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Calls Handle on all handlers
func (h *MultiHandler) Handle(e *lx.Entry) error {
var errs []error // Collect errors from handlers
for i, handler := range h.Handlers {
// Process entry with each handler
if err := handler.Handle(e); err != nil {
// fmt.Fprintf(os.Stderr, "MultiHandler error for handler %d: %v\n", i, err)
// Wrap error with handler index for context
errs = append(errs, fmt.Errorf("handler %d: %w", i, err))
}
}
// Combine errors into a single error, or return nil if no errors
return errors.Join(errs...)
}

89
vendor/github.com/olekukonko/ll/lh/slog.go generated vendored Normal file
View File

@@ -0,0 +1,89 @@
package lh
import (
"context"
"github.com/olekukonko/ll/lx"
"log/slog"
)
// SlogHandler adapts a slog.Handler to implement lx.Handler.
// It converts lx.Entry objects to slog.Record objects and delegates to an underlying
// slog.Handler for processing, enabling compatibility with Go's standard slog package.
// Thread-safe if the underlying slog.Handler is thread-safe.
type SlogHandler struct {
slogHandler slog.Handler // Underlying slog.Handler for processing log records
}
// NewSlogHandler creates a new SlogHandler wrapping the provided slog.Handler.
// It initializes the handler with the given slog.Handler, allowing lx.Entry logs to be
// processed by slog's logging infrastructure.
// Example:
//
// slogText := slog.NewTextHandler(os.Stdout, nil)
// handler := NewSlogHandler(slogText)
// logger := ll.New("app").Enable().Handler(handler)
// logger.Info("Test") // Output: level=INFO msg=Test namespace=app class=Text
func NewSlogHandler(h slog.Handler) *SlogHandler {
return &SlogHandler{slogHandler: h}
}
// Handle converts an lx.Entry to slog.Record and delegates to the slog.Handler.
// It maps the entry's fields, level, namespace, class, and stack trace to slog attributes,
// passing the resulting record to the underlying slog.Handler.
// Returns an error if the slog.Handler fails to process the record.
// Thread-safe if the underlying slog.Handler is thread-safe.
// Example:
//
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Processes as slog record
func (h *SlogHandler) Handle(e *lx.Entry) error {
// Convert lx.LevelType to slog.Level
level := toSlogLevel(e.Level)
// Create a slog.Record with the entry's data
record := slog.NewRecord(
e.Timestamp, // time.Time for log timestamp
level, // slog.Level for log severity
e.Message, // string for log message
0, // pc (program counter, optional, not used)
)
// Add standard fields as attributes
record.AddAttrs(
slog.String("namespace", e.Namespace), // Add namespace as string attribute
slog.String("class", e.Class.String()), // Add class as string attribute
)
// Add stack trace if present
if len(e.Stack) > 0 {
record.AddAttrs(slog.String("stack", string(e.Stack))) // Add stack trace as string
}
// Add custom fields
for k, v := range e.Fields {
record.AddAttrs(slog.Any(k, v)) // Add each field as a key-value attribute
}
// Handle the record with the underlying slog.Handler
return h.slogHandler.Handle(context.Background(), record)
}
// toSlogLevel converts lx.LevelType to slog.Level.
// It maps the logging levels used by the lx package to those used by slog,
// defaulting to slog.LevelInfo for unknown levels.
// Example (internal usage):
//
// level := toSlogLevel(lx.LevelDebug) // Returns slog.LevelDebug
func toSlogLevel(level lx.LevelType) slog.Level {
switch level {
case lx.LevelDebug:
return slog.LevelDebug
case lx.LevelInfo:
return slog.LevelInfo
case lx.LevelWarn:
return slog.LevelWarn
case lx.LevelError:
return slog.LevelError
default:
return slog.LevelInfo // Default for unknown levels
}
}

193
vendor/github.com/olekukonko/ll/lh/text.go generated vendored Normal file
View File

@@ -0,0 +1,193 @@
package lh
import (
"fmt"
"github.com/olekukonko/ll/lx"
"io"
"sort"
"strings"
)
// TextHandler is a handler that outputs log entries as plain text.
// It formats log entries with namespace, level, message, fields, and optional stack traces,
// writing the result to the provided writer.
// Thread-safe if the underlying writer is thread-safe.
type TextHandler struct {
w io.Writer // Destination for formatted log output
}
// NewTextHandler creates a new TextHandler writing to the specified writer.
// It initializes the handler with the given writer, suitable for outputs like stdout or files.
// Example:
//
// handler := NewTextHandler(os.Stdout)
// logger := ll.New("app").Enable().Handler(handler)
// logger.Info("Test") // Output: [app] INFO: Test
func NewTextHandler(w io.Writer) *TextHandler {
return &TextHandler{w: w}
}
// Handle processes a log entry and writes it as plain text.
// It delegates to specialized methods based on the entry's class (Dump, Raw, or regular).
// Returns an error if writing to the underlying writer fails.
// Thread-safe if the writer is thread-safe.
// Example:
//
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
func (h *TextHandler) Handle(e *lx.Entry) error {
// Special handling for dump output
if e.Class == lx.ClassDump {
return h.handleDumpOutput(e)
}
// Raw entries are written directly without formatting
if e.Class == lx.ClassRaw {
_, err := h.w.Write([]byte(e.Message))
return err
}
// Handle standard log entries
return h.handleRegularOutput(e)
}
// handleRegularOutput handles normal log entries.
// It formats the entry with namespace, level, message, fields, and stack trace (if present),
// writing the result to the handler's writer.
// Returns an error if writing fails.
// Example (internal usage):
//
// h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
func (h *TextHandler) handleRegularOutput(e *lx.Entry) error {
var builder strings.Builder // Buffer for building formatted output
// Format namespace based on style
switch e.Style {
case lx.NestedPath:
if e.Namespace != "" {
// Split namespace into parts and format as [parent]→[child]
parts := strings.Split(e.Namespace, lx.Slash)
for i, part := range parts {
builder.WriteString(lx.LeftBracket)
builder.WriteString(part)
builder.WriteString(lx.RightBracket)
if i < len(parts)-1 {
builder.WriteString(lx.Arrow)
}
}
builder.WriteString(lx.Colon)
builder.WriteString(lx.Space)
}
default: // FlatPath
if e.Namespace != "" {
// Format namespace as [parent/child]
builder.WriteString(lx.LeftBracket)
builder.WriteString(e.Namespace)
builder.WriteString(lx.RightBracket)
builder.WriteString(lx.Space)
}
}
// Add level and message
builder.WriteString(e.Level.String())
builder.WriteString(lx.Colon)
builder.WriteString(lx.Space)
builder.WriteString(e.Message)
// Add fields in sorted order
if len(e.Fields) > 0 {
var keys []string
for k := range e.Fields {
keys = append(keys, k)
}
// Sort keys for consistent output
sort.Strings(keys)
builder.WriteString(lx.Space)
builder.WriteString(lx.LeftBracket)
for i, k := range keys {
if i > 0 {
builder.WriteString(lx.Space)
}
// Format field as key=value
builder.WriteString(k)
builder.WriteString("=")
builder.WriteString(fmt.Sprint(e.Fields[k]))
}
builder.WriteString(lx.RightBracket)
}
// Add stack trace if present
if len(e.Stack) > 0 {
h.formatStack(&builder, e.Stack)
}
// Append newline for non-None levels
if e.Level != lx.LevelNone {
builder.WriteString(lx.Newline)
}
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
return err
}
// handleDumpOutput specially formats hex dump output (plain text version).
// It wraps the dump message with BEGIN/END separators for clarity.
// Returns an error if writing fails.
// Example (internal usage):
//
// h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61"}) // Writes "---- BEGIN DUMP ----\npos 00 hex: 61\n---- END DUMP ----\n"
func (h *TextHandler) handleDumpOutput(e *lx.Entry) error {
// For text handler, we just add a newline before dump output
var builder strings.Builder // Buffer for building formatted output
// Add separator lines and dump content
builder.WriteString("---- BEGIN DUMP ----\n")
builder.WriteString(e.Message)
builder.WriteString("---- END DUMP ----\n")
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
return err
}
// formatStack formats a stack trace for plain text output.
// It structures the stack trace with indentation and separators for readability,
// including goroutine and function/file details.
// Example (internal usage):
//
// h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends formatted stack trace
func (h *TextHandler) formatStack(b *strings.Builder, stack []byte) {
lines := strings.Split(string(stack), "\n")
if len(lines) == 0 {
return
}
// Start stack trace section
b.WriteString("\n[stack]\n")
// First line: goroutine
b.WriteString(" ┌─ ")
b.WriteString(lines[0])
b.WriteString("\n")
// Iterate through remaining lines
for i := 1; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
if strings.Contains(line, ".go") {
// File path lines get extra indent
b.WriteString(" ├ ")
} else {
// Function names
b.WriteString(" │ ")
}
b.WriteString(line)
b.WriteString("\n")
}
// End stack trace section
b.WriteString(" └\n")
}

1368
vendor/github.com/olekukonko/ll/ll.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

154
vendor/github.com/olekukonko/ll/lx/lx.go generated vendored Normal file
View File

@@ -0,0 +1,154 @@
package lx
import (
"time"
)
// Formatting constants for log output.
// These constants define the characters used to format log messages, ensuring consistency
// across handlers (e.g., text, JSON, colorized). They are used to construct namespace paths,
// level indicators, and field separators in log entries.
const (
Space = " " // Single space for separating elements (e.g., between level and message)
DoubleSpace = " " // Double space for indentation (e.g., for hierarchical output)
Slash = "/" // Separator for namespace paths (e.g., "parent/child")
Arrow = "→" // Arrow for NestedPath style namespaces (e.g., [parent]→[child])
LeftBracket = "[" // Opening bracket for namespaces and fields (e.g., [app])
RightBracket = "]" // Closing bracket for namespaces and fields (e.g., [app])
Colon = ":" // Separator after namespace or level (e.g., [app]: INFO:)
Dot = "." // Separator for namespace paths (e.g., "parent.child")
Newline = "\n" // Newline for separating log entries or stack trace lines
)
// DefaultEnabled defines the default logging state (disabled).
// It specifies whether logging is enabled by default for new Logger instances in the ll package.
// Set to false to prevent logging until explicitly enabled.
const (
DefaultEnabled = false // Default state for new loggers (disabled)
)
// Log level constants, ordered by increasing severity.
// These constants define the severity levels for log messages, used to filter logs based
// on the loggers minimum level. They are ordered to allow comparison (e.g., LevelDebug < LevelWarn).
const (
LevelNone LevelType = iota // Debug level for detailed diagnostic information
LevelInfo // Info level for general operational messages
LevelWarn // Warn level for warning conditions
LevelError // Error level for error conditions requiring attention
LevelDebug // None level for logs without a specific severity (e.g., raw output)
)
// Log class constants, defining the type of log entry.
// These constants categorize log entries by their content or purpose, influencing how
// handlers process them (e.g., text, JSON, hex dump).
const (
ClassText ClassType = iota // Text entries for standard log messages
ClassJSON // JSON entries for structured output
ClassDump // Dump entries for hex/ASCII dumps
ClassSpecial // Special entries for custom or non-standard logs
ClassRaw // Raw entries for unformatted output
)
// Namespace style constants.
// These constants define how namespace paths are formatted in log output, affecting the
// visual representation of hierarchical namespaces.
const (
FlatPath StyleType = iota // Formats namespaces as [parent/child]
NestedPath // Formats namespaces as [parent]→[child]
)
// LevelType represents the severity of a log message.
// It is an integer type used to define log levels (Debug, Info, Warn, Error, None), with associated
// string representations for display in log output.
type LevelType int
// String converts a LevelType to its string representation.
// It maps each level constant to a human-readable string, returning "UNKNOWN" for invalid levels.
// Used by handlers to display the log level in output.
// Example:
//
// var level lx.LevelType = lx.LevelInfo
// fmt.Println(level.String()) // Output: INFO
func (l LevelType) String() string {
switch l {
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
case LevelNone:
return "NONE"
default:
return "UNKNOWN"
}
}
// StyleType defines how namespace paths are formatted in log output.
// It is an integer type used to select between FlatPath ([parent/child]) and NestedPath
// ([parent]→[child]) styles, affecting how handlers render namespace hierarchies.
type StyleType int
// Entry represents a single log entry passed to handlers.
// It encapsulates all information about a log message, including its timestamp, severity,
// content, namespace, metadata, and formatting style. Handlers process Entry instances
// to produce formatted output (e.g., text, JSON). The struct is immutable once created,
// ensuring thread-safety in handler processing.
type Entry struct {
Timestamp time.Time // Time the log was created
Level LevelType // Severity level of the log (Debug, Info, Warn, Error, None)
Message string // Log message content
Namespace string // Namespace path (e.g., "parent/child")
Fields map[string]interface{} // Additional key-value metadata (e.g., {"user": "alice"})
Style StyleType // Namespace formatting style (FlatPath or NestedPath)
Error error // Associated error, if any (e.g., for error logs)
Class ClassType // Type of log entry (Text, JSON, Dump, Special, Raw)
Stack []byte // Stack trace data (if present)
Id int `json:"-"` // Unique ID for the entry, ignored in JSON output
}
// Handler defines the interface for processing log entries.
// Implementations (e.g., TextHandler, JSONHandler) format and output log entries to various
// destinations (e.g., stdout, files). The Handle method returns an error if processing fails,
// allowing the logger to handle output failures gracefully.
// Example (simplified handler implementation):
//
// type MyHandler struct{}
// func (h *MyHandler) Handle(e *Entry) error {
// fmt.Printf("[%s] %s: %s\n", e.Namespace, e.Level.String(), e.Message)
// return nil
// }
type Handler interface {
Handle(e *Entry) error // Processes a log entry, returning any error
}
// ClassType represents the type of a log entry.
// It is an integer type used to categorize log entries (Text, JSON, Dump, Special, Raw),
// influencing how handlers process and format them.
type ClassType int
// String converts a ClassType to its string representation.
// It maps each class constant to a human-readable string, returning "UNKNOWN" for invalid classes.
// Used by handlers to indicate the entry type in output (e.g., JSON fields).
// Example:
//
// var class lx.ClassType = lx.ClassText
// fmt.Println(class.String()) // Output: TEST
func (t ClassType) String() string {
switch t {
case ClassText:
return "TEST" // Note: Likely a typo, should be "TEXT"
case ClassJSON:
return "JSON"
case ClassDump:
return "DUMP"
case ClassSpecial:
return "SPECIAL"
case ClassRaw:
return "RAW"
default:
return "UNKNOWN"
}
}

102
vendor/github.com/olekukonko/ll/lx/ns.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
package lx
import (
"strings"
"sync"
)
// namespaceRule stores the cached result of Enabled.
type namespaceRule struct {
isEnabledByRule bool
isDisabledByRule bool
}
// Namespace manages thread-safe namespace enable/disable states with caching.
// The store holds explicit user-defined rules (path -> bool).
// The cache holds computed effective states for paths (path -> namespaceRule)
// based on hierarchical rules to optimize lookups.
type Namespace struct {
store sync.Map // path (string) -> rule (bool: true=enable, false=disable)
cache sync.Map // path (string) -> namespaceRule
}
// Set defines an explicit enable/disable rule for a namespace path.
// It clears the cache to ensure subsequent lookups reflect the change.
func (ns *Namespace) Set(path string, enabled bool) {
ns.store.Store(path, enabled)
ns.clearCache()
}
// Load retrieves an explicit rule from the store for a path.
// Returns the rule (true=enable, false=disable) and whether it exists.
// Does not consider hierarchy or caching.
func (ns *Namespace) Load(path string) (rule interface{}, found bool) {
return ns.store.Load(path)
}
// Store directly sets a rule in the store, bypassing cache invalidation.
// Intended for internal use or sync.Map parity; prefer Set for standard use.
func (ns *Namespace) Store(path string, rule bool) {
ns.store.Store(path, rule)
}
// clearCache clears the cache of Enabled results.
// Called by Set to ensure consistency after rule changes.
func (ns *Namespace) clearCache() {
ns.cache.Range(func(key, _ interface{}) bool {
ns.cache.Delete(key)
return true
})
}
// Enabled checks if a path is enabled by namespace rules, considering the most
// specific rule (path or closest prefix) in the store. Results are cached.
// Args:
// - path: Absolute namespace path to check.
// - separator: Character delimiting path segments (e.g., "/", ".").
//
// Returns:
// - isEnabledByRule: True if an explicit rule enables the path.
// - isDisabledByRule: True if an explicit rule disables the path.
//
// If both are false, no explicit rule applies to the path or its prefixes.
func (ns *Namespace) Enabled(path string, separator string) (isEnabledByRule bool, isDisabledByRule bool) {
if path == "" { // Root path has no explicit rule
return false, false
}
// Check cache
if cachedValue, found := ns.cache.Load(path); found {
if state, ok := cachedValue.(namespaceRule); ok {
return state.isEnabledByRule, state.isDisabledByRule
}
ns.cache.Delete(path) // Remove invalid cache entry
}
// Compute: Most specific rule wins
parts := strings.Split(path, separator)
computedIsEnabled := false
computedIsDisabled := false
for i := len(parts); i >= 1; i-- {
currentPrefix := strings.Join(parts[:i], separator)
if val, ok := ns.store.Load(currentPrefix); ok {
if rule := val.(bool); rule {
computedIsEnabled = true
computedIsDisabled = false
} else {
computedIsEnabled = false
computedIsDisabled = true
}
break
}
}
// Cache result, including (false, false) for no rule
ns.cache.Store(path, namespaceRule{
isEnabledByRule: computedIsEnabled,
isDisabledByRule: computedIsDisabled,
})
return computedIsEnabled, computedIsDisabled
}

124
vendor/github.com/olekukonko/ll/middleware.go generated vendored Normal file
View File

@@ -0,0 +1,124 @@
package ll
import (
"github.com/olekukonko/ll/lx"
)
// Middleware represents a registered middleware and its operations in the logging pipeline.
// It holds an ID for identification, a reference to the parent logger, and the handler function
// that processes log entries. Middleware is used to transform or filter log entries before they
// are passed to the logger's output handler.
type Middleware struct {
id int // Unique identifier for the middleware
logger *Logger // Parent logger instance for context and logging operations
fn lx.Handler // Handler function that processes log entries
}
// Remove unregisters the middleware from the loggers middleware chain.
// It safely removes the middleware by its ID, ensuring thread-safety with a mutex lock.
// If the middleware or logger is nil, it returns early to prevent panics.
// Example usage:
//
// // Using a named middleware function
// mw := logger.Use(authMiddleware)
// defer mw.Remove()
//
// // Using an inline middleware
// mw = logger.Use(ll.Middle(func(e *lx.Entry) error {
// if e.Level < lx.LevelWarn {
// return fmt.Errorf("level too low")
// }
// return nil
// }))
// defer mw.Remove()
func (m *Middleware) Remove() {
// Check for nil middleware or logger to avoid panics
if m == nil || m.logger == nil {
return
}
// Acquire write lock to modify middleware slice
m.logger.mu.Lock()
defer m.logger.mu.Unlock()
// Iterate through middleware slice to find and remove matching ID
for i, entry := range m.logger.middleware {
if entry.id == m.id {
// Remove middleware by slicing out the matching entry
m.logger.middleware = append(m.logger.middleware[:i], m.logger.middleware[i+1:]...)
return
}
}
}
// Logger returns the parent logger for optional chaining.
// This allows middleware to access the logger for additional operations, such as logging errors
// or creating derived loggers. It is useful for fluent API patterns.
// Example:
//
// mw := logger.Use(authMiddleware)
// mw.Logger().Info("Middleware registered")
func (m *Middleware) Logger() *Logger {
return m.logger
}
// Error logs an error message at the Error level if the middleware blocks a log entry.
// It uses the parent logger to emit the error and returns the middleware for chaining.
// This is useful for debugging or auditing when middleware rejects a log.
// Example:
//
// mw := logger.Use(ll.Middle(func(e *lx.Entry) error {
// if e.Level < lx.LevelWarn {
// return fmt.Errorf("level too low")
// }
// return nil
// }))
// mw.Error("Rejected low-level log")
func (m *Middleware) Error(args ...any) *Middleware {
m.logger.Error(args...)
return m
}
// Errorf logs an error message at the Error level if the middleware blocks a log entry.
// It uses the parent logger to emit the error and returns the middleware for chaining.
// This is useful for debugging or auditing when middleware rejects a log.
// Example:
//
// mw := logger.Use(ll.Middle(func(e *lx.Entry) error {
// if e.Level < lx.LevelWarn {
// return fmt.Errorf("level too low")
// }
// return nil
// }))
// mw.Errorf("Rejected low-level log")
func (m *Middleware) Errorf(format string, args ...any) *Middleware {
m.logger.Errorf(format, args...)
return m
}
// middlewareFunc is a function adapter that implements the lx.Handler interface.
// It allows plain functions with the signature `func(*lx.Entry) error` to be used as middleware.
// The function should return nil to allow the log to proceed or a non-nil error to reject it,
// stopping the log from being emitted by the logger.
type middlewareFunc func(*lx.Entry) error
// Handle implements the lx.Handler interface for middlewareFunc.
// It calls the underlying function with the log entry and returns its result.
// This enables seamless integration of function-based middleware into the logging pipeline.
func (mf middlewareFunc) Handle(e *lx.Entry) error {
return mf(e)
}
// Middle creates a middleware handler from a function.
// It wraps a function with the signature `func(*lx.Entry) error` into a middlewareFunc,
// allowing it to be used in the loggers middleware pipeline. A non-nil error returned by
// the function will stop the log from being emitted, ensuring precise control over logging.
// Example:
//
// logger.Use(ll.Middle(func(e *lx.Entry) error {
// if e.Level == lx.LevelDebug {
// return fmt.Errorf("debug logs disabled")
// }
// return nil
// }))
func Middle(fn func(*lx.Entry) error) lx.Handler {
return middlewareFunc(fn)
}

View File

@@ -1,15 +1,10 @@
# Created by .ignore support plugin (hsz.mobi)
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# folders
.idea
.vscode
/tmp
/lab
dev.sh
*csv2table
_test/

View File

@@ -1,22 +0,0 @@
language: go
arch:
- ppc64le
- amd64
go:
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7
- 1.8
- 1.9
- "1.10"
- tip
jobs:
exclude :
- arch : ppc64le
go :
- 1.3
- arch : ppc64le
go :
- 1.4

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,466 @@
ASCII Table Writer
=========
[![ci](https://github.com/olekukonko/tablewriter/workflows/ci/badge.svg?branch=master)](https://github.com/olekukonko/tablewriter/actions?query=workflow%3Aci)
[![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter)
[![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter)
## Important Notice: Modernization in Progress
The `tablewriter` package is being modernized on the `prototype` branch with generics, streaming support, and a modular design, targeting `v0.2.0`. Until this is released:
**For Production Use**: Use the stable version `v0.0.5`:
```bash
go get github.com/olekukonko/tablewriter@v0.0.5
```
####
For Development Preview: Try the in-progress version (unstable)
```bash
go get github.com/olekukonko/tablewriter@master
```
#### Features
- Automatic Padding
- Support Multiple Lines
- Supports Alignment
- Support Custom Separators
- Automatic Alignment of numbers & percentage
- Write directly to http , file etc via `io.Writer`
- Read directly from CSV file
- Optional row line via `SetRowLine`
- Normalise table header
- Make CSV Headers optional
- Enable or disable table border
- Set custom footer support
- Optional identical cells merging
- Set custom caption
- Optional reflowing of paragraphs in multi-line cells.
#### Example 1 - Basic
```go
data := [][]string{
[]string{"A", "The Good", "500"},
[]string{"B", "The Very very Bad Man", "288"},
[]string{"C", "The Ugly", "120"},
[]string{"D", "The Gopher", "800"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Name", "Sign", "Rating"})
for _, v := range data {
table.Append(v)
}
table.Render() // Send output
```
##### Output 1
```
+------+-----------------------+--------+
| NAME | SIGN | RATING |
+------+-----------------------+--------+
| A | The Good | 500 |
| B | The Very very Bad Man | 288 |
| C | The Ugly | 120 |
| D | The Gopher | 800 |
+------+-----------------------+--------+
```
#### Example 2 - Without Border / Footer / Bulk Append
```go
data := [][]string{
[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
table.EnableBorder(false) // Set Border to false
table.AppendBulk(data) // Add Bulk Data
table.Render()
```
##### Output 2
```
DATE | DESCRIPTION | CV2 | AMOUNT
-----------+--------------------------+-------+----------
1/1/2014 | Domain name | 2233 | $10.98
1/1/2014 | January Hosting | 2233 | $54.95
1/4/2014 | February Hosting | 2233 | $51.00
1/4/2014 | February Extra Bandwidth | 2233 | $30.00
-----------+--------------------------+-------+----------
TOTAL | $146 93
--------+----------
```
#### Example 3 - CSV
```go
table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true)
table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment
table.Render()
```
##### Output 3
```
+----------+--------------+------+-----+---------+----------------+
| FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA |
+----------+--------------+------+-----+---------+----------------+
| user_id | smallint(5) | NO | PRI | NULL | auto_increment |
| username | varchar(10) | NO | | NULL | |
| password | varchar(100) | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+
```
#### Example 4 - Custom Separator
```go
table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true)
table.SetRowLine(true) // Enable row line
// Change table lines
table.SetCenterSeparator("*")
table.SetColumnSeparator("╪")
table.SetRowSeparator("-")
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
```
##### Output 4
```
*------------*-----------*---------*
╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪
*------------*-----------*---------*
╪ John ╪ Barry ╪ 123456 ╪
*------------*-----------*---------*
╪ Kathy ╪ Smith ╪ 687987 ╪
*------------*-----------*---------*
╪ Bob ╪ McCornick ╪ 3979870 ╪
*------------*-----------*---------*
```
#### Example 5 - Markdown Format
```go
data := [][]string{
[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.AppendBulk(data) // Add Bulk Data
table.Render()
```
##### Output 5
```
| DATE | DESCRIPTION | CV2 | AMOUNT |
|----------|--------------------------|------|--------|
| 1/1/2014 | Domain name | 2233 | $10.98 |
| 1/1/2014 | January Hosting | 2233 | $54.95 |
| 1/4/2014 | February Hosting | 2233 | $51.00 |
| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 |
```
#### Example 6 - Identical cells merging
```go
data := [][]string{
[]string{"1/1/2014", "Domain name", "1234", "$10.98"},
[]string{"1/1/2014", "January Hosting", "2345", "$54.95"},
[]string{"1/4/2014", "February Hosting", "3456", "$51.00"},
[]string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
table.SetFooter([]string{"", "", "Total", "$146.93"})
table.SetAutoMergeCells(true)
table.SetRowLine(true)
table.AppendBulk(data)
table.Render()
```
##### Output 6
```
+----------+--------------------------+-------+---------+
| DATE | DESCRIPTION | CV2 | AMOUNT |
+----------+--------------------------+-------+---------+
| 1/1/2014 | Domain name | 1234 | $10.98 |
+ +--------------------------+-------+---------+
| | January Hosting | 2345 | $54.95 |
+----------+--------------------------+-------+---------+
| 1/4/2014 | February Hosting | 3456 | $51.00 |
+ +--------------------------+-------+---------+
| | February Extra Bandwidth | 4567 | $30.00 |
+----------+--------------------------+-------+---------+
| TOTAL | $146 93 |
+----------+--------------------------+-------+---------+
```
#### Example 7 - Identical cells merging (specify the column index to merge)
```go
data := [][]string{
[]string{"1/1/2014", "Domain name", "1234", "$10.98"},
[]string{"1/1/2014", "January Hosting", "1234", "$10.98"},
[]string{"1/4/2014", "February Hosting", "3456", "$51.00"},
[]string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
table.SetFooter([]string{"", "", "Total", "$146.93"})
table.SetAutoMergeCellsByColumnIndex([]int{2, 3})
table.SetRowLine(true)
table.AppendBulk(data)
table.Render()
```
##### Output 7
```
+----------+--------------------------+-------+---------+
| DATE | DESCRIPTION | CV2 | AMOUNT |
+----------+--------------------------+-------+---------+
| 1/1/2014 | Domain name | 1234 | $10.98 |
+----------+--------------------------+ + +
| 1/1/2014 | January Hosting | | |
+----------+--------------------------+-------+---------+
| 1/4/2014 | February Hosting | 3456 | $51.00 |
+----------+--------------------------+-------+---------+
| 1/4/2014 | February Extra Bandwidth | 4567 | $30.00 |
+----------+--------------------------+-------+---------+
| TOTAL | $146.93 |
+----------+--------------------------+-------+---------+
```
#### Table with color
```go
data := [][]string{
[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
table.EnableBorder(false) // Set Border to false
table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor},
tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor},
tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor})
table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{},
tablewriter.Colors{tablewriter.Bold},
tablewriter.Colors{tablewriter.FgHiRedColor})
table.AppendBulk(data)
table.Render()
```
#### Table with color Output
![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png)
#### Example - 8 Table Cells with Color
Individual Cell Colors from `func Rich` take precedence over Column Colors
```go
data := [][]string{
[]string{"Test1Merge", "HelloCol2 - 1", "HelloCol3 - 1", "HelloCol4 - 1"},
[]string{"Test1Merge", "HelloCol2 - 2", "HelloCol3 - 2", "HelloCol4 - 2"},
[]string{"Test1Merge", "HelloCol2 - 3", "HelloCol3 - 3", "HelloCol4 - 3"},
[]string{"Test2Merge", "HelloCol2 - 4", "HelloCol3 - 4", "HelloCol4 - 4"},
[]string{"Test2Merge", "HelloCol2 - 5", "HelloCol3 - 5", "HelloCol4 - 5"},
[]string{"Test2Merge", "HelloCol2 - 6", "HelloCol3 - 6", "HelloCol4 - 6"},
[]string{"Test2Merge", "HelloCol2 - 7", "HelloCol3 - 7", "HelloCol4 - 7"},
[]string{"Test3Merge", "HelloCol2 - 8", "HelloCol3 - 8", "HelloCol4 - 8"},
[]string{"Test3Merge", "HelloCol2 - 9", "HelloCol3 - 9", "HelloCol4 - 9"},
[]string{"Test3Merge", "HelloCol2 - 10", "HelloCol3 -10", "HelloCol4 - 10"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Col1", "Col2", "Col3", "Col4"})
table.SetFooter([]string{"", "", "Footer3", "Footer4"})
table.EnableBorder(false)
table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor},
tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor},
tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor})
table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{},
tablewriter.Colors{tablewriter.Bold},
tablewriter.Colors{tablewriter.FgHiRedColor})
colorData1 := []string{"TestCOLOR1Merge", "HelloCol2 - COLOR1", "HelloCol3 - COLOR1", "HelloCol4 - COLOR1"}
colorData2 := []string{"TestCOLOR2Merge", "HelloCol2 - COLOR2", "HelloCol3 - COLOR2", "HelloCol4 - COLOR2"}
for i, row := range data {
if i == 4 {
table.Rich(colorData1, []tablewriter.Colors{tablewriter.Colors{}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgWhiteColor}, tablewriter.Colors{}})
table.Rich(colorData2, []tablewriter.Colors{tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold, tablewriter.BgRedColor}, tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Italic, tablewriter.BgHiCyanColor}})
}
table.Append(row)
}
table.SetAutoMergeCells(true)
table.Render()
```
##### Table cells with color Output
![Table cells with Color](https://user-images.githubusercontent.com/9064687/63969376-bcd88d80-ca6f-11e9-9466-c3d954700b25.png)
#### Example 9 - Set table caption
```go
data := [][]string{
[]string{"A", "The Good", "500"},
[]string{"B", "The Very very Bad Man", "288"},
[]string{"C", "The Ugly", "120"},
[]string{"D", "The Gopher", "800"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Name", "Sign", "Rating"})
table.SetCaption(true, "Movie ratings.")
for _, v := range data {
table.Append(v)
}
table.Render() // Send output
```
Note: Caption text will wrap with total width of rendered table.
##### Output 9
```
+------+-----------------------+--------+
| NAME | SIGN | RATING |
+------+-----------------------+--------+
| A | The Good | 500 |
| B | The Very very Bad Man | 288 |
| C | The Ugly | 120 |
| D | The Gopher | 800 |
+------+-----------------------+--------+
Movie ratings.
```
#### Example 10 - Set NoWhiteSpace and TablePadding option
```go
data := [][]string{
{"node1.example.com", "Ready", "compute", "1.11"},
{"node2.example.com", "Ready", "compute", "1.11"},
{"node3.example.com", "Ready", "compute", "1.11"},
{"node4.example.com", "NotReady", "compute", "1.11"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Name", "Status", "Role", "Version"})
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.EnableBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
table.AppendBulk(data) // Add Bulk Data
table.Render()
```
##### Output 10
```
NAME STATUS ROLE VERSION
node1.example.com Ready compute 1.11
node2.example.com Ready compute 1.11
node3.example.com Ready compute 1.11
node4.example.com NotReady compute 1.11
```
#### Render table into a string
Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the
`strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example:
```go
package main
import (
"strings"
"fmt"
"github.com/olekukonko/tablewriter"
)
func main() {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
/*
* Code to fill the table
*/
table.Render()
fmt.Println(tableString.String())
}
```
#### TODO
- ~~Import Directly from CSV~~ - `done`
- ~~Support for `SetFooter`~~ - `done`
- ~~Support for `SetBorder`~~ - `done`
- ~~Support table with uneven rows~~ - `done`
- ~~Support custom alignment~~
- General Improvement & Optimisation
- `NewHTML` Parse table from HTML

1151
vendor/github.com/olekukonko/tablewriter/config.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,3 @@
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
package tablewriter
import (
@@ -13,40 +6,89 @@ import (
"os"
)
// Start A new table by importing from a CSV file
// NewCSV Start A new table by importing from a CSV file
// Takes io.Writer and csv File name
func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) {
func NewCSV(writer io.Writer, fileName string, hasHeader bool, opts ...Option) (*Table, error) {
// Open the CSV file
file, err := os.Open(fileName)
if err != nil {
return &Table{}, err
// Log implicitly handled by NewTable if logger is configured via opts
return nil, err // Return nil *Table on error
}
defer file.Close()
defer file.Close() // Ensure file is closed
// Create a CSV reader
csvReader := csv.NewReader(file)
t, err := NewCSVReader(writer, csvReader, hasHeader)
return t, err
// Delegate to NewCSVReader, passing through the options
return NewCSVReader(writer, csvReader, hasHeader, opts...)
}
// Start a New Table Writer with csv.Reader
// NewCSVReader Start a New Table Writer with csv.Reader
// This enables customisation such as reader.Comma = ';'
// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94
func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) {
t := NewWriter(writer)
func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool, opts ...Option) (*Table, error) {
// Create a new table instance using the modern API and provided options.
// Options configure the table's appearance and behavior (renderer, borders, etc.).
t := NewTable(writer, opts...) // Logger setup happens here if WithLogger/WithDebug is passed
// Process header row if specified
if hasHeader {
// Read the first row
headers, err := csvReader.Read()
if err != nil {
return &Table{}, err
// Handle EOF specifically: means the CSV was empty or contained only an empty header line.
if err == io.EOF {
t.logger.Debug("NewCSVReader: CSV empty or only header found (EOF after header read attempt).")
// Return the table configured by opts, but without data/header.
// It's ready for Render() which will likely output nothing or just borders if configured.
return t, nil
}
// Log other read errors
t.logger.Errorf("NewCSVReader: Error reading CSV header: %v", err)
return nil, err // Return nil *Table on critical read error
}
// Check if the read header is genuinely empty (e.g., a blank line in the CSV)
isEmptyHeader := true
for _, h := range headers {
if h != "" {
isEmptyHeader = false
break
}
}
if !isEmptyHeader {
t.Header(headers) // Use the Table method to set the header data
t.logger.Debugf("NewCSVReader: Header set from CSV: %v", headers)
} else {
t.logger.Debug("NewCSVReader: Read an empty header line, skipping setting table header.")
}
t.SetHeader(headers)
}
// Process data rows
rowCount := 0
for {
record, err := csvReader.Read()
if err == io.EOF {
break
} else if err != nil {
return &Table{}, err
break // Reached the end of the CSV data
}
t.Append(record)
if err != nil {
// Log other read errors during data processing
t.logger.Errorf("NewCSVReader: Error reading CSV record: %v", err)
return nil, err // Return nil *Table on critical read error
}
// Append the record to the table's internal buffer (for batch rendering).
// The Table.Append method handles conversion and storage.
if appendErr := t.Append(record); appendErr != nil {
t.logger.Errorf("NewCSVReader: Error appending record #%d: %v", rowCount+1, appendErr)
// Decide if append error is fatal. For now, let's treat it as fatal.
return nil, appendErr
}
rowCount++
}
t.logger.Debugf("NewCSVReader: Finished reading CSV. Appended %d data rows.", rowCount)
// Return the configured and populated table instance, ready for Render() call.
return t, nil
}

40
vendor/github.com/olekukonko/tablewriter/deprecated.go generated vendored Normal file
View File

@@ -0,0 +1,40 @@
package tablewriter
import "github.com/olekukonko/tablewriter/tw"
// Deprecated: WithBorders is no longer used.
// Border control has been moved to the renderer, which now manages its own borders.
// This Option has no effect on the Table and may be removed in future versions.
func WithBorders(borders tw.Border) Option {
return func(target *Table) {
if target.renderer != nil {
cfg := target.renderer.Config()
cfg.Borders = borders
if target.logger != nil {
target.logger.Debugf("Option: WithBorders applied to Table: %+v", borders)
}
}
}
}
// Deprecated: WithBorders is no longer supported.
// Use [tw.Behavior] directly to configure border settings.
type Behavior tw.Behavior
// Deprecated: WithRendererSettings i sno longer supported.
type Settings tw.Settings
// WithRendererSettings updates the renderer's settings (e.g., separators, lines).
// Render setting has move to renders directly
// you can also use WithRendition for renders that have rendition support
func WithRendererSettings(settings tw.Settings) Option {
return func(target *Table) {
if target.renderer != nil {
cfg := target.renderer.Config()
cfg.Settings = settings
if target.logger != nil {
target.logger.Debugf("Option: WithRendererSettings applied to Table: %+v", settings)
}
}
}
}

View File

@@ -0,0 +1,234 @@
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
package twwarp
import (
"github.com/rivo/uniseg"
"math"
"strings"
"unicode"
"github.com/mattn/go-runewidth"
)
const (
nl = "\n"
sp = " "
)
const defaultPenalty = 1e5
func SplitWords(s string) []string {
words := make([]string, 0, len(s)/5)
var wordBegin int
wordPending := false
for i, c := range s {
if unicode.IsSpace(c) {
if wordPending {
words = append(words, s[wordBegin:i])
wordPending = false
}
continue
}
if !wordPending {
wordBegin = i
wordPending = true
}
}
if wordPending {
words = append(words, s[wordBegin:])
}
return words
}
// WrapString wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapString(s string, lim int) ([]string, int) {
if s == sp {
return []string{sp}, lim
}
words := SplitWords(s)
if len(words) == 0 {
return []string{""}, lim
}
var lines []string
max := 0
for _, v := range words {
max = runewidth.StringWidth(v)
if max > lim {
lim = max
}
}
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, strings.Join(line, sp))
}
return lines, lim
}
// WrapStringWithSpaces wraps a string into lines of a specified display width while preserving
// leading and trailing spaces. It splits the input string into words, condenses internal multiple
// spaces to a single space, and wraps the content to fit within the given width limit, measured
// using Unicode-aware display width. The function is used in the logging library to format log
// messages for consistent output. It returns the wrapped lines as a slice of strings and the
// adjusted width limit, which may increase if a single word exceeds the input limit. Thread-safe
// as it does not modify shared state.
func WrapStringWithSpaces(s string, lim int) ([]string, int) {
if len(s) == 0 {
return []string{""}, lim
}
if strings.TrimSpace(s) == "" { // All spaces
if runewidth.StringWidth(s) <= lim {
return []string{s}, runewidth.StringWidth(s)
}
// For very long all-space strings, "wrap" by truncating to the limit.
if lim > 0 {
// Use our new helper function to get a substring of the correct display width
substring, _ := stringToDisplayWidth(s, lim)
return []string{substring}, lim
}
return []string{""}, lim
}
var leadingSpaces, trailingSpaces, coreContent string
firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
// firstNonSpace will not be -1 due to TrimSpace check above.
leadingSpaces = s[:firstNonSpace]
lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
trailingSpaces = s[lastNonSpace+1:]
coreContent = s[firstNonSpace : lastNonSpace+1]
if coreContent == "" {
return []string{leadingSpaces + trailingSpaces}, lim
}
words := SplitWords(coreContent)
if len(words) == 0 {
return []string{leadingSpaces + trailingSpaces}, lim
}
var lines []string
currentLim := lim
maxCoreWordWidth := 0
for _, v := range words {
w := runewidth.StringWidth(v)
if w > maxCoreWordWidth {
maxCoreWordWidth = w
}
}
if maxCoreWordWidth > currentLim {
currentLim = maxCoreWordWidth
}
wrappedWordLines := WrapWords(words, 1, currentLim, defaultPenalty)
for i, lineWords := range wrappedWordLines {
joinedLine := strings.Join(lineWords, sp)
finalLine := leadingSpaces + joinedLine
if i == len(wrappedWordLines)-1 { // Last line
finalLine += trailingSpaces
}
lines = append(lines, finalLine)
}
return lines, currentLim
}
// stringToDisplayWidth returns a substring of s that has a display width
// as close as possible to, but not exceeding, targetWidth.
// It returns the substring and its actual display width.
func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWidth int) {
if targetWidth <= 0 {
return "", 0
}
var currentWidth int
var endIndex int // Tracks the byte index in the original string
g := uniseg.NewGraphemes(s)
for g.Next() {
grapheme := g.Str()
graphemeWidth := runewidth.StringWidth(grapheme) // Get width of the current grapheme cluster
if currentWidth+graphemeWidth > targetWidth {
// Adding this grapheme would exceed the target width
break
}
currentWidth += graphemeWidth
// Get the end byte position of the current grapheme cluster
_, e := g.Positions()
endIndex = e
}
return s[:endIndex], currentWidth
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses,
// WrapString will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each rune as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words []string, spc, lim, pen int) [][]string {
n := len(words)
if n == 0 {
return nil
}
lengths := make([]int, n)
for i := 0; i < n; i++ {
lengths[i] = runewidth.StringWidth(words[i])
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
remainderLen := lengths[n-1]
for i := n - 1; i >= 0; i-- {
if i < n-1 {
remainderLen += spc + lengths[i]
}
if remainderLen <= lim {
cost[i] = 0
nbrk[i] = n
continue
}
phraseLen := lengths[i]
for j := i + 1; j < n; j++ {
if j > i+1 {
phraseLen += spc + lengths[j-1]
}
d := lim - phraseLen
c := d*d + cost[j]
if phraseLen > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
var lines [][]string
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}
// getLines decomposes a multiline string into a slice of strings.
func getLines(s string) []string {
return strings.Split(s, nl)
}

View File

@@ -0,0 +1,578 @@
package renderer
import (
"github.com/olekukonko/ll"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw"
)
// Blueprint implements a primary table rendering engine with customizable borders and alignments.
type Blueprint struct {
config tw.Rendition // Rendering configuration for table borders and symbols
logger *ll.Logger // Logger for debug trace messages
w io.Writer
}
// NewBlueprint creates a new Blueprint instance with optional custom configurations.
func NewBlueprint(configs ...tw.Rendition) *Blueprint {
// Initialize with default configuration
cfg := defaultBlueprint()
if len(configs) > 0 {
userCfg := configs[0]
// Override default borders if provided
if userCfg.Borders.Left != 0 {
cfg.Borders.Left = userCfg.Borders.Left
}
if userCfg.Borders.Right != 0 {
cfg.Borders.Right = userCfg.Borders.Right
}
if userCfg.Borders.Top != 0 {
cfg.Borders.Top = userCfg.Borders.Top
}
if userCfg.Borders.Bottom != 0 {
cfg.Borders.Bottom = userCfg.Borders.Bottom
}
// Override symbols if provided
if userCfg.Symbols != nil {
cfg.Symbols = userCfg.Symbols
}
// Merge user settings with default settings
cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings)
}
return &Blueprint{config: cfg}
}
// Close performs cleanup (no-op in this implementation).
func (f *Blueprint) Close() error {
f.logger.Debug("Blueprint.Close() called (no-op).")
return nil
}
// Config returns the renderer's current configuration.
func (f *Blueprint) Config() tw.Rendition {
return f.config
}
// Footer renders the table footer section with configured formatting.
func (f *Blueprint) Footer(footers [][]string, ctx tw.Formatting) {
f.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position)
// Render the footer line
f.renderLine(ctx)
f.logger.Debug("Completed Footer render")
}
// Header renders the table header section with configured formatting.
func (f *Blueprint) Header(headers [][]string, ctx tw.Formatting) {
f.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v",
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(ctx.Row.Current), ctx.Row.Widths)
// Render the header line
f.renderLine(ctx)
f.logger.Debug("Completed Header render")
}
// Line renders a full horizontal row line with junctions and segments.
func (f *Blueprint) Line(ctx tw.Formatting) {
// Initialize junction renderer
jr := NewJunction(JunctionContext{
Symbols: f.config.Symbols,
Ctx: ctx,
ColIdx: 0,
Logger: f.logger,
BorderTint: Tint{},
SeparatorTint: Tint{},
})
var line strings.Builder
totalLineWidth := 0 // Track total display width
// Get sorted column indices
sortedKeys := ctx.Row.Widths.SortedKeys()
numCols := 0
if len(sortedKeys) > 0 {
numCols = sortedKeys[len(sortedKeys)-1] + 1
}
// Handle empty row case
if numCols == 0 {
prefix := tw.Empty
suffix := tw.Empty
if f.config.Borders.Left.Enabled() {
prefix = jr.RenderLeft()
}
if f.config.Borders.Right.Enabled() {
suffix = jr.RenderRight(-1)
}
if prefix != tw.Empty || suffix != tw.Empty {
line.WriteString(prefix + suffix + tw.NewLine)
totalLineWidth = tw.DisplayWidth(prefix) + tw.DisplayWidth(suffix)
f.w.Write([]byte(line.String()))
}
f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth)
return
}
// Calculate target total width based on data rows
targetTotalWidth := 0
for _, colIdx := range sortedKeys {
targetTotalWidth += ctx.Row.Widths.Get(colIdx)
}
if f.config.Borders.Left.Enabled() {
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column())
}
if f.config.Borders.Right.Enabled() {
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column())
}
if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 {
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) * (len(sortedKeys) - 1)
}
// Add left border if enabled
leftBorderWidth := 0
if f.config.Borders.Left.Enabled() {
leftBorder := jr.RenderLeft()
line.WriteString(leftBorder)
leftBorderWidth = tw.DisplayWidth(leftBorder)
totalLineWidth += leftBorderWidth
f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth)
}
visibleColIndices := make([]int, 0)
// Calculate visible columns
for _, colIdx := range sortedKeys {
colWidth := ctx.Row.Widths.Get(colIdx)
if colWidth > 0 {
visibleColIndices = append(visibleColIndices, colIdx)
}
}
f.logger.Debugf("Line: sortedKeys=%v, Widths=%v, visibleColIndices=%v, targetTotalWidth=%d", sortedKeys, ctx.Row.Widths, visibleColIndices, targetTotalWidth)
// Render each column segment
for keyIndex, currentColIdx := range visibleColIndices {
jr.colIdx = currentColIdx
segment := jr.GetSegment()
colWidth := ctx.Row.Widths.Get(currentColIdx)
// Adjust colWidth to account for wider borders
adjustedColWidth := colWidth
if f.config.Borders.Left.Enabled() && keyIndex == 0 {
adjustedColWidth -= leftBorderWidth - tw.DisplayWidth(f.config.Symbols.Column())
}
if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 {
rightBorderWidth := tw.DisplayWidth(jr.RenderRight(currentColIdx))
adjustedColWidth -= rightBorderWidth - tw.DisplayWidth(f.config.Symbols.Column())
}
if adjustedColWidth < 0 {
adjustedColWidth = 0
}
f.logger.Debugf("Line: colIdx=%d, segment='%s', adjusted colWidth=%d", currentColIdx, segment, adjustedColWidth)
if segment == tw.Empty {
spaces := strings.Repeat(tw.Space, adjustedColWidth)
line.WriteString(spaces)
totalLineWidth += adjustedColWidth
f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx)
} else {
segmentWidth := tw.DisplayWidth(segment)
if segmentWidth == 0 {
segmentWidth = 1 // Avoid division by zero
f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment)
}
// Calculate how many full segments fit
repeat := adjustedColWidth / segmentWidth
if repeat < 1 && adjustedColWidth > 0 {
repeat = 1
}
repeatedSegment := strings.Repeat(segment, repeat)
actualWidth := tw.DisplayWidth(repeatedSegment)
if actualWidth > adjustedColWidth {
// Truncate if too long
repeatedSegment = tw.TruncateString(repeatedSegment, adjustedColWidth)
actualWidth = tw.DisplayWidth(repeatedSegment)
f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth)
} else if actualWidth < adjustedColWidth {
// Pad with segment character to match adjustedColWidth
remainingWidth := adjustedColWidth - actualWidth
for i := 0; i < remainingWidth/segmentWidth; i++ {
repeatedSegment += segment
}
actualWidth = tw.DisplayWidth(repeatedSegment)
if actualWidth < adjustedColWidth {
repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth)
actualWidth = adjustedColWidth
f.logger.Debugf("Line: Padded segment with spaces='%s' to width %d", repeatedSegment, actualWidth)
}
f.logger.Debugf("Line: Padded segment='%s' to width %d", repeatedSegment, actualWidth)
}
line.WriteString(repeatedSegment)
totalLineWidth += actualWidth
f.logger.Debugf("Line: Rendered segment='%s' (f.width %d) for col %d", repeatedSegment, actualWidth, currentColIdx)
}
// Add junction between columns if not the last column
isLast := keyIndex == len(visibleColIndices)-1
if !isLast && f.config.Settings.Separators.BetweenColumns.Enabled() {
nextColIdx := visibleColIndices[keyIndex+1]
junction := jr.RenderJunction(currentColIdx, nextColIdx)
// Use center symbol (❀) or column separator (|) to match data rows
if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) {
junction = f.config.Symbols.Center()
if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) {
junction = f.config.Symbols.Column()
}
}
junctionWidth := tw.DisplayWidth(junction)
line.WriteString(junction)
totalLineWidth += junctionWidth
f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth)
}
}
// Add right border
rightBorderWidth := 0
if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 {
lastIdx := visibleColIndices[len(visibleColIndices)-1]
rightBorder := jr.RenderRight(lastIdx)
rightBorderWidth = tw.DisplayWidth(rightBorder)
line.WriteString(rightBorder)
totalLineWidth += rightBorderWidth
f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth)
}
// Write the final line
line.WriteString(tw.NewLine)
f.w.Write([]byte(line.String()))
f.logger.Debugf("Line rendered: '%s' (total width %d, target %d)", strings.TrimSuffix(line.String(), tw.NewLine), totalLineWidth, targetTotalWidth)
}
// Logger sets the logger for the Blueprint instance.
func (f *Blueprint) Logger(logger *ll.Logger) {
f.logger = logger.Namespace("blueprint")
}
// Row renders a table data row with configured formatting.
func (f *Blueprint) Row(row []string, ctx tw.Formatting) {
f.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v",
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter)
// Render the row line
f.renderLine(ctx)
f.logger.Debug("Completed Row render")
}
// Start initializes the rendering process (no-op in this implementation).
func (f *Blueprint) Start(w io.Writer) error {
f.w = w
f.logger.Debug("Blueprint.Start() called (no-op).")
return nil
}
// formatCell formats a cell's content with specified width, padding, and alignment, returning an empty string if width is non-positive.
func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, align tw.Align) string {
if width <= 0 {
return tw.Empty
}
f.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, padding={L:'%s' R:'%s'}",
content, width, align, padding.Left, padding.Right)
// Calculate display width of content
runeWidth := tw.DisplayWidth(content)
// Set default padding characters
leftPadChar := padding.Left
rightPadChar := padding.Right
//if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() {
// if leftPadChar == tw.Empty {
// leftPadChar = tw.Space
// }
// if rightPadChar == tw.Empty {
// rightPadChar = tw.Space
// }
//}
// Calculate padding widths
padLeftWidth := tw.DisplayWidth(leftPadChar)
padRightWidth := tw.DisplayWidth(rightPadChar)
// Calculate available width for content
availableContentWidth := width - padLeftWidth - padRightWidth
if availableContentWidth < 0 {
availableContentWidth = 0
}
f.logger.Debugf("Available content width: %d", availableContentWidth)
// Truncate content if it exceeds available width
if runeWidth > availableContentWidth {
content = tw.TruncateString(content, availableContentWidth)
runeWidth = tw.DisplayWidth(content)
f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth)
}
// Calculate total padding needed
totalPaddingWidth := width - runeWidth
if totalPaddingWidth < 0 {
totalPaddingWidth = 0
}
f.logger.Debugf("Total padding width: %d", totalPaddingWidth)
var result strings.Builder
var leftPaddingWidth, rightPaddingWidth int
// Apply alignment and padding
switch align {
case tw.AlignLeft:
result.WriteString(leftPadChar)
result.WriteString(content)
rightPaddingWidth = totalPaddingWidth - padLeftWidth
if rightPaddingWidth > 0 {
result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth))
f.logger.Debugf("Applied right padding: '%s' for %d width", rightPadChar, rightPaddingWidth)
}
case tw.AlignRight:
leftPaddingWidth = totalPaddingWidth - padRightWidth
if leftPaddingWidth > 0 {
result.WriteString(tw.PadLeft(tw.Empty, leftPadChar, leftPaddingWidth))
f.logger.Debugf("Applied left padding: '%s' for %d width", leftPadChar, leftPaddingWidth)
}
result.WriteString(content)
result.WriteString(rightPadChar)
case tw.AlignCenter:
leftPaddingWidth = (totalPaddingWidth-padLeftWidth-padRightWidth)/2 + padLeftWidth
rightPaddingWidth = totalPaddingWidth - leftPaddingWidth
if leftPaddingWidth > padLeftWidth {
result.WriteString(tw.PadLeft(tw.Empty, leftPadChar, leftPaddingWidth-padLeftWidth))
f.logger.Debugf("Applied left centering padding: '%s' for %d width", leftPadChar, leftPaddingWidth-padLeftWidth)
}
result.WriteString(leftPadChar)
result.WriteString(content)
result.WriteString(rightPadChar)
if rightPaddingWidth > padRightWidth {
result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth-padRightWidth))
f.logger.Debugf("Applied right centering padding: '%s' for %d width", rightPadChar, rightPaddingWidth-padRightWidth)
}
default:
// Default to left alignment
result.WriteString(leftPadChar)
result.WriteString(content)
rightPaddingWidth = totalPaddingWidth - padLeftWidth
if rightPaddingWidth > 0 {
result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth))
f.logger.Debugf("Applied right padding: '%s' for %d width", rightPadChar, rightPaddingWidth)
}
}
output := result.String()
finalWidth := tw.DisplayWidth(output)
// Adjust output to match target width
if finalWidth > width {
output = tw.TruncateString(output, width)
f.logger.Debugf("formatCell: Truncated output to width %d", width)
} else if finalWidth < width {
output = tw.PadRight(output, tw.Space, width)
f.logger.Debugf("formatCell: Padded output to meet width %d", width)
}
// Log warning if final width doesn't match target
if f.logger.Enabled() && tw.DisplayWidth(output) != width {
f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'",
tw.DisplayWidth(output), width, output)
}
f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width)
return output
}
// renderLine renders a single line (header, row, or footer) with borders, separators, and merge handling.
func (f *Blueprint) renderLine(ctx tw.Formatting) {
// Get sorted column indices
sortedKeys := ctx.Row.Widths.SortedKeys()
numCols := 0
if len(sortedKeys) > 0 {
numCols = sortedKeys[len(sortedKeys)-1] + 1
}
// Set column separator and borders
columnSeparator := f.config.Symbols.Column()
prefix := tw.Empty
if f.config.Borders.Left.Enabled() {
prefix = columnSeparator
}
suffix := tw.Empty
if f.config.Borders.Right.Enabled() {
suffix = columnSeparator
}
var output strings.Builder
totalLineWidth := 0 // Track total display width
if prefix != tw.Empty {
output.WriteString(prefix)
totalLineWidth += tw.DisplayWidth(prefix)
f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, tw.DisplayWidth(prefix))
}
colIndex := 0
separatorDisplayWidth := 0
if f.config.Settings.Separators.BetweenColumns.Enabled() {
separatorDisplayWidth = tw.DisplayWidth(columnSeparator)
}
// Process each column
for colIndex < numCols {
visualWidth := ctx.Row.Widths.Get(colIndex)
cellCtx, ok := ctx.Row.Current[colIndex]
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
if visualWidth == 0 && !isHMergeStart {
f.logger.Debugf("renderLine: Skipping col %d (zero width, not HMerge start)", colIndex)
colIndex++
continue
}
// Determine if a separator is needed
shouldAddSeparator := false
if colIndex > 0 && f.config.Settings.Separators.BetweenColumns.Enabled() {
prevWidth := ctx.Row.Widths.Get(colIndex - 1)
prevCellCtx, prevOk := ctx.Row.Current[colIndex-1]
prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End
if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start)) {
shouldAddSeparator = true
}
}
if shouldAddSeparator {
output.WriteString(columnSeparator)
totalLineWidth += separatorDisplayWidth
f.logger.Debugf("renderLine: Added separator '%s' before col %d (f.width %d)", columnSeparator, colIndex, separatorDisplayWidth)
} else if colIndex > 0 {
f.logger.Debugf("renderLine: Skipped separator before col %d due to zero-width prev col or HMerge continuation", colIndex)
}
// Handle merged cells
span := 1
if isHMergeStart {
span = cellCtx.Merge.Horizontal.Span
if ctx.Row.Position == tw.Row {
dynamicTotalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ {
normWidth := ctx.NormalizedWidths.Get(colIndex + k)
if normWidth < 0 {
normWidth = 0
}
dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 {
dynamicTotalWidth += separatorDisplayWidth
}
}
visualWidth = dynamicTotalWidth
f.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", colIndex, span, visualWidth)
} else {
visualWidth = ctx.Row.Widths.Get(colIndex)
f.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", colIndex, span, visualWidth)
}
} else {
visualWidth = ctx.Row.Widths.Get(colIndex)
f.logger.Debugf("renderLine: Regular col %d, visualWidth %d", colIndex, visualWidth)
}
if visualWidth < 0 {
visualWidth = 0
}
// Skip processing for non-start merged cells
if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
f.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", colIndex)
colIndex++
continue
}
// Handle empty cell context
if !ok {
if visualWidth > 0 {
spaces := strings.Repeat(tw.Space, visualWidth)
output.WriteString(spaces)
totalLineWidth += visualWidth
f.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces (f.width %d)", colIndex, visualWidth, visualWidth)
} else {
f.logger.Debugf("renderLine: No cell context for col %d, visualWidth is 0, writing nothing", colIndex)
}
colIndex += span
continue
}
// Set cell padding and alignment
padding := cellCtx.Padding
align := cellCtx.Align
if align == tw.AlignNone {
if ctx.Row.Position == tw.Header {
align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
align = tw.AlignRight
} else {
align = tw.AlignLeft
}
f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position)
} else if align == tw.Skip {
if ctx.Row.Position == tw.Header {
align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
align = tw.AlignRight
} else {
align = tw.AlignLeft
}
f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align)
}
isTotalPattern := false
// Override alignment for footer merged cells
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
if align != tw.AlignRight {
f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align)
align = tw.AlignRight
}
}
// Handle vertical/hierarchical merges
cellData := cellCtx.Data
if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) ||
(cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
cellData = tw.Empty
f.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", colIndex)
}
// Format and render the cell
formattedCell := f.formatCell(cellData, visualWidth, padding, align)
if len(formattedCell) > 0 {
output.WriteString(formattedCell)
cellWidth := tw.DisplayWidth(formattedCell)
totalLineWidth += cellWidth
f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth)
}
// Log rendering details
if isHMergeStart {
f.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %v): '%s'",
colIndex, span, visualWidth, align, formattedCell)
} else {
f.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %v): '%s'",
colIndex, visualWidth, align, formattedCell)
}
colIndex += span
}
// Add suffix and adjust total width
if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() {
output.WriteString(suffix)
totalLineWidth += tw.DisplayWidth(suffix)
f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, tw.DisplayWidth(suffix))
}
output.WriteString(tw.NewLine)
f.w.Write([]byte(output.String()))
f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth)
}
func (f *Blueprint) Rendition(config tw.Rendition) {
f.config = mergeRendition(f.config, config)
f.logger.Debugf("Blueprint.Rendition updated. New internal config: %+v", f.config)
}
// Ensure Blueprint implements tw.Renditioning
var _ tw.Renditioning = (*Blueprint)(nil)

View File

@@ -0,0 +1,719 @@
package renderer
import (
"github.com/fatih/color"
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw"
)
// ColorizedConfig holds configuration for the Colorized table renderer.
type ColorizedConfig struct {
Borders tw.Border // Border visibility settings
Settings tw.Settings // Rendering behavior settings (e.g., separators, whitespace)
Header Tint // Colors for header cells
Column Tint // Colors for row cells
Footer Tint // Colors for footer cells
Border Tint // Colors for borders and lines
Separator Tint // Colors for column separators
Symbols tw.Symbols // Symbols for table drawing (e.g., corners, lines)
}
// Colors is a slice of color attributes for use with fatih/color, such as color.FgWhite or color.Bold.
type Colors []color.Attribute
// Tint defines foreground and background color settings for table elements, with optional per-column overrides.
type Tint struct {
FG Colors // Foreground color attributes
BG Colors // Background color attributes
Columns []Tint // Per-column color settings
}
// Apply applies the Tint's foreground and background colors to the given text, returning the text unchanged if no colors are set.
func (t Tint) Apply(text string) string {
if len(t.FG) == 0 && len(t.BG) == 0 {
return text
}
// Combine foreground and background colors
combinedColors := append(t.FG, t.BG...)
// Create a color function and apply it to the text
c := color.New(combinedColors...).SprintFunc()
return c(text)
}
// Colorized renders colored ASCII tables with customizable borders, colors, and alignments.
type Colorized struct {
config ColorizedConfig // Renderer configuration
trace []string // Debug trace messages
newLine string // Newline character
defaultAlign map[tw.Position]tw.Align // Default alignments for header, row, and footer
logger *ll.Logger // Logger for debug messages
w io.Writer
}
// NewColorized creates a Colorized renderer with the specified configuration, falling back to defaults if none provided.
// Only the first config is used if multiple are passed.
func NewColorized(configs ...ColorizedConfig) *Colorized {
// Initialize with default configuration
baseCfg := defaultColorized()
if len(configs) > 0 {
userCfg := configs[0]
// Override border settings if provided
if userCfg.Borders.Left != 0 {
baseCfg.Borders.Left = userCfg.Borders.Left
}
if userCfg.Borders.Right != 0 {
baseCfg.Borders.Right = userCfg.Borders.Right
}
if userCfg.Borders.Top != 0 {
baseCfg.Borders.Top = userCfg.Borders.Top
}
if userCfg.Borders.Bottom != 0 {
baseCfg.Borders.Bottom = userCfg.Borders.Bottom
}
// Merge separator and line settings
baseCfg.Settings.Separators = mergeSeparators(baseCfg.Settings.Separators, userCfg.Settings.Separators)
baseCfg.Settings.Lines = mergeLines(baseCfg.Settings.Lines, userCfg.Settings.Lines)
// Override compact mode if specified
if userCfg.Settings.CompactMode != 0 {
baseCfg.Settings.CompactMode = userCfg.Settings.CompactMode
}
// Override color settings for various table elements
if len(userCfg.Header.FG) > 0 || len(userCfg.Header.BG) > 0 || userCfg.Header.Columns != nil {
baseCfg.Header = userCfg.Header
}
if len(userCfg.Column.FG) > 0 || len(userCfg.Column.BG) > 0 || userCfg.Column.Columns != nil {
baseCfg.Column = userCfg.Column
}
if len(userCfg.Footer.FG) > 0 || len(userCfg.Footer.BG) > 0 || userCfg.Footer.Columns != nil {
baseCfg.Footer = userCfg.Footer
}
if len(userCfg.Border.FG) > 0 || len(userCfg.Border.BG) > 0 || userCfg.Border.Columns != nil {
baseCfg.Border = userCfg.Border
}
if len(userCfg.Separator.FG) > 0 || len(userCfg.Separator.BG) > 0 || userCfg.Separator.Columns != nil {
baseCfg.Separator = userCfg.Separator
}
// Override symbols if provided
if userCfg.Symbols != nil {
baseCfg.Symbols = userCfg.Symbols
}
}
cfg := baseCfg
// Ensure symbols are initialized
if cfg.Symbols == nil {
cfg.Symbols = tw.NewSymbols(tw.StyleLight)
}
// Initialize the Colorized renderer
f := &Colorized{
config: cfg,
newLine: tw.NewLine,
defaultAlign: map[tw.Position]tw.Align{
tw.Header: tw.AlignCenter,
tw.Row: tw.AlignLeft,
tw.Footer: tw.AlignRight,
},
logger: ll.New("colorized", ll.WithHandler(lh.NewMemoryHandler())),
}
// Log initialization details
f.logger.Debugf("Initialized Colorized renderer with symbols: Center=%q, Row=%q, Column=%q", f.config.Symbols.Center(), f.config.Symbols.Row(), f.config.Symbols.Column())
f.logger.Debugf("Final ColorizedConfig.Settings.Lines: %+v", f.config.Settings.Lines)
f.logger.Debugf("Final ColorizedConfig.Borders: %+v", f.config.Borders)
return f
}
// Close performs cleanup (no-op in this implementation).
func (c *Colorized) Close() error {
c.logger.Debug("Colorized.Close() called (no-op).")
return nil
}
// Config returns the renderer's configuration as a Rendition.
func (c *Colorized) Config() tw.Rendition {
return tw.Rendition{
Borders: c.config.Borders,
Settings: c.config.Settings,
Symbols: c.config.Symbols,
Streaming: true,
}
}
// Debug returns the accumulated debug trace messages.
func (c *Colorized) Debug() []string {
return c.trace
}
// Footer renders the table footer with configured colors and formatting.
func (c *Colorized) Footer(footers [][]string, ctx tw.Formatting) {
c.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s",
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position)
// Check if there are footers to render
if len(footers) == 0 || len(footers[0]) == 0 {
c.logger.Debug("Footer: No footers to render")
return
}
// Render the footer line
c.renderLine(ctx, footers[0], c.config.Footer)
c.logger.Debug("Completed Footer render")
}
// Header renders the table header with configured colors and formatting.
func (c *Colorized) Header(headers [][]string, ctx tw.Formatting) {
c.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v",
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(headers), ctx.Row.Widths)
// Check if there are headers to render
if len(headers) == 0 || len(headers[0]) == 0 {
c.logger.Debug("Header: No headers to render")
return
}
// Render the header line
c.renderLine(ctx, headers[0], c.config.Header)
c.logger.Debug("Completed Header render")
}
// Line renders a horizontal row line with colored junctions and segments, skipping zero-width columns.
func (c *Colorized) Line(ctx tw.Formatting) {
c.logger.Debugf("Line: Starting with Level=%v, Location=%v, IsSubRow=%v, Widths=%v", ctx.Level, ctx.Row.Location, ctx.IsSubRow, ctx.Row.Widths)
// Initialize junction renderer
jr := NewJunction(JunctionContext{
Symbols: c.config.Symbols,
Ctx: ctx,
ColIdx: 0,
BorderTint: c.config.Border,
SeparatorTint: c.config.Separator,
Logger: c.logger,
})
var line strings.Builder
// Get sorted column indices and filter out zero-width columns
allSortedKeys := ctx.Row.Widths.SortedKeys()
effectiveKeys := []int{}
keyWidthMap := make(map[int]int)
for _, k := range allSortedKeys {
width := ctx.Row.Widths.Get(k)
keyWidthMap[k] = width
if width > 0 {
effectiveKeys = append(effectiveKeys, k)
}
}
c.logger.Debugf("Line: All keys=%v, Effective keys (width>0)=%v", allSortedKeys, effectiveKeys)
// Handle case with no effective columns
if len(effectiveKeys) == 0 {
prefix := tw.Empty
suffix := tw.Empty
if c.config.Borders.Left.Enabled() {
prefix = jr.RenderLeft()
}
if c.config.Borders.Right.Enabled() {
originalLastColIdx := -1
if len(allSortedKeys) > 0 {
originalLastColIdx = allSortedKeys[len(allSortedKeys)-1]
}
suffix = jr.RenderRight(originalLastColIdx)
}
if prefix != tw.Empty || suffix != tw.Empty {
line.WriteString(prefix + suffix + tw.NewLine)
c.w.Write([]byte(line.String()))
}
c.logger.Debug("Line: Handled empty row/widths case (no effective keys)")
return
}
// Add left border if enabled
if c.config.Borders.Left.Enabled() {
line.WriteString(jr.RenderLeft())
}
// Render segments for each effective column
for keyIndex, currentColIdx := range effectiveKeys {
jr.colIdx = currentColIdx
segment := jr.GetSegment()
colWidth := keyWidthMap[currentColIdx]
c.logger.Debugf("Line: Drawing segment for Effective colIdx=%d, segment='%s', width=%d", currentColIdx, segment, colWidth)
if segment == tw.Empty {
line.WriteString(strings.Repeat(tw.Space, colWidth))
} else {
// Calculate how many times to repeat the segment
segmentWidth := tw.DisplayWidth(segment)
if segmentWidth <= 0 {
segmentWidth = 1
}
repeat := 0
if colWidth > 0 && segmentWidth > 0 {
repeat = colWidth / segmentWidth
}
drawnSegment := strings.Repeat(segment, repeat)
line.WriteString(drawnSegment)
// Adjust for width discrepancies
actualDrawnWidth := tw.DisplayWidth(drawnSegment)
if actualDrawnWidth < colWidth {
missingWidth := colWidth - actualDrawnWidth
spaces := strings.Repeat(tw.Space, missingWidth)
if len(c.config.Border.BG) > 0 {
line.WriteString(Tint{BG: c.config.Border.BG}.Apply(spaces))
} else {
line.WriteString(spaces)
}
c.logger.Debugf("Line: colIdx=%d corrected segment width, added %d spaces", currentColIdx, missingWidth)
} else if actualDrawnWidth > colWidth {
c.logger.Debugf("Line: WARNING colIdx=%d segment draw width %d > target %d", currentColIdx, actualDrawnWidth, colWidth)
}
}
// Add junction between columns if not the last visible column
isLastVisible := keyIndex == len(effectiveKeys)-1
if !isLastVisible && c.config.Settings.Separators.BetweenColumns.Enabled() {
nextVisibleColIdx := effectiveKeys[keyIndex+1]
originalPrecedingCol := -1
foundCurrent := false
for _, k := range allSortedKeys {
if k == currentColIdx {
foundCurrent = true
}
if foundCurrent && k < nextVisibleColIdx {
originalPrecedingCol = k
}
if k >= nextVisibleColIdx {
break
}
}
if originalPrecedingCol != -1 {
jr.colIdx = originalPrecedingCol
junction := jr.RenderJunction(originalPrecedingCol, nextVisibleColIdx)
c.logger.Debugf("Line: Junction between visible %d (orig preceding %d) and next visible %d: '%s'", currentColIdx, originalPrecedingCol, nextVisibleColIdx, junction)
line.WriteString(junction)
} else {
c.logger.Debugf("Line: Could not determine original preceding column for junction before visible %d", nextVisibleColIdx)
line.WriteString(c.config.Separator.Apply(jr.sym.Center()))
}
}
}
// Add right border if enabled
if c.config.Borders.Right.Enabled() {
originalLastColIdx := -1
if len(allSortedKeys) > 0 {
originalLastColIdx = allSortedKeys[len(allSortedKeys)-1]
}
jr.colIdx = originalLastColIdx
line.WriteString(jr.RenderRight(originalLastColIdx))
}
// Write the final line
line.WriteString(c.newLine)
c.w.Write([]byte(line.String()))
c.logger.Debugf("Line rendered: %s", strings.TrimSuffix(line.String(), c.newLine))
}
// Logger sets the logger for the Colorized instance.
func (c *Colorized) Logger(logger *ll.Logger) {
c.logger = logger.Namespace("colorized")
}
// Reset clears the renderer's internal state, including debug traces.
func (c *Colorized) Reset() {
c.trace = nil
c.logger.Debugf("Reset: Cleared debug trace")
}
// Row renders a table data row with configured colors and formatting.
func (c *Colorized) Row(row []string, ctx tw.Formatting) {
c.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v",
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter)
// Check if there is data to render
if len(row) == 0 {
c.logger.Debugf("Row: No data to render")
return
}
// Render the row line
c.renderLine(ctx, row, c.config.Column)
c.logger.Debugf("Completed Row render")
}
// Start initializes the rendering process (no-op in this implementation).
func (c *Colorized) Start(w io.Writer) error {
c.w = w
c.logger.Debugf("Colorized.Start() called (no-op).")
return nil
}
// formatCell formats a cell's content with color, width, padding, and alignment, handling whitespace trimming and truncation.
func (c *Colorized) formatCell(content string, width int, padding tw.Padding, align tw.Align, tint Tint) string {
c.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s', tintFG=%v, tintBG=%v",
content, width, align, padding.Left, padding.Right, tint.FG, tint.BG)
// Return empty string if width is non-positive
if width <= 0 {
c.logger.Debugf("formatCell: width %d <= 0, returning empty string", width)
return tw.Empty
}
// Calculate visual width of content
contentVisualWidth := tw.DisplayWidth(content)
// Set default padding characters
padLeftCharStr := padding.Left
if padLeftCharStr == tw.Empty {
padLeftCharStr = tw.Space
}
padRightCharStr := padding.Right
if padRightCharStr == tw.Empty {
padRightCharStr = tw.Space
}
// Calculate padding widths
definedPadLeftWidth := tw.DisplayWidth(padLeftCharStr)
definedPadRightWidth := tw.DisplayWidth(padRightCharStr)
// Calculate available width for content and alignment
availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth
if availableForContentAndAlign < 0 {
availableForContentAndAlign = 0
}
// Truncate content if it exceeds available width
if contentVisualWidth > availableForContentAndAlign {
content = tw.TruncateString(content, availableForContentAndAlign)
contentVisualWidth = tw.DisplayWidth(content)
c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth)
}
// Calculate remaining space for alignment
remainingSpaceForAlignment := availableForContentAndAlign - contentVisualWidth
if remainingSpaceForAlignment < 0 {
remainingSpaceForAlignment = 0
}
// Apply alignment padding
leftAlignmentPadSpaces := tw.Empty
rightAlignmentPadSpaces := tw.Empty
switch align {
case tw.AlignLeft:
rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
case tw.AlignRight:
leftAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
case tw.AlignCenter:
leftSpacesCount := remainingSpaceForAlignment / 2
rightSpacesCount := remainingSpaceForAlignment - leftSpacesCount
leftAlignmentPadSpaces = strings.Repeat(tw.Space, leftSpacesCount)
rightAlignmentPadSpaces = strings.Repeat(tw.Space, rightSpacesCount)
default:
// Default to left alignment
rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
}
// Apply colors to content and padding
coloredContent := tint.Apply(content)
coloredPadLeft := padLeftCharStr
coloredPadRight := padRightCharStr
coloredAlignPadLeft := leftAlignmentPadSpaces
coloredAlignPadRight := rightAlignmentPadSpaces
if len(tint.BG) > 0 {
bgTint := Tint{BG: tint.BG}
// Apply foreground color to non-space padding if foreground is defined
if len(tint.FG) > 0 && padLeftCharStr != tw.Space {
coloredPadLeft = tint.Apply(padLeftCharStr)
} else {
coloredPadLeft = bgTint.Apply(padLeftCharStr)
}
if len(tint.FG) > 0 && padRightCharStr != tw.Space {
coloredPadRight = tint.Apply(padRightCharStr)
} else {
coloredPadRight = bgTint.Apply(padRightCharStr)
}
// Apply background color to alignment padding
if leftAlignmentPadSpaces != tw.Empty {
coloredAlignPadLeft = bgTint.Apply(leftAlignmentPadSpaces)
}
if rightAlignmentPadSpaces != tw.Empty {
coloredAlignPadRight = bgTint.Apply(rightAlignmentPadSpaces)
}
} else if len(tint.FG) > 0 {
// Apply foreground color to non-space padding
if padLeftCharStr != tw.Space {
coloredPadLeft = tint.Apply(padLeftCharStr)
}
if padRightCharStr != tw.Space {
coloredPadRight = tint.Apply(padRightCharStr)
}
}
// Build final cell string
var sb strings.Builder
sb.WriteString(coloredPadLeft)
sb.WriteString(coloredAlignPadLeft)
sb.WriteString(coloredContent)
sb.WriteString(coloredAlignPadRight)
sb.WriteString(coloredPadRight)
output := sb.String()
// Adjust output width if necessary
currentVisualWidth := tw.DisplayWidth(output)
if currentVisualWidth != width {
c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'",
content, width, currentVisualWidth, output)
if currentVisualWidth > width {
output = tw.TruncateString(output, width)
} else {
paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth)
if len(tint.BG) > 0 {
output += Tint{BG: tint.BG}.Apply(paddingSpacesStr)
} else {
output += paddingSpacesStr
}
}
c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, tw.DisplayWidth(output), output)
}
c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, tw.DisplayWidth(output))
return output
}
// renderLine renders a single line (header, row, or footer) with colors, handling merges and separators.
func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
// Determine number of columns
numCols := 0
if len(ctx.Row.Current) > 0 {
maxKey := -1
for k := range ctx.Row.Current {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
} else {
maxKey := -1
for k := range ctx.Row.Widths {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
}
var output strings.Builder
// Add left border if enabled
prefix := tw.Empty
if c.config.Borders.Left.Enabled() {
prefix = c.config.Border.Apply(c.config.Symbols.Column())
}
output.WriteString(prefix)
// Set up separator
separatorDisplayWidth := 0
separatorString := tw.Empty
if c.config.Settings.Separators.BetweenColumns.Enabled() {
separatorString = c.config.Separator.Apply(c.config.Symbols.Column())
separatorDisplayWidth = tw.DisplayWidth(c.config.Symbols.Column())
}
// Process each column
for i := 0; i < numCols; {
// Determine if a separator is needed
shouldAddSeparator := false
if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() {
cellCtx, ok := ctx.Row.Current[i]
if !ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start) {
shouldAddSeparator = true
}
}
if shouldAddSeparator {
output.WriteString(separatorString)
c.logger.Debugf("renderLine: Added separator '%s' before col %d", separatorString, i)
} else if i > 0 {
c.logger.Debugf("renderLine: Skipped separator before col %d due to HMerge continuation", i)
}
// Get cell context, use default if not present
cellCtx, ok := ctx.Row.Current[i]
if !ok {
cellCtx = tw.CellContext{
Data: tw.Empty,
Align: c.defaultAlign[ctx.Row.Position],
Padding: tw.Padding{Left: tw.Space, Right: tw.Space},
Width: ctx.Row.Widths.Get(i),
Merge: tw.MergeState{},
}
}
// Handle merged cells
visualWidth := 0
span := 1
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
if isHMergeStart {
span = cellCtx.Merge.Horizontal.Span
if ctx.Row.Position == tw.Row {
// Calculate dynamic width for row merges
dynamicTotalWidth := 0
for k := 0; k < span && i+k < numCols; k++ {
colToSum := i + k
normWidth := ctx.NormalizedWidths.Get(colToSum)
if normWidth < 0 {
normWidth = 0
}
dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 {
dynamicTotalWidth += separatorDisplayWidth
}
}
visualWidth = dynamicTotalWidth
c.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", i, span, visualWidth)
} else {
visualWidth = ctx.Row.Widths.Get(i)
c.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", i, span, visualWidth)
}
} else {
visualWidth = ctx.Row.Widths.Get(i)
c.logger.Debugf("renderLine: Regular col %d, visualWidth %d", i, visualWidth)
}
if visualWidth < 0 {
visualWidth = 0
}
// Skip processing for non-start merged cells
if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
c.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", i)
i++
continue
}
// Handle empty cell context with non-zero width
if !ok && visualWidth > 0 {
spaces := strings.Repeat(tw.Space, visualWidth)
if len(tint.BG) > 0 {
output.WriteString(Tint{BG: tint.BG}.Apply(spaces))
} else {
output.WriteString(spaces)
}
c.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces", i, visualWidth)
i += span
continue
}
// Set cell alignment
padding := cellCtx.Padding
align := cellCtx.Align
if align == tw.AlignNone {
align = c.defaultAlign[ctx.Row.Position]
c.logger.Debugf("renderLine: col %d using default renderer align '%s' for position %s because cellCtx.Align was AlignNone", i, align, ctx.Row.Position)
}
// Detect and handle TOTAL pattern
isTotalPattern := false
if i == 0 && isHMergeStart && cellCtx.Merge.Horizontal.Span >= 3 && strings.TrimSpace(cellCtx.Data) == "TOTAL" {
isTotalPattern = true
c.logger.Debugf("renderLine: Detected 'TOTAL' HMerge pattern at col 0")
}
// Override alignment for footer merges or TOTAL pattern
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
if align != tw.AlignRight {
c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align)
align = tw.AlignRight
}
}
// Handle vertical/hierarchical merges
content := cellCtx.Data
if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) ||
(cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
content = tw.Empty
c.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", i)
}
// Apply per-column tint if available
cellTint := tint
if i < len(tint.Columns) {
columnTint := tint.Columns[i]
if len(columnTint.FG) > 0 || len(columnTint.BG) > 0 {
cellTint = columnTint
}
}
// Format and render the cell
formattedCell := c.formatCell(content, visualWidth, padding, align, cellTint)
if len(formattedCell) > 0 {
output.WriteString(formattedCell)
} else if visualWidth == 0 && isHMergeStart {
c.logger.Debugf("renderLine: Rendered HMerge START col %d resulted in 0 visual width, wrote nothing.", i)
} else if visualWidth == 0 {
c.logger.Debugf("renderLine: Rendered regular col %d resulted in 0 visual width, wrote nothing.", i)
}
// Log rendering details
if isHMergeStart {
c.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %s): '%s'",
i, span, visualWidth, align, formattedCell)
} else {
c.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %s): '%s'",
i, visualWidth, align, formattedCell)
}
i += span
}
// Add right border if enabled
suffix := tw.Empty
if c.config.Borders.Right.Enabled() {
suffix = c.config.Border.Apply(c.config.Symbols.Column())
}
output.WriteString(suffix)
// Write the final line
output.WriteString(c.newLine)
c.w.Write([]byte(output.String()))
c.logger.Debugf("renderLine: Final rendered line: %s", strings.TrimSuffix(output.String(), c.newLine))
}
// Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition
// by merging the provided newRendition. Color-specific Tints are not modified.
func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface
c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v",
c.config.Borders, c.config.Symbols, c.config.Settings, newRendition)
currentRenditionPart := tw.Rendition{
Borders: c.config.Borders,
Symbols: c.config.Symbols,
Settings: c.config.Settings,
}
mergedRenditionPart := mergeRendition(currentRenditionPart, newRendition)
c.config.Borders = mergedRenditionPart.Borders
c.config.Symbols = mergedRenditionPart.Symbols
if c.config.Symbols == nil {
c.config.Symbols = tw.NewSymbols(tw.StyleLight)
}
c.config.Settings = mergedRenditionPart.Settings
c.logger.Debugf("Colorized.Rendition updated. New B/Sym/Set: B:%+v, Sym:%T, S:%+v",
c.config.Borders, c.config.Symbols, c.config.Settings)
}
// Ensure Colorized implements tw.Renditioning
var _ tw.Renditioning = (*Colorized)(nil)

236
vendor/github.com/olekukonko/tablewriter/renderer/fn.go generated vendored Normal file
View File

@@ -0,0 +1,236 @@
package renderer
import (
"fmt"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter/tw"
)
// defaultBlueprint returns a default Rendition for ASCII table rendering with borders and light symbols.
func defaultBlueprint() tw.Rendition {
return tw.Rendition{
Borders: tw.Border{
Left: tw.On,
Right: tw.On,
Top: tw.On,
Bottom: tw.On,
},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.On,
ShowFooter: tw.On,
BetweenRows: tw.Off,
BetweenColumns: tw.On,
},
Lines: tw.Lines{
ShowTop: tw.On,
ShowBottom: tw.On,
ShowHeaderLine: tw.On,
ShowFooterLine: tw.On,
},
CompactMode: tw.Off,
// Cushion: tw.On,
},
Symbols: tw.NewSymbols(tw.StyleLight),
Streaming: true,
}
}
// defaultColorized returns a default ColorizedConfig optimized for dark terminal backgrounds with colored headers, rows, and borders.
func defaultColorized() ColorizedConfig {
return ColorizedConfig{
Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.On,
ShowFooter: tw.On,
BetweenRows: tw.Off,
BetweenColumns: tw.On,
},
Lines: tw.Lines{
ShowTop: tw.On,
ShowBottom: tw.On,
ShowHeaderLine: tw.On,
ShowFooterLine: tw.On,
},
CompactMode: tw.Off,
},
Header: Tint{
FG: Colors{color.FgWhite, color.Bold},
BG: Colors{color.BgBlack},
},
Column: Tint{
FG: Colors{color.FgCyan},
BG: Colors{color.BgBlack},
},
Footer: Tint{
FG: Colors{color.FgYellow},
BG: Colors{color.BgBlack},
},
Border: Tint{
FG: Colors{color.FgWhite},
BG: Colors{color.BgBlack},
},
Separator: Tint{
FG: Colors{color.FgWhite},
BG: Colors{color.BgBlack},
},
Symbols: tw.NewSymbols(tw.StyleLight),
}
}
// defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer.
func defaultOceanRendererConfig() tw.Rendition {
return tw.Rendition{
Borders: tw.Border{
Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On,
},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.On,
ShowFooter: tw.Off,
BetweenRows: tw.Off,
BetweenColumns: tw.On,
},
Lines: tw.Lines{
ShowTop: tw.On,
ShowBottom: tw.On,
ShowHeaderLine: tw.On,
ShowFooterLine: tw.Off,
},
CompactMode: tw.Off,
},
Symbols: tw.NewSymbols(tw.StyleDefault),
Streaming: true,
}
}
// getHTMLStyle remains the same
func getHTMLStyle(align tw.Align) string {
styleContent := tw.Empty
switch align {
case tw.AlignRight:
styleContent = "text-align: right;"
case tw.AlignCenter:
styleContent = "text-align: center;"
case tw.AlignLeft:
styleContent = "text-align: left;"
}
if styleContent != tw.Empty {
return fmt.Sprintf(` style="%s"`, styleContent)
}
return tw.Empty
}
// mergeLines combines default and override line settings, preserving defaults for unset (zero) overrides.
func mergeLines(defaults, overrides tw.Lines) tw.Lines {
if overrides.ShowTop != 0 {
defaults.ShowTop = overrides.ShowTop
}
if overrides.ShowBottom != 0 {
defaults.ShowBottom = overrides.ShowBottom
}
if overrides.ShowHeaderLine != 0 {
defaults.ShowHeaderLine = overrides.ShowHeaderLine
}
if overrides.ShowFooterLine != 0 {
defaults.ShowFooterLine = overrides.ShowFooterLine
}
return defaults
}
// mergeSeparators combines default and override separator settings, preserving defaults for unset (zero) overrides.
func mergeSeparators(defaults, overrides tw.Separators) tw.Separators {
if overrides.ShowHeader != 0 {
defaults.ShowHeader = overrides.ShowHeader
}
if overrides.ShowFooter != 0 {
defaults.ShowFooter = overrides.ShowFooter
}
if overrides.BetweenRows != 0 {
defaults.BetweenRows = overrides.BetweenRows
}
if overrides.BetweenColumns != 0 {
defaults.BetweenColumns = overrides.BetweenColumns
}
return defaults
}
// mergeSettings combines default and override settings, preserving defaults for unset (zero) overrides.
func mergeSettings(defaults, overrides tw.Settings) tw.Settings {
if overrides.Separators.ShowHeader != tw.Unknown {
defaults.Separators.ShowHeader = overrides.Separators.ShowHeader
}
if overrides.Separators.ShowFooter != tw.Unknown {
defaults.Separators.ShowFooter = overrides.Separators.ShowFooter
}
if overrides.Separators.BetweenRows != tw.Unknown {
defaults.Separators.BetweenRows = overrides.Separators.BetweenRows
}
if overrides.Separators.BetweenColumns != tw.Unknown {
defaults.Separators.BetweenColumns = overrides.Separators.BetweenColumns
}
if overrides.Lines.ShowTop != tw.Unknown {
defaults.Lines.ShowTop = overrides.Lines.ShowTop
}
if overrides.Lines.ShowBottom != tw.Unknown {
defaults.Lines.ShowBottom = overrides.Lines.ShowBottom
}
if overrides.Lines.ShowHeaderLine != tw.Unknown {
defaults.Lines.ShowHeaderLine = overrides.Lines.ShowHeaderLine
}
if overrides.Lines.ShowFooterLine != tw.Unknown {
defaults.Lines.ShowFooterLine = overrides.Lines.ShowFooterLine
}
if overrides.CompactMode != tw.Unknown {
defaults.CompactMode = overrides.CompactMode
}
//if overrides.Cushion != tw.Unknown {
// defaults.Cushion = overrides.Cushion
//}
return defaults
}
// MergeRendition merges the 'override' rendition into the 'current' rendition.
// It only updates fields in 'current' if they are explicitly set (non-zero/non-nil) in 'override'.
// This allows for partial updates to a renderer's configuration.
func mergeRendition(current, override tw.Rendition) tw.Rendition {
// Merge Borders: Only update if override border states are explicitly set (not 0).
// A tw.State's zero value is 0, which is distinct from tw.On (1) or tw.Off (-1).
// So, if override.Borders.Left is 0, it means "not specified", so we keep current.
if override.Borders.Left != 0 {
current.Borders.Left = override.Borders.Left
}
if override.Borders.Right != 0 {
current.Borders.Right = override.Borders.Right
}
if override.Borders.Top != 0 {
current.Borders.Top = override.Borders.Top
}
if override.Borders.Bottom != 0 {
current.Borders.Bottom = override.Borders.Bottom
}
// Merge Symbols: Only update if override.Symbols is not nil.
if override.Symbols != nil {
current.Symbols = override.Symbols
}
// Merge Settings: Use the existing mergeSettings for granular control.
// mergeSettings already handles preserving defaults for unset (zero) overrides.
current.Settings = mergeSettings(current.Settings, override.Settings)
// Streaming flag: typically set at renderer creation, but can be overridden if needed.
// For now, let's assume it's not commonly changed post-creation by a generic rendition merge.
// If override provides a different streaming capability, it might indicate a fundamental
// change that a simple merge shouldn't handle without more context.
// current.Streaming = override.Streaming // Or keep current.Streaming
return current
}

View File

@@ -0,0 +1,440 @@
package renderer
import (
"errors"
"fmt"
"github.com/olekukonko/ll"
"html"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw"
)
// HTMLConfig defines settings for the HTML table renderer.
type HTMLConfig struct {
EscapeContent bool // Whether to escape cell content
AddLinesTag bool // Whether to wrap multiline content in <lines> tags
TableClass string // CSS class for <table>
HeaderClass string // CSS class for <thead>
BodyClass string // CSS class for <tbody>
FooterClass string // CSS class for <tfoot>
RowClass string // CSS class for <tr> in body
HeaderRowClass string // CSS class for <tr> in header
FooterRowClass string // CSS class for <tr> in footer
}
// HTML renders tables in HTML format with customizable classes and content handling.
type HTML struct {
config HTMLConfig // Renderer configuration
w io.Writer // Output w
trace []string // Debug trace messages
debug bool // Enables debug logging
tableStarted bool // Tracks if <table> tag is open
tbodyStarted bool // Tracks if <tbody> tag is open
tfootStarted bool // Tracks if <tfoot> tag is open
vMergeTrack map[int]int // Tracks vertical merge spans by column index
logger *ll.Logger
}
// NewHTML initializes an HTML renderer with the given w, debug setting, and optional configuration.
// It panics if the w is nil and applies defaults for unset config fields.
// Update: see https://github.com/olekukonko/tablewriter/issues/258
func NewHTML(configs ...HTMLConfig) *HTML {
cfg := HTMLConfig{
EscapeContent: true,
AddLinesTag: false,
}
if len(configs) > 0 {
userCfg := configs[0]
cfg.EscapeContent = userCfg.EscapeContent
cfg.AddLinesTag = userCfg.AddLinesTag
cfg.TableClass = userCfg.TableClass
cfg.HeaderClass = userCfg.HeaderClass
cfg.BodyClass = userCfg.BodyClass
cfg.FooterClass = userCfg.FooterClass
cfg.RowClass = userCfg.RowClass
cfg.HeaderRowClass = userCfg.HeaderRowClass
cfg.FooterRowClass = userCfg.FooterRowClass
}
return &HTML{
config: cfg,
vMergeTrack: make(map[int]int),
tableStarted: false,
tbodyStarted: false,
tfootStarted: false,
}
}
func (h *HTML) Logger(logger *ll.Logger) {
h.logger = logger
}
// Config returns a Rendition representation of the current configuration.
func (h *HTML) Config() tw.Rendition {
return tw.Rendition{
Borders: tw.BorderNone,
Symbols: tw.NewSymbols(tw.StyleNone),
Settings: tw.Settings{},
Streaming: false,
}
}
// debugLog appends a formatted message to the debug trace if debugging is enabled.
//func (h *HTML) debugLog(format string, a ...interface{}) {
// if h.debug {
// msg := fmt.Sprintf(format, a...)
// h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg))
// }
//}
// Debug returns the accumulated debug trace messages.
func (h *HTML) Debug() []string {
return h.trace
}
// Start begins the HTML table rendering by opening the <table> tag.
func (h *HTML) Start(w io.Writer) error {
h.w = w
h.Reset()
h.logger.Debug("HTML.Start() called.")
classAttr := tw.Empty
if h.config.TableClass != tw.Empty {
classAttr = fmt.Sprintf(` class="%s"`, h.config.TableClass)
}
h.logger.Debugf("Writing opening <table%s> tag", classAttr)
_, err := fmt.Fprintf(h.w, "<table%s>\n", classAttr)
if err != nil {
return err
}
h.tableStarted = true
return nil
}
// closePreviousSection closes any open <tbody> or <tfoot> sections.
func (h *HTML) closePreviousSection() {
if h.tbodyStarted {
h.logger.Debug("Closing <tbody> tag")
fmt.Fprintln(h.w, "</tbody>")
h.tbodyStarted = false
}
if h.tfootStarted {
h.logger.Debug("Closing <tfoot> tag")
fmt.Fprintln(h.w, "</tfoot>")
h.tfootStarted = false
}
}
// Header renders the <thead> section with header rows, supporting horizontal merges.
func (h *HTML) Header(headers [][]string, ctx tw.Formatting) {
if !h.tableStarted {
h.logger.Debug("WARN: Header called before Start")
return
}
if len(headers) == 0 || len(headers[0]) == 0 {
h.logger.Debug("Header: No headers")
return
}
h.closePreviousSection()
classAttr := tw.Empty
if h.config.HeaderClass != tw.Empty {
classAttr = fmt.Sprintf(` class="%s"`, h.config.HeaderClass)
}
fmt.Fprintf(h.w, "<thead%s>\n", classAttr)
headerRow := headers[0]
numCols := 0
if len(ctx.Row.Current) > 0 {
maxKey := -1
for k := range ctx.Row.Current {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
} else if len(headerRow) > 0 {
numCols = len(headerRow)
}
indent := " "
rowClassAttr := tw.Empty
if h.config.HeaderRowClass != tw.Empty {
rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.HeaderRowClass)
}
fmt.Fprintf(h.w, "%s<tr%s>", indent, rowClassAttr)
renderedCols := 0
for colIdx := 0; renderedCols < numCols && colIdx < numCols; {
// Skip columns consumed by vertical merges
if remainingSpan, merging := h.vMergeTrack[colIdx]; merging && remainingSpan > 1 {
h.logger.Debugf("Header: Skipping col %d due to vmerge", colIdx)
h.vMergeTrack[colIdx]--
if h.vMergeTrack[colIdx] <= 1 {
delete(h.vMergeTrack, colIdx)
}
colIdx++
continue
}
// Render cell
cellCtx, ok := ctx.Row.Current[colIdx]
if !ok {
cellCtx = tw.CellContext{Align: tw.AlignCenter}
}
originalContent := tw.Empty
if colIdx < len(headerRow) {
originalContent = headerRow[colIdx]
}
tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, true, colIdx)
fmt.Fprintf(h.w, "<%s%s>%s</%s>", tag, attributes, processedContent, tag)
renderedCols++
// Handle horizontal merges
hSpan := 1
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
renderedCols += (hSpan - 1)
}
colIdx += hSpan
}
fmt.Fprintf(h.w, "</tr>\n")
fmt.Fprintln(h.w, "</thead>")
}
// Row renders a <tr> element within <tbody>, supporting horizontal and vertical merges.
func (h *HTML) Row(row []string, ctx tw.Formatting) {
if !h.tableStarted {
h.logger.Debug("WARN: Row called before Start")
return
}
if !h.tbodyStarted {
h.closePreviousSection()
classAttr := tw.Empty
if h.config.BodyClass != tw.Empty {
classAttr = fmt.Sprintf(` class="%s"`, h.config.BodyClass)
}
h.logger.Debugf("Writing opening <tbody%s> tag", classAttr)
fmt.Fprintf(h.w, "<tbody%s>\n", classAttr)
h.tbodyStarted = true
}
h.logger.Debugf("Rendering row data: %v", row)
numCols := 0
if len(ctx.Row.Current) > 0 {
maxKey := -1
for k := range ctx.Row.Current {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
} else if len(row) > 0 {
numCols = len(row)
}
indent := " "
rowClassAttr := tw.Empty
if h.config.RowClass != tw.Empty {
rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.RowClass)
}
fmt.Fprintf(h.w, "%s<tr%s>", indent, rowClassAttr)
renderedCols := 0
for colIdx := 0; renderedCols < numCols && colIdx < numCols; {
// Skip columns consumed by vertical merges
if remainingSpan, merging := h.vMergeTrack[colIdx]; merging && remainingSpan > 1 {
h.logger.Debugf("Row: Skipping render for col %d due to vertical merge (remaining %d)", colIdx, remainingSpan-1)
h.vMergeTrack[colIdx]--
if h.vMergeTrack[colIdx] <= 1 {
delete(h.vMergeTrack, colIdx)
}
colIdx++
continue
}
// Render cell
cellCtx, ok := ctx.Row.Current[colIdx]
if !ok {
cellCtx = tw.CellContext{Align: tw.AlignLeft}
}
originalContent := tw.Empty
if colIdx < len(row) {
originalContent = row[colIdx]
}
tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, false, colIdx)
fmt.Fprintf(h.w, "<%s%s>%s</%s>", tag, attributes, processedContent, tag)
renderedCols++
// Handle horizontal merges
hSpan := 1
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
renderedCols += (hSpan - 1)
}
colIdx += hSpan
}
fmt.Fprintf(h.w, "</tr>\n")
}
// Footer renders the <tfoot> section with footer rows, supporting horizontal merges.
func (h *HTML) Footer(footers [][]string, ctx tw.Formatting) {
if !h.tableStarted {
h.logger.Debug("WARN: Footer called before Start")
return
}
if len(footers) == 0 || len(footers[0]) == 0 {
h.logger.Debug("Footer: No footers")
return
}
h.closePreviousSection()
classAttr := tw.Empty
if h.config.FooterClass != tw.Empty {
classAttr = fmt.Sprintf(` class="%s"`, h.config.FooterClass)
}
fmt.Fprintf(h.w, "<tfoot%s>\n", classAttr)
h.tfootStarted = true
footerRow := footers[0]
numCols := 0
if len(ctx.Row.Current) > 0 {
maxKey := -1
for k := range ctx.Row.Current {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
} else if len(footerRow) > 0 {
numCols = len(footerRow)
}
indent := " "
rowClassAttr := tw.Empty
if h.config.FooterRowClass != tw.Empty {
rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.FooterRowClass)
}
fmt.Fprintf(h.w, "%s<tr%s>", indent, rowClassAttr)
renderedCols := 0
for colIdx := 0; renderedCols < numCols && colIdx < numCols; {
cellCtx, ok := ctx.Row.Current[colIdx]
if !ok {
cellCtx = tw.CellContext{Align: tw.AlignRight}
}
originalContent := tw.Empty
if colIdx < len(footerRow) {
originalContent = footerRow[colIdx]
}
tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, false, colIdx)
fmt.Fprintf(h.w, "<%s%s>%s</%s>", tag, attributes, processedContent, tag)
renderedCols++
hSpan := 1
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
renderedCols += (hSpan - 1)
}
colIdx += hSpan
}
fmt.Fprintf(h.w, "</tr>\n")
fmt.Fprintln(h.w, "</tfoot>")
h.tfootStarted = false
}
// renderRowCell generates HTML for a single cell, handling content escaping, merges, and alignment.
func (h *HTML) renderRowCell(originalContent string, cellCtx tw.CellContext, isHeader bool, colIdx int) (tag, attributes, processedContent string) {
tag = "td"
if isHeader {
tag = "th"
}
// Process content
processedContent = originalContent
containsNewline := strings.Contains(originalContent, "\n")
if h.config.EscapeContent {
if containsNewline {
const newlinePlaceholder = "[[--HTML_RENDERER_BR_PLACEHOLDER--]]"
tempContent := strings.ReplaceAll(originalContent, "\n", newlinePlaceholder)
escapedContent := html.EscapeString(tempContent)
processedContent = strings.ReplaceAll(escapedContent, newlinePlaceholder, "<br>")
} else {
processedContent = html.EscapeString(originalContent)
}
} else if containsNewline {
processedContent = strings.ReplaceAll(originalContent, "\n", "<br>")
}
if containsNewline && h.config.AddLinesTag {
processedContent = "<lines>" + processedContent + "</lines>"
}
// Build attributes
var attrBuilder strings.Builder
merge := cellCtx.Merge
if merge.Horizontal.Present && merge.Horizontal.Start && merge.Horizontal.Span > 1 {
fmt.Fprintf(&attrBuilder, ` colspan="%d"`, merge.Horizontal.Span)
}
vSpan := 0
if !isHeader {
if merge.Vertical.Present && merge.Vertical.Start {
vSpan = merge.Vertical.Span
} else if merge.Hierarchical.Present && merge.Hierarchical.Start {
vSpan = merge.Hierarchical.Span
}
if vSpan > 1 {
fmt.Fprintf(&attrBuilder, ` rowspan="%d"`, vSpan)
h.vMergeTrack[colIdx] = vSpan
h.logger.Debugf("renderRowCell: Tracking rowspan=%d for col %d", vSpan, colIdx)
}
}
if style := getHTMLStyle(cellCtx.Align); style != tw.Empty {
attrBuilder.WriteString(style)
}
attributes = attrBuilder.String()
return
}
// Line is a no-op for HTML rendering, as structural lines are handled by tags.
func (h *HTML) Line(ctx tw.Formatting) {}
// Reset clears the renderer's internal state, including debug traces and merge tracking.
func (h *HTML) Reset() {
h.logger.Debug("HTML.Reset() called.")
h.tableStarted = false
h.tbodyStarted = false
h.tfootStarted = false
h.vMergeTrack = make(map[int]int)
h.trace = nil
}
// Close ensures all open HTML tags (<table>, <tbody>, <tfoot>) are properly closed.
func (h *HTML) Close() error {
if h.w == nil {
return errors.New("HTML Renderer Close called on nil internal w")
}
if h.tableStarted {
h.logger.Debug("HTML.Close() called.")
h.closePreviousSection()
h.logger.Debug("Closing <table> tag.")
_, err := fmt.Fprintln(h.w, "</table>")
h.tableStarted = false
h.tbodyStarted = false
h.tfootStarted = false
h.vMergeTrack = make(map[int]int)
return err
}
h.logger.Debug("HTML.Close() called, but table was not started (no-op).")
return nil
}

View File

@@ -0,0 +1,273 @@
package renderer
import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
)
// Junction handles rendering of table junction points (corners, intersections) with color support.
type Junction struct {
sym tw.Symbols // Symbols used for rendering junctions and lines
ctx tw.Formatting // Current table formatting context
colIdx int // Index of the column being processed
debugging bool // Enables debug logging
borderTint Tint // Colors for border symbols
separatorTint Tint // Colors for separator symbols
logger *ll.Logger
}
type JunctionContext struct {
Symbols tw.Symbols
Ctx tw.Formatting
ColIdx int
Logger *ll.Logger
BorderTint Tint
SeparatorTint Tint
}
// NewJunction initializes a Junction with the given symbols, context, and tints.
// If debug is nil, a no-op debug function is used.
func NewJunction(ctx JunctionContext) *Junction {
return &Junction{
sym: ctx.Symbols,
ctx: ctx.Ctx,
colIdx: ctx.ColIdx,
logger: ctx.Logger.Namespace("junction"),
borderTint: ctx.BorderTint,
separatorTint: ctx.SeparatorTint,
}
}
// getMergeState retrieves the merge state for a specific column in a row, returning an empty state if not found.
func (jr *Junction) getMergeState(row map[int]tw.CellContext, colIdx int) tw.MergeState {
if row == nil || colIdx < 0 {
return tw.MergeState{}
}
return row[colIdx].Merge
}
// GetSegment determines whether to render a colored horizontal line or an empty space based on merge states.
func (jr *Junction) GetSegment() string {
currentMerge := jr.getMergeState(jr.ctx.Row.Current, jr.colIdx)
nextMerge := jr.getMergeState(jr.ctx.Row.Next, jr.colIdx)
vPassThruStrict := (currentMerge.Vertical.Present && nextMerge.Vertical.Present && !currentMerge.Vertical.End && !nextMerge.Vertical.Start) ||
(currentMerge.Hierarchical.Present && nextMerge.Hierarchical.Present && !currentMerge.Hierarchical.End && !nextMerge.Hierarchical.Start)
if vPassThruStrict {
jr.logger.Debugf("GetSegment col %d: VPassThruStrict=%v -> Empty segment", jr.colIdx, vPassThruStrict)
return tw.Empty
}
symbol := jr.sym.Row()
coloredSymbol := jr.borderTint.Apply(symbol)
jr.logger.Debugf("GetSegment col %d: VPassThruStrict=%v -> Colored row symbol '%s'", jr.colIdx, vPassThruStrict, coloredSymbol)
return coloredSymbol
}
// RenderLeft selects and colors the leftmost junction symbol for the current row line based on position and merges.
func (jr *Junction) RenderLeft() string {
mergeAbove := jr.getMergeState(jr.ctx.Row.Current, 0)
mergeBelow := jr.getMergeState(jr.ctx.Row.Next, 0)
jr.logger.Debugf("RenderLeft: Level=%v, Location=%v, Previous=%v", jr.ctx.Level, jr.ctx.Row.Location, jr.ctx.Row.Previous)
isTopBorder := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) ||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && jr.ctx.Row.Previous == nil)
if isTopBorder {
symbol := jr.sym.TopLeft()
return jr.borderTint.Apply(symbol)
}
isBottom := jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter
isFooter := jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd
if isBottom || isFooter {
symbol := jr.sym.BottomLeft()
return jr.borderTint.Apply(symbol)
}
isVPassThruStrict := (mergeAbove.Vertical.Present && mergeBelow.Vertical.Present && !mergeAbove.Vertical.End && !mergeBelow.Vertical.Start) ||
(mergeAbove.Hierarchical.Present && mergeBelow.Hierarchical.Present && !mergeAbove.Hierarchical.End && !mergeBelow.Hierarchical.Start)
if isVPassThruStrict {
symbol := jr.sym.Column()
return jr.separatorTint.Apply(symbol)
}
symbol := jr.sym.MidLeft()
return jr.borderTint.Apply(symbol)
}
// RenderRight selects and colors the rightmost junction symbol for the row line based on position, merges, and last column index.
func (jr *Junction) RenderRight(lastColIdx int) string {
jr.logger.Debugf("RenderRight: lastColIdx=%d, Level=%v, Location=%v, Previous=%v", lastColIdx, jr.ctx.Level, jr.ctx.Row.Location, jr.ctx.Row.Previous)
if lastColIdx < 0 {
switch jr.ctx.Level {
case tw.LevelHeader:
symbol := jr.sym.TopRight()
return jr.borderTint.Apply(symbol)
case tw.LevelFooter:
symbol := jr.sym.BottomRight()
return jr.borderTint.Apply(symbol)
default:
if jr.ctx.Row.Location == tw.LocationFirst {
symbol := jr.sym.TopRight()
return jr.borderTint.Apply(symbol)
}
if jr.ctx.Row.Location == tw.LocationEnd {
symbol := jr.sym.BottomRight()
return jr.borderTint.Apply(symbol)
}
symbol := jr.sym.MidRight()
return jr.borderTint.Apply(symbol)
}
}
mergeAbove := jr.getMergeState(jr.ctx.Row.Current, lastColIdx)
mergeBelow := jr.getMergeState(jr.ctx.Row.Next, lastColIdx)
isTopBorder := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) ||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && jr.ctx.Row.Previous == nil)
if isTopBorder {
symbol := jr.sym.TopRight()
return jr.borderTint.Apply(symbol)
}
isBottom := jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter
isFooter := jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd
if isBottom || isFooter {
symbol := jr.sym.BottomRight()
return jr.borderTint.Apply(symbol)
}
isVPassThruStrict := (mergeAbove.Vertical.Present && mergeBelow.Vertical.Present && !mergeAbove.Vertical.End && !mergeBelow.Vertical.Start) ||
(mergeAbove.Hierarchical.Present && mergeBelow.Hierarchical.Present && !mergeAbove.Hierarchical.End && !mergeBelow.Hierarchical.Start)
if isVPassThruStrict {
symbol := jr.sym.Column()
return jr.separatorTint.Apply(symbol)
}
symbol := jr.sym.MidRight()
return jr.borderTint.Apply(symbol)
}
// RenderJunction selects and colors the junction symbol between two adjacent columns based on merge states and table position.
func (jr *Junction) RenderJunction(leftColIdx, rightColIdx int) string {
mergeCurrentL := jr.getMergeState(jr.ctx.Row.Current, leftColIdx)
mergeCurrentR := jr.getMergeState(jr.ctx.Row.Current, rightColIdx)
mergeNextL := jr.getMergeState(jr.ctx.Row.Next, leftColIdx)
mergeNextR := jr.getMergeState(jr.ctx.Row.Next, rightColIdx)
isSpannedCurrent := mergeCurrentL.Horizontal.Present && !mergeCurrentL.Horizontal.End
isSpannedNext := mergeNextL.Horizontal.Present && !mergeNextL.Horizontal.End
vPassThruLStrict := (mergeCurrentL.Vertical.Present && mergeNextL.Vertical.Present && !mergeCurrentL.Vertical.End && !mergeNextL.Vertical.Start) ||
(mergeCurrentL.Hierarchical.Present && mergeNextL.Hierarchical.Present && !mergeCurrentL.Hierarchical.End && !mergeNextL.Hierarchical.Start)
vPassThruRStrict := (mergeCurrentR.Vertical.Present && mergeNextR.Vertical.Present && !mergeCurrentR.Vertical.End && !mergeNextR.Vertical.Start) ||
(mergeCurrentR.Hierarchical.Present && mergeNextR.Hierarchical.Present && !mergeCurrentR.Hierarchical.End && !mergeNextR.Hierarchical.Start)
isTop := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) ||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && len(jr.ctx.Row.Previous) == 0)
isBottom := (jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd) ||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter)
isPreFooter := jr.ctx.Level == tw.LevelFooter && (jr.ctx.Row.Position == tw.Row || jr.ctx.Row.Position == tw.Header)
if isTop {
if isSpannedNext {
symbol := jr.sym.Row()
return jr.borderTint.Apply(symbol)
}
symbol := jr.sym.TopMid()
return jr.borderTint.Apply(symbol)
}
if isBottom {
if vPassThruLStrict && vPassThruRStrict {
symbol := jr.sym.Column()
return jr.separatorTint.Apply(symbol)
}
if vPassThruLStrict {
symbol := jr.sym.MidLeft()
return jr.borderTint.Apply(symbol)
}
if vPassThruRStrict {
symbol := jr.sym.MidRight()
return jr.borderTint.Apply(symbol)
}
if isSpannedCurrent {
symbol := jr.sym.Row()
return jr.borderTint.Apply(symbol)
}
symbol := jr.sym.BottomMid()
return jr.borderTint.Apply(symbol)
}
if isPreFooter {
if vPassThruLStrict && vPassThruRStrict {
symbol := jr.sym.Column()
return jr.separatorTint.Apply(symbol)
}
if vPassThruLStrict {
symbol := jr.sym.MidLeft()
return jr.borderTint.Apply(symbol)
}
if vPassThruRStrict {
symbol := jr.sym.MidRight()
return jr.borderTint.Apply(symbol)
}
if mergeCurrentL.Horizontal.Present {
if !mergeCurrentL.Horizontal.End && mergeCurrentR.Horizontal.Present && !mergeCurrentR.Horizontal.End {
jr.logger.Debugf("Footer separator: H-merge continues from col %d to %d (mid-span), using BottomMid", leftColIdx, rightColIdx)
symbol := jr.sym.BottomMid()
return jr.borderTint.Apply(symbol)
}
if !mergeCurrentL.Horizontal.End && mergeCurrentR.Horizontal.Present && mergeCurrentR.Horizontal.End {
jr.logger.Debugf("Footer separator: H-merge ends at col %d, using BottomMid", rightColIdx)
symbol := jr.sym.BottomMid()
return jr.borderTint.Apply(symbol)
}
if mergeCurrentL.Horizontal.End && !mergeCurrentR.Horizontal.Present {
jr.logger.Debugf("Footer separator: H-merge ends at col %d, next col %d not merged, using Center", leftColIdx, rightColIdx)
symbol := jr.sym.Center()
return jr.borderTint.Apply(symbol)
}
}
if isSpannedNext {
symbol := jr.sym.BottomMid()
return jr.borderTint.Apply(symbol)
}
if isSpannedCurrent {
symbol := jr.sym.TopMid()
return jr.borderTint.Apply(symbol)
}
symbol := jr.sym.Center()
return jr.borderTint.Apply(symbol)
}
if vPassThruLStrict && vPassThruRStrict {
symbol := jr.sym.Column()
return jr.separatorTint.Apply(symbol)
}
if vPassThruLStrict {
symbol := jr.sym.MidLeft()
return jr.borderTint.Apply(symbol)
}
if vPassThruRStrict {
symbol := jr.sym.MidRight()
return jr.borderTint.Apply(symbol)
}
if isSpannedCurrent && isSpannedNext {
symbol := jr.sym.Row()
return jr.borderTint.Apply(symbol)
}
if isSpannedCurrent {
symbol := jr.sym.TopMid()
return jr.borderTint.Apply(symbol)
}
if isSpannedNext {
symbol := jr.sym.BottomMid()
return jr.borderTint.Apply(symbol)
}
symbol := jr.sym.Center()
return jr.borderTint.Apply(symbol)
}

View File

@@ -0,0 +1,419 @@
package renderer
import (
"github.com/olekukonko/ll"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw"
)
// Markdown renders tables in Markdown format with customizable settings.
type Markdown struct {
config tw.Rendition // Rendering configuration
logger *ll.Logger // Debug trace messages
alignment tw.Alignment // alias of []tw.Align
w io.Writer
}
// NewMarkdown initializes a Markdown renderer with defaults tailored for Markdown (e.g., pipes, header separator).
// Only the first config is used if multiple are provided.
func NewMarkdown(configs ...tw.Rendition) *Markdown {
cfg := defaultBlueprint()
// Configure Markdown-specific defaults
cfg.Symbols = tw.NewSymbols(tw.StyleMarkdown)
cfg.Borders = tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off}
cfg.Settings.Separators.BetweenColumns = tw.On
cfg.Settings.Separators.BetweenRows = tw.Off
cfg.Settings.Lines.ShowHeaderLine = tw.On
cfg.Settings.Lines.ShowTop = tw.Off
cfg.Settings.Lines.ShowBottom = tw.Off
cfg.Settings.Lines.ShowFooterLine = tw.Off
// cfg.Settings.TrimWhitespace = tw.On
// Apply user overrides
if len(configs) > 0 {
cfg = mergeMarkdownConfig(cfg, configs[0])
}
return &Markdown{config: cfg}
}
// mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings.
func mergeMarkdownConfig(defaults, overrides tw.Rendition) tw.Rendition {
if overrides.Borders.Left != 0 {
defaults.Borders.Left = overrides.Borders.Left
}
if overrides.Borders.Right != 0 {
defaults.Borders.Right = overrides.Borders.Right
}
if overrides.Symbols != nil {
defaults.Symbols = overrides.Symbols
}
defaults.Settings = mergeSettings(defaults.Settings, overrides.Settings)
// Enforce Markdown requirements
defaults.Settings.Lines.ShowHeaderLine = tw.On
defaults.Settings.Separators.BetweenColumns = tw.On
// defaults.Settings.TrimWhitespace = tw.On
return defaults
}
func (m *Markdown) Logger(logger *ll.Logger) {
m.logger = logger.Namespace("markdown")
}
// Config returns the renderer's current configuration.
func (m *Markdown) Config() tw.Rendition {
return m.config
}
// Header renders the Markdown table header and its separator line.
func (m *Markdown) Header(headers [][]string, ctx tw.Formatting) {
m.resolveAlignment(ctx)
if len(headers) == 0 || len(headers[0]) == 0 {
m.logger.Debug("Header: No headers to render")
return
}
m.logger.Debugf("Rendering header with %d lines, widths=%v, current=%v, next=%v", len(headers), ctx.Row.Widths, ctx.Row.Current, ctx.Row.Next)
// Render header content
m.renderMarkdownLine(headers[0], ctx, false)
// Render separator if enabled
if m.config.Settings.Lines.ShowHeaderLine.Enabled() {
sepCtx := ctx
sepCtx.Row.Widths = ctx.Row.Widths
sepCtx.Row.Current = ctx.Row.Current
sepCtx.Row.Previous = ctx.Row.Current
sepCtx.IsSubRow = true
m.renderMarkdownLine(nil, sepCtx, true)
}
}
// Row renders a Markdown table data row.
func (m *Markdown) Row(row []string, ctx tw.Formatting) {
m.resolveAlignment(ctx)
m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
m.renderMarkdownLine(row, ctx, false)
}
// Footer renders the Markdown table footer.
func (m *Markdown) Footer(footers [][]string, ctx tw.Formatting) {
m.resolveAlignment(ctx)
if len(footers) == 0 || len(footers[0]) == 0 {
m.logger.Debug("Footer: No footers to render")
return
}
m.logger.Debugf("Rendering footer with %d lines, widths=%v, previous=%v, current=%v, next=%v",
len(footers), ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
m.renderMarkdownLine(footers[0], ctx, false)
}
// Line is a no-op for Markdown, as only the header separator is rendered (handled by Header).
func (m *Markdown) Line(ctx tw.Formatting) {
m.logger.Debugf("Line: Generic Line call received (pos: %s, loc: %s). Markdown ignores these.", ctx.Row.Position, ctx.Row.Location)
}
// Reset clears the renderer's internal state, including debug traces.
func (m *Markdown) Reset() {
m.logger.Info("Reset: Cleared debug trace")
}
func (m *Markdown) Start(w io.Writer) error {
m.w = w
m.logger.Warn("Markdown.Start() called (no-op).")
return nil
}
func (m *Markdown) Close() error {
m.logger.Warn("Markdown.Close() called (no-op).")
return nil
}
func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
if len(m.alignment) != 0 {
return m.alignment
}
// get total columns
total := len(ctx.Row.Current)
// build default alignment
for i := 0; i < total; i++ {
m.alignment = append(m.alignment, tw.AlignLeft)
}
// add per colum alignment if it exits
for i := 0; i < total; i++ {
m.alignment[i] = ctx.Row.Current[i].Align
}
m.logger.Debugf(" → Align Resolved %s", m.alignment)
return m.alignment
}
// formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide.
func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string {
//if m.config.Settings.TrimWhitespace.Enabled() {
// content = strings.TrimSpace(content)
//}
contentVisualWidth := tw.DisplayWidth(content)
// Use specified padding characters or default to spaces
padLeftChar := padding.Left
if padLeftChar == tw.Empty {
padLeftChar = tw.Space
}
padRightChar := padding.Right
if padRightChar == tw.Empty {
padRightChar = tw.Space
}
// Calculate padding widths
padLeftCharWidth := tw.DisplayWidth(padLeftChar)
padRightCharWidth := tw.DisplayWidth(padRightChar)
minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth)
targetWidth := tw.Max(width, minWidth)
// Calculate padding
totalPaddingNeeded := targetWidth - contentVisualWidth
if totalPaddingNeeded < 0 {
totalPaddingNeeded = 0
}
var leftPadStr, rightPadStr string
switch align {
case tw.AlignRight:
leftPadCount := tw.Max(0, totalPaddingNeeded-padRightCharWidth)
rightPadCount := totalPaddingNeeded - leftPadCount
leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
rightPadStr = strings.Repeat(padRightChar, rightPadCount)
case tw.AlignCenter:
leftPadCount := totalPaddingNeeded / 2
rightPadCount := totalPaddingNeeded - leftPadCount
if leftPadCount < padLeftCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth {
leftPadCount = padLeftCharWidth
rightPadCount = totalPaddingNeeded - leftPadCount
}
if rightPadCount < padRightCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth {
rightPadCount = padRightCharWidth
leftPadCount = totalPaddingNeeded - rightPadCount
}
leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
rightPadStr = strings.Repeat(padRightChar, rightPadCount)
default: // AlignLeft
rightPadCount := tw.Max(0, totalPaddingNeeded-padLeftCharWidth)
leftPadCount := totalPaddingNeeded - rightPadCount
leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
rightPadStr = strings.Repeat(padRightChar, rightPadCount)
}
// Build result
result := leftPadStr + content + rightPadStr
// Adjust width if needed
finalWidth := tw.DisplayWidth(result)
if finalWidth != targetWidth {
m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d",
content, targetWidth, padding.Left, padding.Right, align, result, finalWidth)
adjNeeded := targetWidth - finalWidth
if adjNeeded > 0 {
adjStr := strings.Repeat(tw.Space, adjNeeded)
if align == tw.AlignRight {
result = adjStr + result
} else if align == tw.AlignCenter {
leftAdj := adjNeeded / 2
rightAdj := adjNeeded - leftAdj
result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj)
} else {
result += adjStr
}
} else {
result = tw.TruncateString(result, targetWidth)
}
m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, tw.DisplayWidth(result))
}
m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)",
content, width, align, padding.Left, padding.Right, result, targetWidth)
return result
}
// formatSeparator generates a Markdown separator (e.g., `---`, `:--`, `:-:`) with alignment indicators.
func (m *Markdown) formatSeparator(width int, align tw.Align) string {
targetWidth := tw.Max(3, width)
var sb strings.Builder
switch align {
case tw.AlignLeft:
sb.WriteRune(':')
sb.WriteString(strings.Repeat("-", targetWidth-1))
case tw.AlignRight:
sb.WriteString(strings.Repeat("-", targetWidth-1))
sb.WriteRune(':')
case tw.AlignCenter:
sb.WriteRune(':')
sb.WriteString(strings.Repeat("-", targetWidth-2))
sb.WriteRune(':')
default:
sb.WriteRune(':')
sb.WriteString(strings.Repeat("-", targetWidth-1))
}
result := sb.String()
currentLen := tw.DisplayWidth(result)
if currentLen < targetWidth {
result += strings.Repeat("-", targetWidth-currentLen)
} else if currentLen > targetWidth {
result = tw.TruncateString(result, targetWidth)
}
m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result)
return result
}
// renderMarkdownLine renders a single Markdown line (header, row, footer, or separator) with pipes and alignment.
func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeaderSep bool) {
numCols := 0
if len(ctx.Row.Widths) > 0 {
maxKey := -1
for k := range ctx.Row.Widths {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
} else if len(ctx.Row.Current) > 0 {
maxKey := -1
for k := range ctx.Row.Current {
if k > maxKey {
maxKey = k
}
}
numCols = maxKey + 1
} else if len(line) > 0 && !isHeaderSep {
numCols = len(line)
}
if numCols == 0 && !isHeaderSep {
m.logger.Debug("renderMarkdownLine: Skipping line with zero columns.")
return
}
var output strings.Builder
prefix := m.config.Symbols.Column()
if m.config.Borders.Left == tw.Off {
prefix = tw.Empty
}
suffix := m.config.Symbols.Column()
if m.config.Borders.Right == tw.Off {
suffix = tw.Empty
}
separator := m.config.Symbols.Column()
output.WriteString(prefix)
colIndex := 0
separatorWidth := tw.DisplayWidth(separator)
for colIndex < numCols {
cellCtx, ok := ctx.Row.Current[colIndex]
align := m.alignment[colIndex]
defaultPadding := tw.Padding{Left: tw.Space, Right: tw.Space}
if !ok {
cellCtx = tw.CellContext{
Data: tw.Empty, Align: align, Padding: defaultPadding,
Width: ctx.Row.Widths.Get(colIndex), Merge: tw.MergeState{},
}
} else if cellCtx.Padding == (tw.Padding{}) {
cellCtx.Padding = defaultPadding
}
// Add separator
isContinuation := ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start
if colIndex > 0 && !isContinuation {
output.WriteString(separator)
m.logger.Debugf("renderMarkdownLine: Added separator '%s' before col %d", separator, colIndex)
}
// Calculate width and span
span := 1
if align == tw.AlignNone || align == tw.Empty {
if ctx.Row.Position == tw.Header && !isHeaderSep {
align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
align = tw.AlignRight
} else {
align = tw.AlignLeft
}
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, align)
}
visualWidth := 0
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
if isHMergeStart {
span = cellCtx.Merge.Horizontal.Span
totalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ {
colWidth := ctx.NormalizedWidths.Get(colIndex + k)
if colWidth < 0 {
colWidth = 0
}
totalWidth += colWidth
if k > 0 && separatorWidth > 0 {
totalWidth += separatorWidth
}
}
visualWidth = totalWidth
m.logger.Debugf("renderMarkdownLine: HMerge col %d, span %d, visualWidth %d", colIndex, span, visualWidth)
} else {
visualWidth = ctx.Row.Widths.Get(colIndex)
m.logger.Debugf("renderMarkdownLine: Regular col %d, visualWidth %d", colIndex, visualWidth)
}
if visualWidth < 0 {
visualWidth = 0
}
// Render segment
if isContinuation {
m.logger.Debugf("renderMarkdownLine: Skipping col %d (HMerge continuation)", colIndex)
} else {
var formattedSegment string
if isHeaderSep {
// Use header's alignment from ctx.Row.Previous
headerAlign := tw.AlignCenter // Default for headers
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK {
headerAlign = headerCellCtx.Align
if headerAlign == tw.AlignNone || headerAlign == tw.Empty {
headerAlign = tw.AlignCenter
}
}
formattedSegment = m.formatSeparator(visualWidth, headerAlign)
} else {
content := tw.Empty
if colIndex < len(line) {
content = line[colIndex]
}
// For rows, use the header's alignment if specified
rowAlign := align
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false {
if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty {
rowAlign = headerCellCtx.Align
}
}
formattedSegment = m.formatCell(content, visualWidth, rowAlign, cellCtx.Padding)
}
output.WriteString(formattedSegment)
m.logger.Debugf("renderMarkdownLine: Wrote col %d (span %d, width %d): '%s'", colIndex, span, visualWidth, formattedSegment)
}
colIndex += span
}
output.WriteString(suffix)
output.WriteString(tw.NewLine)
m.w.Write([]byte(output.String()))
m.logger.Debugf("renderMarkdownLine: Final line: %s", strings.TrimSuffix(output.String(), tw.NewLine))
}

View File

@@ -0,0 +1,505 @@
package renderer
import (
"io"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
)
// OceanConfig defines configuration specific to the Ocean renderer.
type OceanConfig struct {
}
// Ocean is a streaming table renderer that writes ASCII tables.
type Ocean struct {
config tw.Rendition
oceanConfig OceanConfig
fixedWidths tw.Mapper[int, int]
widthsFinalized bool
tableOutputStarted bool
headerContentRendered bool // True if actual header *content* has been rendered by Ocean.Header
logger *ll.Logger
w io.Writer
}
func NewOcean(oceanConfig ...OceanConfig) *Ocean {
cfg := defaultOceanRendererConfig()
oCfg := OceanConfig{}
if len(oceanConfig) > 0 {
// Apply user-provided OceanConfig if necessary
}
r := &Ocean{
config: cfg,
oceanConfig: oCfg,
fixedWidths: tw.NewMapper[int, int](),
logger: ll.New("ocean"),
}
r.resetState()
return r
}
func (o *Ocean) resetState() {
o.fixedWidths = tw.NewMapper[int, int]()
o.widthsFinalized = false
o.tableOutputStarted = false
o.headerContentRendered = false
o.logger.Debug("State reset.")
}
func (o *Ocean) Logger(logger *ll.Logger) {
o.logger = logger.Namespace("ocean")
}
func (o *Ocean) Config() tw.Rendition {
return o.config
}
func (o *Ocean) tryFinalizeWidths(ctx tw.Formatting) {
if o.widthsFinalized {
return
}
if ctx.Row.Widths != nil && ctx.Row.Widths.Len() > 0 {
o.fixedWidths = ctx.Row.Widths.Clone()
o.widthsFinalized = true
o.logger.Debugf("Widths finalized from context: %v", o.fixedWidths)
} else {
o.logger.Warn("Attempted to finalize widths, but no width data in context.")
}
}
func (o *Ocean) Start(w io.Writer) error {
o.w = w
o.logger.Debug("Start() called.")
o.resetState()
// Top border is drawn by the first component (Header or Row) that has widths
// OR by an explicit Line() call from table.go's batch renderer.
return nil
}
// renderTopBorderIfNeeded is called by Header or Row if it's the first to render
// and tableOutputStarted is false.
func (o *Ocean) renderTopBorderIfNeeded(currentPosition tw.Position, ctx tw.Formatting) {
if !o.tableOutputStarted && o.widthsFinalized {
// This renderer's config for Top border
if o.config.Borders.Top.Enabled() && o.config.Settings.Lines.ShowTop.Enabled() {
o.logger.Debugf("Ocean itself rendering top border (triggered by %s)", currentPosition)
lineCtx := tw.Formatting{ // Construct specific context for this line
Row: tw.RowContext{
Widths: o.fixedWidths,
Location: tw.LocationFirst,
Position: currentPosition,
Next: ctx.Row.Current, // The actual first content is "Next" to the top border
},
Level: tw.LevelHeader,
}
o.Line(lineCtx)
o.tableOutputStarted = true
}
}
}
func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) {
o.logger.Debugf("Ocean.Header called: IsSubRow=%v, Location=%v, NumLines=%d", ctx.IsSubRow, ctx.Row.Location, len(headers))
if !o.widthsFinalized {
o.tryFinalizeWidths(ctx)
}
// The batch renderer (table.go/renderHeader) will call Line() for the top border
// and for the header separator if its main config t.config says so.
// So, Ocean.Header should *not* draw these itself when in batch mode.
// For true streaming, table.go's streamRenderHeader would make these Line() calls.
// Decision: Ocean.Header *only* renders header content.
// Lines (top border, header separator) are managed by the caller (batch or stream logic in table.go).
if !o.widthsFinalized {
o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.")
// o.headerContentRendered = true; // No, content wasn't rendered.
return
}
if len(headers) > 0 && len(headers[0]) > 0 {
for i, headerLineData := range headers {
currentLineCtx := ctx
currentLineCtx.Row.Widths = o.fixedWidths
if i > 0 {
currentLineCtx.IsSubRow = true
}
o.renderContentLine(currentLineCtx, headerLineData)
o.tableOutputStarted = true // Content was written
}
o.headerContentRendered = true
} else {
o.logger.Debug("Ocean.Header: No actual header content lines to render.")
// If header is empty, table.go's renderHeader might still call Line() for the separator.
// o.headerContentRendered remains false if no content.
}
// DO NOT draw the header separator line here. Let table.go's renderHeader or streamRenderHeader call o.Line().
}
func (o *Ocean) Row(row []string, ctx tw.Formatting) {
o.logger.Debugf("Ocean.Row called: IsSubRow=%v, Location=%v, DataItems=%d", ctx.IsSubRow, ctx.Row.Location, len(row))
if !o.widthsFinalized {
o.tryFinalizeWidths(ctx)
}
// Top border / header separator logic:
// If this is the very first output, table.go's batch renderHeader (or streamRenderHeader)
// should have already called Line() for top border and header separator.
// If Header() was called but rendered no content, table.go's renderHeader would still call Line() for the separator.
// If Header() was never called by table.go (e.g. streaming rows directly after Start()),
// then table.go's streamAppendRow needs to handle initial lines.
// Decision: Ocean.Row *only* renders row content.
if !o.widthsFinalized {
o.logger.Error("Ocean.Row: Cannot render content, widths are not finalized.")
return
}
ctx.Row.Widths = o.fixedWidths
o.renderContentLine(ctx, row)
o.tableOutputStarted = true
}
func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) {
o.logger.Debugf("Ocean.Footer called: IsSubRow=%v, Location=%v, NumLines=%d", ctx.IsSubRow, ctx.Row.Location, len(footers))
if !o.widthsFinalized {
o.tryFinalizeWidths(ctx)
o.logger.Warn("Ocean.Footer: Widths finalized at Footer stage (unusual).")
}
// Separator line before footer:
// This should be handled by table.go's renderFooter or streamRenderFooter calling o.Line().
// Decision: Ocean.Footer *only* renders footer content.
if !o.widthsFinalized {
o.logger.Error("Ocean.Footer: Cannot render content, widths are not finalized.")
return
}
if len(footers) > 0 && len(footers[0]) > 0 {
for i, footerLineData := range footers {
currentLineCtx := ctx
currentLineCtx.Row.Widths = o.fixedWidths
if i > 0 {
currentLineCtx.IsSubRow = true
}
o.renderContentLine(currentLineCtx, footerLineData)
o.tableOutputStarted = true
}
} else {
o.logger.Debug("Ocean.Footer: No actual footer content lines to render.")
}
// DO NOT draw the bottom border here. Let table.go's main Close or batch renderFooter call o.Line().
}
func (o *Ocean) Line(ctx tw.Formatting) {
// This method is now called EXTERNALLY by table.go's batch or stream logic
// to draw all horizontal lines (top border, header sep, footer sep, bottom border).
if !o.widthsFinalized {
// If Line is called before widths are known (e.g. table.go's batch renderHeader trying to draw top border)
// we must try to finalize widths from this context.
o.tryFinalizeWidths(ctx)
if !o.widthsFinalized {
o.logger.Error("Ocean.Line: Called but widths could not be finalized. Skipping line rendering.")
return
}
}
// Ensure Line uses the consistent fixedWidths for drawing
ctx.Row.Widths = o.fixedWidths
o.logger.Debugf("Ocean.Line DRAWING: Level=%v, Loc=%s, Pos=%s, IsSubRow=%t, WidthsLen=%d", ctx.Level, ctx.Row.Location, ctx.Row.Position, ctx.IsSubRow, ctx.Row.Widths.Len())
jr := NewJunction(JunctionContext{
Symbols: o.config.Symbols,
Ctx: ctx,
ColIdx: 0,
Logger: o.logger,
BorderTint: Tint{},
SeparatorTint: Tint{},
})
var line strings.Builder
sortedColIndices := o.fixedWidths.SortedKeys()
if len(sortedColIndices) == 0 {
drewEmptyBorders := false
if o.config.Borders.Left.Enabled() {
line.WriteString(jr.RenderLeft())
drewEmptyBorders = true
}
if o.config.Borders.Right.Enabled() {
line.WriteString(jr.RenderRight(-1))
drewEmptyBorders = true
}
if drewEmptyBorders {
line.WriteString(tw.NewLine)
o.w.Write([]byte(line.String()))
o.logger.Debug("Line: Drew empty table borders based on Line call.")
} else {
o.logger.Debug("Line: Handled empty table case (no columns, no borders).")
}
o.tableOutputStarted = drewEmptyBorders // A line counts as output
return
}
if o.config.Borders.Left.Enabled() {
line.WriteString(jr.RenderLeft())
}
for i, colIdx := range sortedColIndices {
jr.colIdx = colIdx
segmentChar := jr.GetSegment()
colVisualWidth := o.fixedWidths.Get(colIdx)
if colVisualWidth <= 0 {
// Still need to consider separators after zero-width columns
} else {
if segmentChar == tw.Empty {
segmentChar = o.config.Symbols.Row()
}
segmentDisplayWidth := tw.DisplayWidth(segmentChar)
if segmentDisplayWidth <= 0 {
segmentDisplayWidth = 1
}
repeatCount := 0
if colVisualWidth > 0 {
repeatCount = colVisualWidth / segmentDisplayWidth
if repeatCount == 0 {
repeatCount = 1
}
}
line.WriteString(strings.Repeat(segmentChar, repeatCount))
}
if i < len(sortedColIndices)-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
nextColIdx := sortedColIndices[i+1]
line.WriteString(jr.RenderJunction(colIdx, nextColIdx))
}
}
if o.config.Borders.Right.Enabled() {
lastColIdx := sortedColIndices[len(sortedColIndices)-1]
line.WriteString(jr.RenderRight(lastColIdx))
}
line.WriteString(tw.NewLine)
o.w.Write([]byte(line.String()))
o.tableOutputStarted = true
o.logger.Debugf("Line rendered by explicit call: %s", strings.TrimSuffix(line.String(), tw.NewLine))
}
func (o *Ocean) Close() error {
o.logger.Debug("Ocean.Close() called.")
// The actual bottom border drawing is expected to be handled by table.go's
// batch render logic (renderFooter) or stream logic (streamRenderBottomBorder)
// by making an explicit call to o.Line() with the correct context.
// Ocean.Close() itself does not draw the bottom border to avoid duplication.
// Only reset state.
o.resetState()
return nil
}
func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) {
if !o.widthsFinalized || o.fixedWidths.Len() == 0 {
o.logger.Error("renderContentLine: Cannot render, fixedWidths not set or empty.")
return
}
var output strings.Builder
if o.config.Borders.Left.Enabled() {
output.WriteString(o.config.Symbols.Column())
}
sortedColIndices := o.fixedWidths.SortedKeys()
for i, colIdx := range sortedColIndices {
cellVisualWidth := o.fixedWidths.Get(colIdx)
cellContent := tw.Empty
align := tw.AlignDefault
padding := tw.Padding{Left: tw.Space, Right: tw.Space}
switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter
case tw.Footer:
align = tw.AlignRight
default:
align = tw.AlignLeft
}
cellCtx, hasCellCtx := ctx.Row.Current[colIdx]
if hasCellCtx {
cellContent = cellCtx.Data
if cellCtx.Align.Validate() == nil && cellCtx.Align != tw.AlignNone {
align = cellCtx.Align
}
if cellCtx.Padding != (tw.Padding{}) {
padding = cellCtx.Padding
}
} else if colIdx < len(lineData) {
cellContent = lineData[colIdx]
}
actualCellWidthToRender := cellVisualWidth
isHMergeContinuation := false
if hasCellCtx && cellCtx.Merge.Horizontal.Present {
if cellCtx.Merge.Horizontal.Start {
hSpan := cellCtx.Merge.Horizontal.Span
if hSpan <= 0 {
hSpan = 1
}
currentMergeTotalRenderWidth := 0
for k := 0; k < hSpan; k++ {
idxInMergeSpan := colIdx + k
// Check if idxInMergeSpan is a defined column in fixedWidths
foundInFixedWidths := false
for _, sortedCIdx_inner := range sortedColIndices {
if sortedCIdx_inner == idxInMergeSpan {
currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan)
foundInFixedWidths = true
break
}
}
if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] {
o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan)
}
if k < hSpan-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
currentMergeTotalRenderWidth += tw.DisplayWidth(o.config.Symbols.Column())
}
}
actualCellWidthToRender = currentMergeTotalRenderWidth
} else {
isHMergeContinuation = true
}
}
if isHMergeContinuation {
o.logger.Debugf("renderContentLine: Col %d is HMerge continuation, skipping content render.", colIdx)
// The separator logic below needs to handle this correctly.
// If the *previous* column was the start of a merge that spans *this* column,
// then the separator after the previous column should have been suppressed.
} else if actualCellWidthToRender > 0 {
formattedCell := o.formatCellContent(cellContent, actualCellWidthToRender, padding, align)
output.WriteString(formattedCell)
} else {
o.logger.Debugf("renderContentLine: col %d has 0 render width, writing no content.", colIdx)
}
// Add column separator if:
// 1. It's not the last column in sortedColIndices
// 2. Separators are enabled
// 3. This cell is NOT a horizontal merge start that spans over the next column.
if i < len(sortedColIndices)-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
shouldAddSeparator := true
if hasCellCtx && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
// If this merge start spans beyond the current colIdx into the next sortedColIndex
if colIdx+cellCtx.Merge.Horizontal.Span > sortedColIndices[i+1] {
shouldAddSeparator = false // Separator is part of the merged cell's width
o.logger.Debugf("renderContentLine: Suppressed separator after HMerge col %d.", colIdx)
}
}
if shouldAddSeparator {
output.WriteString(o.config.Symbols.Column())
}
}
}
if o.config.Borders.Right.Enabled() {
output.WriteString(o.config.Symbols.Column())
}
output.WriteString(tw.NewLine)
o.w.Write([]byte(output.String()))
o.logger.Debugf("Content line rendered: %s", strings.TrimSuffix(output.String(), tw.NewLine))
}
func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding tw.Padding, align tw.Align) string {
if cellVisualWidth <= 0 {
return tw.Empty
}
contentDisplayWidth := tw.DisplayWidth(content)
padLeftChar := padding.Left
if padLeftChar == tw.Empty {
padLeftChar = tw.Space
}
padRightChar := padding.Right
if padRightChar == tw.Empty {
padRightChar = tw.Space
}
padLeftDisplayWidth := tw.DisplayWidth(padLeftChar)
padRightDisplayWidth := tw.DisplayWidth(padRightChar)
spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth
if spaceForContentAndAlignment < 0 {
spaceForContentAndAlignment = 0
}
if contentDisplayWidth > spaceForContentAndAlignment {
content = tw.TruncateString(content, spaceForContentAndAlignment)
contentDisplayWidth = tw.DisplayWidth(content)
}
remainingSpace := spaceForContentAndAlignment - contentDisplayWidth
if remainingSpace < 0 {
remainingSpace = 0
}
var PL, PR string
switch align {
case tw.AlignRight:
PL = strings.Repeat(tw.Space, remainingSpace)
case tw.AlignCenter:
leftSpaces := remainingSpace / 2
rightSpaces := remainingSpace - leftSpaces
PL = strings.Repeat(tw.Space, leftSpaces)
PR = strings.Repeat(tw.Space, rightSpaces)
default:
PR = strings.Repeat(tw.Space, remainingSpace)
}
var sb strings.Builder
sb.WriteString(padLeftChar)
sb.WriteString(PL)
sb.WriteString(content)
sb.WriteString(PR)
sb.WriteString(padRightChar)
currentFormattedWidth := tw.DisplayWidth(sb.String())
if currentFormattedWidth < cellVisualWidth {
if align == tw.AlignRight {
prefixSpaces := strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth)
finalStr := prefixSpaces + sb.String()
sb.Reset()
sb.WriteString(finalStr)
} else {
sb.WriteString(strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth))
}
} else if currentFormattedWidth > cellVisualWidth {
tempStr := sb.String()
sb.Reset()
sb.WriteString(tw.TruncateString(tempStr, cellVisualWidth))
o.logger.Warnf("formatCellContent: Final string '%s' (width %d) exceeded target %d. Force truncated.", tempStr, currentFormattedWidth, cellVisualWidth)
}
return sb.String()
}
func (o *Ocean) Rendition(config tw.Rendition) {
o.config = mergeRendition(o.config, config)
o.logger.Debugf("Blueprint.Rendition updated. New internal config: %+v", o.config)
}
// Ensure Blueprint implements tw.Renditioning
var _ tw.Renditioning = (*Ocean)(nil)

View File

@@ -0,0 +1,702 @@
package renderer
import (
"fmt"
"github.com/olekukonko/ll"
"html"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw"
)
// SVGConfig holds configuration for the SVG renderer.
// Fields include font, colors, padding, and merge rendering options.
// Used to customize SVG output appearance and behavior.
type SVGConfig struct {
FontFamily string // e.g., "Arial, sans-serif"
FontSize float64 // Base font size in SVG units
LineHeightFactor float64 // Factor for line height (e.g., 1.2)
Padding float64 // Padding inside cells
StrokeWidth float64 // Line width for borders
StrokeColor string // Color for strokes (e.g., "black")
HeaderBG string // Background color for header
RowBG string // Background color for rows
RowAltBG string // Alternating row background color
FooterBG string // Background color for footer
HeaderColor string // Text color for header
RowColor string // Text color for rows
FooterColor string // Text color for footer
ApproxCharWidthFactor float64 // Char width relative to FontSize
MinColWidth float64 // Minimum column width
RenderTWConfigOverrides bool // Override SVG alignments with tablewriter
Debug bool // Enable debug logging
ScaleFactor float64 // Scaling factor for SVG
}
// SVG implements tw.Renderer for SVG output.
// Manages SVG element generation and merge tracking.
type SVG struct {
config SVGConfig
trace []string
allVisualLineData [][][]string // [section][line][cell]
allVisualLineCtx [][]tw.Formatting // [section][line]Formatting
maxCols int
calculatedColWidths []float64
svgElements strings.Builder
currentY float64
dataRowCounter int
vMergeTrack map[int]int // Tracks vertical merge spans
numVisualRowsDrawn int
logger *ll.Logger
w io.Writer
}
const (
sectionTypeHeader = 0
sectionTypeRow = 1
sectionTypeFooter = 2
)
// NewSVG creates a new SVG renderer with configuration.
// Parameter configs provides optional SVGConfig; defaults used if empty.
// Returns a configured SVG instance.
func NewSVG(configs ...SVGConfig) *SVG {
cfg := SVGConfig{
FontFamily: "sans-serif",
FontSize: 12.0,
LineHeightFactor: 1.4,
Padding: 5.0,
StrokeWidth: 1.0,
StrokeColor: "black",
HeaderBG: "#F0F0F0",
RowBG: "white",
RowAltBG: "#F9F9F9",
FooterBG: "#F0F0F0",
HeaderColor: "black",
RowColor: "black",
FooterColor: "black",
ApproxCharWidthFactor: 0.6,
MinColWidth: 30.0,
ScaleFactor: 1.0,
RenderTWConfigOverrides: true,
Debug: false,
}
if len(configs) > 0 {
userCfg := configs[0]
if userCfg.FontFamily != tw.Empty {
cfg.FontFamily = userCfg.FontFamily
}
if userCfg.FontSize > 0 {
cfg.FontSize = userCfg.FontSize
}
if userCfg.LineHeightFactor > 0 {
cfg.LineHeightFactor = userCfg.LineHeightFactor
}
if userCfg.Padding >= 0 {
cfg.Padding = userCfg.Padding
}
if userCfg.StrokeWidth > 0 {
cfg.StrokeWidth = userCfg.StrokeWidth
}
if userCfg.StrokeColor != tw.Empty {
cfg.StrokeColor = userCfg.StrokeColor
}
if userCfg.HeaderBG != tw.Empty {
cfg.HeaderBG = userCfg.HeaderBG
}
if userCfg.RowBG != tw.Empty {
cfg.RowBG = userCfg.RowBG
}
cfg.RowAltBG = userCfg.RowAltBG
if userCfg.FooterBG != tw.Empty {
cfg.FooterBG = userCfg.FooterBG
}
if userCfg.HeaderColor != tw.Empty {
cfg.HeaderColor = userCfg.HeaderColor
}
if userCfg.RowColor != tw.Empty {
cfg.RowColor = userCfg.RowColor
}
if userCfg.FooterColor != tw.Empty {
cfg.FooterColor = userCfg.FooterColor
}
if userCfg.ApproxCharWidthFactor > 0 {
cfg.ApproxCharWidthFactor = userCfg.ApproxCharWidthFactor
}
if userCfg.MinColWidth >= 0 {
cfg.MinColWidth = userCfg.MinColWidth
}
cfg.RenderTWConfigOverrides = userCfg.RenderTWConfigOverrides
cfg.Debug = userCfg.Debug
}
r := &SVG{
config: cfg,
trace: make([]string, 0, 50),
allVisualLineData: make([][][]string, 3),
allVisualLineCtx: make([][]tw.Formatting, 3),
vMergeTrack: make(map[int]int),
}
for i := 0; i < 3; i++ {
r.allVisualLineData[i] = make([][]string, 0)
r.allVisualLineCtx[i] = make([]tw.Formatting, 0)
}
return r
}
// calculateAllColumnWidths computes column widths based on content and merges.
// Uses content length and merge spans; handles horizontal merges by distributing width.
func (s *SVG) calculateAllColumnWidths() {
s.debug("Calculating column widths")
tempMaxCols := 0
for sectionIdx := 0; sectionIdx < 3; sectionIdx++ {
for lineIdx, lineCtx := range s.allVisualLineCtx[sectionIdx] {
if lineCtx.Row.Current != nil {
visualColCount := 0
for colIdx := 0; colIdx < len(lineCtx.Row.Current); {
cellCtx := lineCtx.Row.Current[colIdx]
if cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
colIdx++ // Skip non-start merged cells
continue
}
visualColCount++
span := 1
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
span = cellCtx.Merge.Horizontal.Span
if span <= 0 {
span = 1
}
}
colIdx += span
}
s.debug("Section %d, line %d: Visual columns = %d", sectionIdx, lineIdx, visualColCount)
if visualColCount > tempMaxCols {
tempMaxCols = visualColCount
}
} else if lineIdx < len(s.allVisualLineData[sectionIdx]) {
if rawDataLen := len(s.allVisualLineData[sectionIdx][lineIdx]); rawDataLen > tempMaxCols {
tempMaxCols = rawDataLen
}
}
}
}
s.maxCols = tempMaxCols
s.debug("Max columns: %d", s.maxCols)
if s.maxCols == 0 {
s.calculatedColWidths = []float64{}
return
}
s.calculatedColWidths = make([]float64, s.maxCols)
for i := range s.calculatedColWidths {
s.calculatedColWidths[i] = s.config.MinColWidth
}
// Structure to track max width for each merge group
type mergeKey struct {
startCol int
span int
}
maxMergeWidths := make(map[mergeKey]float64)
processSectionForWidth := func(sectionIdx int) {
for lineIdx, visualLineData := range s.allVisualLineData[sectionIdx] {
if lineIdx >= len(s.allVisualLineCtx[sectionIdx]) {
s.debug("Warning: Missing context for section %d line %d", sectionIdx, lineIdx)
continue
}
lineCtx := s.allVisualLineCtx[sectionIdx][lineIdx]
currentTableCol := 0
currentVisualCol := 0
for currentVisualCol < len(visualLineData) && currentTableCol < s.maxCols {
cellContent := visualLineData[currentVisualCol]
cellCtx := tw.CellContext{}
if lineCtx.Row.Current != nil {
if c, ok := lineCtx.Row.Current[currentTableCol]; ok {
cellCtx = c
}
}
hSpan := 1
if cellCtx.Merge.Horizontal.Present {
if cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
if hSpan <= 0 {
hSpan = 1
}
} else {
currentTableCol++
continue
}
}
textPixelWidth := s.estimateTextWidth(cellContent)
contentAndPaddingWidth := textPixelWidth + (2 * s.config.Padding)
if hSpan == 1 {
if currentTableCol < len(s.calculatedColWidths) && contentAndPaddingWidth > s.calculatedColWidths[currentTableCol] {
s.calculatedColWidths[currentTableCol] = contentAndPaddingWidth
}
} else {
totalMergedWidth := contentAndPaddingWidth + (float64(hSpan-1) * s.config.Padding * 2)
if totalMergedWidth < s.config.MinColWidth*float64(hSpan) {
totalMergedWidth = s.config.MinColWidth * float64(hSpan)
}
if currentTableCol < len(s.calculatedColWidths) {
key := mergeKey{currentTableCol, hSpan}
if currentWidth, ok := maxMergeWidths[key]; ok {
if totalMergedWidth > currentWidth {
maxMergeWidths[key] = totalMergedWidth
}
} else {
maxMergeWidths[key] = totalMergedWidth
}
s.debug("Horizontal merge at col %d, span %d: Total width %.2f", currentTableCol, hSpan, totalMergedWidth)
}
}
currentTableCol += hSpan
currentVisualCol++
}
}
}
processSectionForWidth(sectionTypeHeader)
processSectionForWidth(sectionTypeRow)
processSectionForWidth(sectionTypeFooter)
// Apply maximum widths for merged cells
for key, width := range maxMergeWidths {
s.calculatedColWidths[key.startCol] = width
for i := 1; i < key.span && (key.startCol+i) < len(s.calculatedColWidths); i++ {
s.calculatedColWidths[key.startCol+i] = 0
}
}
for i := range s.calculatedColWidths {
if s.calculatedColWidths[i] < s.config.MinColWidth && s.calculatedColWidths[i] != 0 {
s.calculatedColWidths[i] = s.config.MinColWidth
}
}
s.debug("Column widths: %v", s.calculatedColWidths)
}
// Close finalizes SVG rendering and writes output.
// Parameter w is the output w.
// Returns an error if writing fails.
func (s *SVG) Close() error {
s.debug("Finalizing SVG output")
s.calculateAllColumnWidths()
s.renderBufferedData()
if s.numVisualRowsDrawn == 0 && s.maxCols == 0 {
fmt.Fprintf(s.w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2f" height="%.2f"></svg>`, s.config.StrokeWidth*2, s.config.StrokeWidth*2)
return nil
}
totalWidth := s.config.StrokeWidth
if len(s.calculatedColWidths) > 0 {
for _, cw := range s.calculatedColWidths {
colWidth := cw
if colWidth <= 0 {
colWidth = s.config.MinColWidth
}
totalWidth += colWidth + s.config.StrokeWidth
}
} else if s.maxCols > 0 {
for i := 0; i < s.maxCols; i++ {
totalWidth += s.config.MinColWidth + s.config.StrokeWidth
}
} else {
totalWidth = s.config.StrokeWidth * 2
}
totalHeight := s.currentY
singleVisualRowHeight := s.config.FontSize*s.config.LineHeightFactor + (2 * s.config.Padding)
if s.numVisualRowsDrawn == 0 {
if s.maxCols > 0 {
totalHeight = s.config.StrokeWidth + singleVisualRowHeight + s.config.StrokeWidth
} else {
totalHeight = s.config.StrokeWidth * 2
}
}
fmt.Fprintf(s.w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2f" height="%.2f" font-family="%s" font-size="%.2f">`,
totalWidth, totalHeight, html.EscapeString(s.config.FontFamily), s.config.FontSize)
fmt.Fprintln(s.w)
fmt.Fprintln(s.w, "<style>text { stroke: none; }</style>")
if _, err := io.WriteString(s.w, s.svgElements.String()); err != nil {
fmt.Fprintln(s.w, `</svg>`)
return fmt.Errorf("failed to write SVG elements: %w", err)
}
if s.maxCols > 0 || s.numVisualRowsDrawn > 0 {
fmt.Fprintf(s.w, ` <g class="table-borders" stroke="%s" stroke-width="%.2f" stroke-linecap="square">`,
html.EscapeString(s.config.StrokeColor), s.config.StrokeWidth)
fmt.Fprintln(s.w)
yPos := s.config.StrokeWidth / 2.0
borderRowsToDraw := s.numVisualRowsDrawn
if borderRowsToDraw == 0 && s.maxCols > 0 {
borderRowsToDraw = 1
}
lineStartX := s.config.StrokeWidth / 2.0
lineEndX := s.config.StrokeWidth / 2.0
for _, width := range s.calculatedColWidths {
lineEndX += width + s.config.StrokeWidth
}
for i := 0; i <= borderRowsToDraw; i++ {
fmt.Fprintf(s.w, ` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" />%s`,
lineStartX, yPos, lineEndX, yPos, "\n")
if i < borderRowsToDraw {
yPos += singleVisualRowHeight + s.config.StrokeWidth
}
}
xPos := s.config.StrokeWidth / 2.0
borderLineStartY := s.config.StrokeWidth / 2.0
borderLineEndY := totalHeight - (s.config.StrokeWidth / 2.0)
for visualColIdx := 0; visualColIdx <= s.maxCols; visualColIdx++ {
fmt.Fprintf(s.w, ` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" />%s`,
xPos, borderLineStartY, xPos, borderLineEndY, "\n")
if visualColIdx < s.maxCols {
colWidth := s.config.MinColWidth
if visualColIdx < len(s.calculatedColWidths) && s.calculatedColWidths[visualColIdx] > 0 {
colWidth = s.calculatedColWidths[visualColIdx]
}
xPos += colWidth + s.config.StrokeWidth
}
}
fmt.Fprintln(s.w, " </g>")
}
fmt.Fprintln(s.w, `</svg>`)
return nil
}
// Config returns the renderer's configuration.
// No parameters are required.
// Returns a Rendition with border and debug settings.
func (s *SVG) Config() tw.Rendition {
return tw.Rendition{
Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On},
Settings: tw.Settings{},
Streaming: false,
}
}
// Debug returns the renderer's debug trace.
// No parameters are required.
// Returns a slice of debug messages.
func (s *SVG) Debug() []string {
return s.trace
}
// estimateTextWidth estimates text width in SVG units.
// Parameter text is the input string to measure.
// Returns the estimated width based on font size and char factor.
func (s *SVG) estimateTextWidth(text string) float64 {
runeCount := float64(len([]rune(text)))
return runeCount * s.config.FontSize * s.config.ApproxCharWidthFactor
}
// Footer buffers footer lines for SVG rendering.
// Parameters include w (w), footers (lines), and ctx (formatting).
// No return value; stores data for later rendering.
func (s *SVG) Footer(footers [][]string, ctx tw.Formatting) {
s.debug("Buffering %d footer lines", len(footers))
for i, line := range footers {
currentCtx := ctx
currentCtx.IsSubRow = (i > 0)
s.storeVisualLine(sectionTypeFooter, line, currentCtx)
}
}
// getSVGAnchorFromTW maps tablewriter alignment to SVG text-anchor.
// Parameter align is the tablewriter alignment setting.
// Returns the corresponding SVG text-anchor value or empty string.
func (s *SVG) getSVGAnchorFromTW(align tw.Align) string {
switch align {
case tw.AlignLeft:
return "start"
case tw.AlignCenter:
return "middle"
case tw.AlignRight:
return "end"
case tw.AlignNone, tw.Skip:
return tw.Empty
}
return tw.Empty
}
// Header buffers header lines for SVG rendering.
// Parameters include w (w), headers (lines), and ctx (formatting).
// No return value; stores data for later rendering.
func (s *SVG) Header(headers [][]string, ctx tw.Formatting) {
s.debug("Buffering %d header lines", len(headers))
for i, line := range headers {
currentCtx := ctx
currentCtx.IsSubRow = i > 0
s.storeVisualLine(sectionTypeHeader, line, currentCtx)
}
}
// Line handles border rendering (ignored in SVG renderer).
// Parameters include w (w) and ctx (formatting).
// No return value; SVG borders are drawn in Close.
func (s *SVG) Line(ctx tw.Formatting) {
s.debug("Line rendering ignored")
}
// padLineSVG pads a line to the specified column count.
// Parameters include line (input strings) and numCols (target length).
// Returns the padded line with empty strings as needed.
func padLineSVG(line []string, numCols int) []string {
if numCols <= 0 {
return []string{}
}
currentLen := len(line)
if currentLen == numCols {
return line
}
if currentLen > numCols {
return line[:numCols]
}
padded := make([]string, numCols)
copy(padded, line)
return padded
}
// renderBufferedData renders all buffered lines to SVG elements.
// No parameters are required.
// No return value; populates svgElements buffer.
func (s *SVG) renderBufferedData() {
s.debug("Rendering buffered data")
s.currentY = s.config.StrokeWidth
s.dataRowCounter = 0
s.vMergeTrack = make(map[int]int)
s.numVisualRowsDrawn = 0
renderSection := func(sectionIdx int, position tw.Position) {
for visualLineIdx, visualLineData := range s.allVisualLineData[sectionIdx] {
if visualLineIdx >= len(s.allVisualLineCtx[sectionIdx]) {
s.debug("Error: Missing context for section %d line %d", sectionIdx, visualLineIdx)
continue
}
s.renderVisualLine(visualLineData, s.allVisualLineCtx[sectionIdx][visualLineIdx], position)
}
}
renderSection(sectionTypeHeader, tw.Header)
renderSection(sectionTypeRow, tw.Row)
renderSection(sectionTypeFooter, tw.Footer)
}
// renderVisualLine renders a single visual line as SVG elements.
// Parameters include lineData (cell content), ctx (formatting), and position (section type).
// No return value; handles horizontal and vertical merges.
func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, position tw.Position) {
if s.maxCols == 0 || len(s.calculatedColWidths) == 0 {
s.debug("Skipping line rendering: maxCols=%d, widths=%d", s.maxCols, len(s.calculatedColWidths))
return
}
s.numVisualRowsDrawn++
s.debug("Rendering visual row %d", s.numVisualRowsDrawn)
singleVisualRowHeight := s.config.FontSize*s.config.LineHeightFactor + (2 * s.config.Padding)
bgColor := tw.Empty
textColor := tw.Empty
defaultTextAnchor := "start"
switch position {
case tw.Header:
bgColor = s.config.HeaderBG
textColor = s.config.HeaderColor
defaultTextAnchor = "middle"
case tw.Footer:
bgColor = s.config.FooterBG
textColor = s.config.FooterColor
defaultTextAnchor = "end"
default:
textColor = s.config.RowColor
if !ctx.IsSubRow {
if s.config.RowAltBG != tw.Empty && s.dataRowCounter%2 != 0 {
bgColor = s.config.RowAltBG
} else {
bgColor = s.config.RowBG
}
s.dataRowCounter++
} else {
parentDataRowStripeIndex := s.dataRowCounter - 1
if parentDataRowStripeIndex < 0 {
parentDataRowStripeIndex = 0
}
if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 {
bgColor = s.config.RowAltBG
} else {
bgColor = s.config.RowBG
}
}
}
currentX := s.config.StrokeWidth
currentVisualCellIdx := 0
for tableColIdx := 0; tableColIdx < s.maxCols; {
if tableColIdx >= len(s.calculatedColWidths) {
s.debug("Table Col %d out of bounds for widths", tableColIdx)
tableColIdx++
continue
}
if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 {
s.vMergeTrack[tableColIdx]--
if s.vMergeTrack[tableColIdx] <= 1 {
delete(s.vMergeTrack, tableColIdx)
}
currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth
tableColIdx++
continue
}
cellContentFromVisualLine := tw.Empty
if currentVisualCellIdx < len(visualLineData) {
cellContentFromVisualLine = visualLineData[currentVisualCellIdx]
}
cellCtx := tw.CellContext{}
if ctx.Row.Current != nil {
if c, ok := ctx.Row.Current[tableColIdx]; ok {
cellCtx = c
}
}
textToRender := cellContentFromVisualLine
if cellCtx.Data != tw.Empty {
if !((cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start)) {
textToRender = cellCtx.Data
} else {
textToRender = tw.Empty
}
} else if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
textToRender = tw.Empty
}
hSpan := 1
if cellCtx.Merge.Horizontal.Present {
if cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
if hSpan <= 0 {
hSpan = 1
}
} else {
currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth
tableColIdx++
continue
}
}
vSpan := 1
isVSpanStart := false
if cellCtx.Merge.Vertical.Present && cellCtx.Merge.Vertical.Start {
vSpan = cellCtx.Merge.Vertical.Span
isVSpanStart = true
} else if cellCtx.Merge.Hierarchical.Present && cellCtx.Merge.Hierarchical.Start {
vSpan = cellCtx.Merge.Hierarchical.Span
isVSpanStart = true
}
if vSpan <= 0 {
vSpan = 1
}
rectWidth := 0.0
for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ {
if (tableColIdx + hs) < len(s.calculatedColWidths) {
rectWidth += s.calculatedColWidths[tableColIdx+hs]
} else {
rectWidth += s.config.MinColWidth
}
}
if hSpan > 1 {
rectWidth += float64(hSpan-1) * s.config.StrokeWidth
}
if rectWidth <= 0 {
tableColIdx += hSpan
if hSpan > 0 {
currentVisualCellIdx++
}
continue
}
rectHeight := singleVisualRowHeight
if isVSpanStart && vSpan > 1 {
rectHeight = float64(vSpan)*singleVisualRowHeight + float64(vSpan-1)*s.config.StrokeWidth
for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ {
s.vMergeTrack[tableColIdx+hs] = vSpan
}
s.debug("Vertical merge at col %d, span %d, height %.2f", tableColIdx, vSpan, rectHeight)
} else if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 {
rectHeight = singleVisualRowHeight
textToRender = tw.Empty
}
fmt.Fprintf(&s.svgElements, ` <rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s"/>%s`,
currentX, s.currentY, rectWidth, rectHeight, html.EscapeString(bgColor), "\n")
cellTextAnchor := defaultTextAnchor
if s.config.RenderTWConfigOverrides {
if al := s.getSVGAnchorFromTW(cellCtx.Align); al != tw.Empty {
cellTextAnchor = al
}
}
textX := currentX + s.config.Padding
if cellTextAnchor == "middle" {
textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0
} else if cellTextAnchor == "end" {
textX = currentX + rectWidth - s.config.Padding
}
textY := s.currentY + rectHeight/2.0
escapedCell := html.EscapeString(textToRender)
fmt.Fprintf(&s.svgElements, ` <text x="%.2f" y="%.2f" fill="%s" text-anchor="%s" dominant-baseline="middle">%s</text>%s`,
textX, textY, html.EscapeString(textColor), cellTextAnchor, escapedCell, "\n")
currentX += rectWidth + s.config.StrokeWidth
tableColIdx += hSpan
currentVisualCellIdx++
}
s.currentY += singleVisualRowHeight + s.config.StrokeWidth
}
// Reset clears the renderer's internal state.
// No parameters are required.
// No return value; prepares for new rendering.
func (s *SVG) Reset() {
s.debug("Resetting state")
s.trace = make([]string, 0, 50)
for i := 0; i < 3; i++ {
s.allVisualLineData[i] = s.allVisualLineData[i][:0]
s.allVisualLineCtx[i] = s.allVisualLineCtx[i][:0]
}
s.maxCols = 0
s.calculatedColWidths = nil
s.svgElements.Reset()
s.currentY = 0
s.dataRowCounter = 0
s.vMergeTrack = make(map[int]int)
s.numVisualRowsDrawn = 0
}
// Row buffers a row line for SVG rendering.
// Parameters include w (w), rowLine (cells), and ctx (formatting).
// No return value; stores data for later rendering.
func (s *SVG) Row(rowLine []string, ctx tw.Formatting) {
s.debug("Buffering row line, IsSubRow: %v", ctx.IsSubRow)
s.storeVisualLine(sectionTypeRow, rowLine, ctx)
}
func (s *SVG) Logger(logger *ll.Logger) {
s.logger = logger.Namespace("svg")
}
// Start initializes SVG rendering.
// Parameter w is the output w.
// Returns nil; prepares internal state.
func (s *SVG) Start(w io.Writer) error {
s.w = w
s.debug("Starting SVG rendering")
s.Reset()
return nil
}
// debug logs a message if debugging is enabled.
// Parameters include format string and variadic arguments.
// No return value; appends to trace.
func (s *SVG) debug(format string, a ...interface{}) {
if s.config.Debug {
msg := fmt.Sprintf(format, a...)
s.trace = append(s.trace, fmt.Sprintf("[SVG] %s", msg))
}
}
// storeVisualLine stores a visual line for rendering.
// Parameters include sectionIdx, lineData (cells), and ctx (formatting).
// No return value; buffers data and context.
func (s *SVG) storeVisualLine(sectionIdx int, lineData []string, ctx tw.Formatting) {
copiedLineData := make([]string, len(lineData))
copy(copiedLineData, lineData)
s.allVisualLineData[sectionIdx] = append(s.allVisualLineData[sectionIdx], copiedLineData)
s.allVisualLineCtx[sectionIdx] = append(s.allVisualLineCtx[sectionIdx], ctx)
hasCurrent := ctx.Row.Current != nil
s.debug("Stored line in section %d, has context: %v", sectionIdx, hasCurrent)
}

1150
vendor/github.com/olekukonko/tablewriter/stream.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,967 +0,0 @@
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
// Create & Generate text based table
package tablewriter
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
)
const (
MAX_ROW_WIDTH = 30
)
const (
CENTER = "+"
ROW = "-"
COLUMN = "|"
SPACE = " "
NEWLINE = "\n"
)
const (
ALIGN_DEFAULT = iota
ALIGN_CENTER
ALIGN_RIGHT
ALIGN_LEFT
)
var (
decimal = regexp.MustCompile(`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$`)
percent = regexp.MustCompile(`^-?\d+\.?\d*$%$`)
)
type Border struct {
Left bool
Right bool
Top bool
Bottom bool
}
type Table struct {
out io.Writer
rows [][]string
lines [][][]string
cs map[int]int
rs map[int]int
headers [][]string
footers [][]string
caption bool
captionText string
autoFmt bool
autoWrap bool
reflowText bool
mW int
pCenter string
pRow string
pColumn string
tColumn int
tRow int
hAlign int
fAlign int
align int
newLine string
rowLine bool
autoMergeCells bool
columnsToAutoMergeCells map[int]bool
noWhiteSpace bool
tablePadding string
hdrLine bool
borders Border
colSize int
headerParams []string
columnsParams []string
footerParams []string
columnsAlign []int
}
// Start New Table
// Take io.Writer Directly
func NewWriter(writer io.Writer) *Table {
t := &Table{
out: writer,
rows: [][]string{},
lines: [][][]string{},
cs: make(map[int]int),
rs: make(map[int]int),
headers: [][]string{},
footers: [][]string{},
caption: false,
captionText: "Table caption.",
autoFmt: true,
autoWrap: true,
reflowText: true,
mW: MAX_ROW_WIDTH,
pCenter: CENTER,
pRow: ROW,
pColumn: COLUMN,
tColumn: -1,
tRow: -1,
hAlign: ALIGN_DEFAULT,
fAlign: ALIGN_DEFAULT,
align: ALIGN_DEFAULT,
newLine: NEWLINE,
rowLine: false,
hdrLine: true,
borders: Border{Left: true, Right: true, Bottom: true, Top: true},
colSize: -1,
headerParams: []string{},
columnsParams: []string{},
footerParams: []string{},
columnsAlign: []int{}}
return t
}
// Render table output
func (t *Table) Render() {
if t.borders.Top {
t.printLine(true)
}
t.printHeading()
if t.autoMergeCells {
t.printRowsMergeCells()
} else {
t.printRows()
}
if !t.rowLine && t.borders.Bottom {
t.printLine(true)
}
t.printFooter()
if t.caption {
t.printCaption()
}
}
const (
headerRowIdx = -1
footerRowIdx = -2
)
// Set table header
func (t *Table) SetHeader(keys []string) {
t.colSize = len(keys)
for i, v := range keys {
lines := t.parseDimension(v, i, headerRowIdx)
t.headers = append(t.headers, lines)
}
}
// Set table Footer
func (t *Table) SetFooter(keys []string) {
//t.colSize = len(keys)
for i, v := range keys {
lines := t.parseDimension(v, i, footerRowIdx)
t.footers = append(t.footers, lines)
}
}
// Set table Caption
func (t *Table) SetCaption(caption bool, captionText ...string) {
t.caption = caption
if len(captionText) == 1 {
t.captionText = captionText[0]
}
}
// Turn header autoformatting on/off. Default is on (true).
func (t *Table) SetAutoFormatHeaders(auto bool) {
t.autoFmt = auto
}
// Turn automatic multiline text adjustment on/off. Default is on (true).
func (t *Table) SetAutoWrapText(auto bool) {
t.autoWrap = auto
}
// Turn automatic reflowing of multiline text when rewrapping. Default is on (true).
func (t *Table) SetReflowDuringAutoWrap(auto bool) {
t.reflowText = auto
}
// Set the Default column width
func (t *Table) SetColWidth(width int) {
t.mW = width
}
// Set the minimal width for a column
func (t *Table) SetColMinWidth(column int, width int) {
t.cs[column] = width
}
// Set the Column Separator
func (t *Table) SetColumnSeparator(sep string) {
t.pColumn = sep
}
// Set the Row Separator
func (t *Table) SetRowSeparator(sep string) {
t.pRow = sep
}
// Set the center Separator
func (t *Table) SetCenterSeparator(sep string) {
t.pCenter = sep
}
// Set Header Alignment
func (t *Table) SetHeaderAlignment(hAlign int) {
t.hAlign = hAlign
}
// Set Footer Alignment
func (t *Table) SetFooterAlignment(fAlign int) {
t.fAlign = fAlign
}
// Set Table Alignment
func (t *Table) SetAlignment(align int) {
t.align = align
}
// Set No White Space
func (t *Table) SetNoWhiteSpace(allow bool) {
t.noWhiteSpace = allow
}
// Set Table Padding
func (t *Table) SetTablePadding(padding string) {
t.tablePadding = padding
}
func (t *Table) SetColumnAlignment(keys []int) {
for _, v := range keys {
switch v {
case ALIGN_CENTER:
break
case ALIGN_LEFT:
break
case ALIGN_RIGHT:
break
default:
v = ALIGN_DEFAULT
}
t.columnsAlign = append(t.columnsAlign, v)
}
}
// Set New Line
func (t *Table) SetNewLine(nl string) {
t.newLine = nl
}
// Set Header Line
// This would enable / disable a line after the header
func (t *Table) SetHeaderLine(line bool) {
t.hdrLine = line
}
// Set Row Line
// This would enable / disable a line on each row of the table
func (t *Table) SetRowLine(line bool) {
t.rowLine = line
}
// Set Auto Merge Cells
// This would enable / disable the merge of cells with identical values
func (t *Table) SetAutoMergeCells(auto bool) {
t.autoMergeCells = auto
}
// Set Auto Merge Cells By Column Index
// This would enable / disable the merge of cells with identical values for specific columns
// If cols is empty, it is the same as `SetAutoMergeCells(true)`.
func (t *Table) SetAutoMergeCellsByColumnIndex(cols []int) {
t.autoMergeCells = true
if len(cols) > 0 {
m := make(map[int]bool)
for _, col := range cols {
m[col] = true
}
t.columnsToAutoMergeCells = m
}
}
// Set Table Border
// This would enable / disable line around the table
func (t *Table) SetBorder(border bool) {
t.SetBorders(Border{border, border, border, border})
}
func (t *Table) SetBorders(border Border) {
t.borders = border
}
// Append row to table
func (t *Table) Append(row []string) {
rowSize := len(t.headers)
if rowSize > t.colSize {
t.colSize = rowSize
}
n := len(t.lines)
line := [][]string{}
for i, v := range row {
// Detect string width
// Detect String height
// Break strings into words
out := t.parseDimension(v, i, n)
// Append broken words
line = append(line, out)
}
t.lines = append(t.lines, line)
}
// Append row to table with color attributes
func (t *Table) Rich(row []string, colors []Colors) {
rowSize := len(t.headers)
if rowSize > t.colSize {
t.colSize = rowSize
}
n := len(t.lines)
line := [][]string{}
for i, v := range row {
// Detect string width
// Detect String height
// Break strings into words
out := t.parseDimension(v, i, n)
if len(colors) > i {
color := colors[i]
out[0] = format(out[0], color)
}
// Append broken words
line = append(line, out)
}
t.lines = append(t.lines, line)
}
// Allow Support for Bulk Append
// Eliminates repeated for loops
func (t *Table) AppendBulk(rows [][]string) {
for _, row := range rows {
t.Append(row)
}
}
// NumLines to get the number of lines
func (t *Table) NumLines() int {
return len(t.lines)
}
// Clear rows
func (t *Table) ClearRows() {
t.lines = [][][]string{}
}
// Clear footer
func (t *Table) ClearFooter() {
t.footers = [][]string{}
}
// Center based on position and border.
func (t *Table) center(i int) string {
if i == -1 && !t.borders.Left {
return t.pRow
}
if i == len(t.cs)-1 && !t.borders.Right {
return t.pRow
}
return t.pCenter
}
// Print line based on row width
func (t *Table) printLine(nl bool) {
fmt.Fprint(t.out, t.center(-1))
for i := 0; i < len(t.cs); i++ {
v := t.cs[i]
fmt.Fprintf(t.out, "%s%s%s%s",
t.pRow,
strings.Repeat(string(t.pRow), v),
t.pRow,
t.center(i))
}
if nl {
fmt.Fprint(t.out, t.newLine)
}
}
// Print line based on row width with our without cell separator
func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) {
fmt.Fprint(t.out, t.pCenter)
for i := 0; i < len(t.cs); i++ {
v := t.cs[i]
if i > len(displayCellSeparator) || displayCellSeparator[i] {
// Display the cell separator
fmt.Fprintf(t.out, "%s%s%s%s",
t.pRow,
strings.Repeat(string(t.pRow), v),
t.pRow,
t.pCenter)
} else {
// Don't display the cell separator for this cell
fmt.Fprintf(t.out, "%s%s",
strings.Repeat(" ", v+2),
t.pCenter)
}
}
if nl {
fmt.Fprint(t.out, t.newLine)
}
}
// Return the PadRight function if align is left, PadLeft if align is right,
// and Pad by default
func pad(align int) func(string, string, int) string {
padFunc := Pad
switch align {
case ALIGN_LEFT:
padFunc = PadRight
case ALIGN_RIGHT:
padFunc = PadLeft
}
return padFunc
}
// Print heading information
func (t *Table) printHeading() {
// Check if headers is available
if len(t.headers) < 1 {
return
}
// Identify last column
end := len(t.cs) - 1
// Get pad function
padFunc := pad(t.hAlign)
// Checking for ANSI escape sequences for header
is_esc_seq := false
if len(t.headerParams) > 0 {
is_esc_seq = true
}
// Maximum height.
max := t.rs[headerRowIdx]
// Print Heading
for x := 0; x < max; x++ {
// Check if border is set
// Replace with space if not set
if !t.noWhiteSpace {
fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
}
for y := 0; y <= end; y++ {
v := t.cs[y]
h := ""
if y < len(t.headers) && x < len(t.headers[y]) {
h = t.headers[y][x]
}
if t.autoFmt {
h = Title(h)
}
pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn)
if t.noWhiteSpace {
pad = ConditionString((y == end && !t.borders.Left), SPACE, t.tablePadding)
}
if is_esc_seq {
if !t.noWhiteSpace {
fmt.Fprintf(t.out, " %s %s",
format(padFunc(h, SPACE, v),
t.headerParams[y]), pad)
} else {
fmt.Fprintf(t.out, "%s %s",
format(padFunc(h, SPACE, v),
t.headerParams[y]), pad)
}
} else {
if !t.noWhiteSpace {
fmt.Fprintf(t.out, " %s %s",
padFunc(h, SPACE, v),
pad)
} else {
// the spaces between breaks the kube formatting
fmt.Fprintf(t.out, "%s%s",
padFunc(h, SPACE, v),
pad)
}
}
}
// Next line
fmt.Fprint(t.out, t.newLine)
}
if t.hdrLine {
t.printLine(true)
}
}
// Print heading information
func (t *Table) printFooter() {
// Check if headers is available
if len(t.footers) < 1 {
return
}
// Only print line if border is not set
if !t.borders.Bottom {
t.printLine(true)
}
// Identify last column
end := len(t.cs) - 1
// Get pad function
padFunc := pad(t.fAlign)
// Checking for ANSI escape sequences for header
is_esc_seq := false
if len(t.footerParams) > 0 {
is_esc_seq = true
}
// Maximum height.
max := t.rs[footerRowIdx]
// Print Footer
erasePad := make([]bool, len(t.footers))
for x := 0; x < max; x++ {
// Check if border is set
// Replace with space if not set
fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE))
for y := 0; y <= end; y++ {
v := t.cs[y]
f := ""
if y < len(t.footers) && x < len(t.footers[y]) {
f = t.footers[y][x]
}
if t.autoFmt {
f = Title(f)
}
pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn)
if erasePad[y] || (x == 0 && len(f) == 0) {
pad = SPACE
erasePad[y] = true
}
if is_esc_seq {
fmt.Fprintf(t.out, " %s %s",
format(padFunc(f, SPACE, v),
t.footerParams[y]), pad)
} else {
fmt.Fprintf(t.out, " %s %s",
padFunc(f, SPACE, v),
pad)
}
//fmt.Fprintf(t.out, " %s %s",
// padFunc(f, SPACE, v),
// pad)
}
// Next line
fmt.Fprint(t.out, t.newLine)
//t.printLine(true)
}
hasPrinted := false
for i := 0; i <= end; i++ {
v := t.cs[i]
pad := t.pRow
center := t.pCenter
length := len(t.footers[i][0])
if length > 0 {
hasPrinted = true
}
// Set center to be space if length is 0
if length == 0 && !t.borders.Right {
center = SPACE
}
// Print first junction
if i == 0 {
if length > 0 && !t.borders.Left {
center = t.pRow
}
fmt.Fprint(t.out, center)
}
// Pad With space of length is 0
if length == 0 {
pad = SPACE
}
// Ignore left space as it has printed before
if hasPrinted || t.borders.Left {
pad = t.pRow
center = t.pCenter
}
// Change Center end position
if center != SPACE {
if i == end && !t.borders.Right {
center = t.pRow
}
}
// Change Center start position
if center == SPACE {
if i < end && len(t.footers[i+1][0]) != 0 {
if !t.borders.Left {
center = t.pRow
} else {
center = t.pCenter
}
}
}
// Print the footer
fmt.Fprintf(t.out, "%s%s%s%s",
pad,
strings.Repeat(string(pad), v),
pad,
center)
}
fmt.Fprint(t.out, t.newLine)
}
// Print caption text
func (t Table) printCaption() {
width := t.getTableWidth()
paragraph, _ := WrapString(t.captionText, width)
for linecount := 0; linecount < len(paragraph); linecount++ {
fmt.Fprintln(t.out, paragraph[linecount])
}
}
// Calculate the total number of characters in a row
func (t Table) getTableWidth() int {
var chars int
for _, v := range t.cs {
chars += v
}
// Add chars, spaces, seperators to calculate the total width of the table.
// ncols := t.colSize
// spaces := ncols * 2
// seps := ncols + 1
return (chars + (3 * t.colSize) + 2)
}
func (t Table) printRows() {
for i, lines := range t.lines {
t.printRow(lines, i)
}
}
func (t *Table) fillAlignment(num int) {
if len(t.columnsAlign) < num {
t.columnsAlign = make([]int, num)
for i := range t.columnsAlign {
t.columnsAlign[i] = t.align
}
}
}
// Print Row Information
// Adjust column alignment based on type
func (t *Table) printRow(columns [][]string, rowIdx int) {
// Get Maximum Height
max := t.rs[rowIdx]
total := len(columns)
// TODO Fix uneven col size
// if total < t.colSize {
// for n := t.colSize - total; n < t.colSize ; n++ {
// columns = append(columns, []string{SPACE})
// t.cs[n] = t.mW
// }
//}
// Pad Each Height
pads := []int{}
// Checking for ANSI escape sequences for columns
is_esc_seq := false
if len(t.columnsParams) > 0 {
is_esc_seq = true
}
t.fillAlignment(total)
for i, line := range columns {
length := len(line)
pad := max - length
pads = append(pads, pad)
for n := 0; n < pad; n++ {
columns[i] = append(columns[i], " ")
}
}
//fmt.Println(max, "\n")
for x := 0; x < max; x++ {
for y := 0; y < total; y++ {
// Check if border is set
if !t.noWhiteSpace {
fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
fmt.Fprintf(t.out, SPACE)
}
str := columns[y][x]
// Embedding escape sequence with column value
if is_esc_seq {
str = format(str, t.columnsParams[y])
}
// This would print alignment
// Default alignment would use multiple configuration
switch t.columnsAlign[y] {
case ALIGN_CENTER: //
fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
case ALIGN_RIGHT:
fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
case ALIGN_LEFT:
fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
default:
if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
} else {
fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
// TODO Custom alignment per column
//if max == 1 || pads[y] > 0 {
// fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
//} else {
// fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
//}
}
}
if !t.noWhiteSpace {
fmt.Fprintf(t.out, SPACE)
} else {
fmt.Fprintf(t.out, t.tablePadding)
}
}
// Check if border is set
// Replace with space if not set
if !t.noWhiteSpace {
fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
}
fmt.Fprint(t.out, t.newLine)
}
if t.rowLine {
t.printLine(true)
}
}
// Print the rows of the table and merge the cells that are identical
func (t *Table) printRowsMergeCells() {
var previousLine []string
var displayCellBorder []bool
var tmpWriter bytes.Buffer
for i, lines := range t.lines {
// We store the display of the current line in a tmp writer, as we need to know which border needs to be print above
previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine)
if i > 0 { //We don't need to print borders above first line
if t.rowLine {
t.printLineOptionalCellSeparators(true, displayCellBorder)
}
}
tmpWriter.WriteTo(t.out)
}
//Print the end of the table
if t.rowLine {
t.printLine(true)
}
}
// Print Row Information to a writer and merge identical cells.
// Adjust column alignment based on type
func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) {
// Get Maximum Height
max := t.rs[rowIdx]
total := len(columns)
// Pad Each Height
pads := []int{}
// Checking for ANSI escape sequences for columns
is_esc_seq := false
if len(t.columnsParams) > 0 {
is_esc_seq = true
}
for i, line := range columns {
length := len(line)
pad := max - length
pads = append(pads, pad)
for n := 0; n < pad; n++ {
columns[i] = append(columns[i], " ")
}
}
var displayCellBorder []bool
t.fillAlignment(total)
for x := 0; x < max; x++ {
for y := 0; y < total; y++ {
// Check if border is set
fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
fmt.Fprintf(writer, SPACE)
str := columns[y][x]
// Embedding escape sequence with column value
if is_esc_seq {
str = format(str, t.columnsParams[y])
}
if t.autoMergeCells {
var mergeCell bool
if t.columnsToAutoMergeCells != nil {
// Check to see if the column index is in columnsToAutoMergeCells.
if t.columnsToAutoMergeCells[y] {
mergeCell = true
}
} else {
// columnsToAutoMergeCells was not set.
mergeCell = true
}
//Store the full line to merge mutli-lines cells
fullLine := strings.TrimRight(strings.Join(columns[y], " "), " ")
if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" && mergeCell {
// If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty.
displayCellBorder = append(displayCellBorder, false)
str = ""
} else {
// First line or different content, keep the content and print the cell border
displayCellBorder = append(displayCellBorder, true)
}
}
// This would print alignment
// Default alignment would use multiple configuration
switch t.columnsAlign[y] {
case ALIGN_CENTER: //
fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y]))
case ALIGN_RIGHT:
fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
case ALIGN_LEFT:
fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
default:
if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
} else {
fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
}
}
fmt.Fprintf(writer, SPACE)
}
// Check if border is set
// Replace with space if not set
fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE))
fmt.Fprint(writer, t.newLine)
}
//The new previous line is the current one
previousLine = make([]string, total)
for y := 0; y < total; y++ {
previousLine[y] = strings.TrimRight(strings.Join(columns[y], " "), " ") //Store the full line for multi-lines cells
}
//Returns the newly added line and wether or not a border should be displayed above.
return previousLine, displayCellBorder
}
func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
var (
raw []string
maxWidth int
)
raw = getLines(str)
maxWidth = 0
for _, line := range raw {
if w := DisplayWidth(line); w > maxWidth {
maxWidth = w
}
}
// If wrapping, ensure that all paragraphs in the cell fit in the
// specified width.
if t.autoWrap {
// If there's a maximum allowed width for wrapping, use that.
if maxWidth > t.mW {
maxWidth = t.mW
}
// In the process of doing so, we need to recompute maxWidth. This
// is because perhaps a word in the cell is longer than the
// allowed maximum width in t.mW.
newMaxWidth := maxWidth
newRaw := make([]string, 0, len(raw))
if t.reflowText {
// Make a single paragraph of everything.
raw = []string{strings.Join(raw, " ")}
}
for i, para := range raw {
paraLines, _ := WrapString(para, maxWidth)
for _, line := range paraLines {
if w := DisplayWidth(line); w > newMaxWidth {
newMaxWidth = w
}
}
if i > 0 {
newRaw = append(newRaw, " ")
}
newRaw = append(newRaw, paraLines...)
}
raw = newRaw
maxWidth = newMaxWidth
}
// Store the new known maximum width.
v, ok := t.cs[colKey]
if !ok || v < maxWidth || v == 0 {
t.cs[colKey] = maxWidth
}
// Remember the number of lines for the row printer.
h := len(raw)
v, ok = t.rs[rowKey]
if !ok || v < h || v == 0 {
t.rs[rowKey] = h
}
//fmt.Printf("Raw %+v %d\n", raw, len(raw))
return raw
}

View File

@@ -1,136 +0,0 @@
package tablewriter
import (
"fmt"
"strconv"
"strings"
)
const ESC = "\033"
const SEP = ";"
const (
BgBlackColor int = iota + 40
BgRedColor
BgGreenColor
BgYellowColor
BgBlueColor
BgMagentaColor
BgCyanColor
BgWhiteColor
)
const (
FgBlackColor int = iota + 30
FgRedColor
FgGreenColor
FgYellowColor
FgBlueColor
FgMagentaColor
FgCyanColor
FgWhiteColor
)
const (
BgHiBlackColor int = iota + 100
BgHiRedColor
BgHiGreenColor
BgHiYellowColor
BgHiBlueColor
BgHiMagentaColor
BgHiCyanColor
BgHiWhiteColor
)
const (
FgHiBlackColor int = iota + 90
FgHiRedColor
FgHiGreenColor
FgHiYellowColor
FgHiBlueColor
FgHiMagentaColor
FgHiCyanColor
FgHiWhiteColor
)
const (
Normal = 0
Bold = 1
UnderlineSingle = 4
Italic
)
type Colors []int
func startFormat(seq string) string {
return fmt.Sprintf("%s[%sm", ESC, seq)
}
func stopFormat() string {
return fmt.Sprintf("%s[%dm", ESC, Normal)
}
// Making the SGR (Select Graphic Rendition) sequence.
func makeSequence(codes []int) string {
codesInString := []string{}
for _, code := range codes {
codesInString = append(codesInString, strconv.Itoa(code))
}
return strings.Join(codesInString, SEP)
}
// Adding ANSI escape sequences before and after string
func format(s string, codes interface{}) string {
var seq string
switch v := codes.(type) {
case string:
seq = v
case []int:
seq = makeSequence(v)
case Colors:
seq = makeSequence(v)
default:
return s
}
if len(seq) == 0 {
return s
}
return startFormat(seq) + s + stopFormat()
}
// Adding header colors (ANSI codes)
func (t *Table) SetHeaderColor(colors ...Colors) {
if t.colSize != len(colors) {
panic("Number of header colors must be equal to number of headers.")
}
for i := 0; i < len(colors); i++ {
t.headerParams = append(t.headerParams, makeSequence(colors[i]))
}
}
// Adding column colors (ANSI codes)
func (t *Table) SetColumnColor(colors ...Colors) {
if t.colSize != len(colors) {
panic("Number of column colors must be equal to number of headers.")
}
for i := 0; i < len(colors); i++ {
t.columnsParams = append(t.columnsParams, makeSequence(colors[i]))
}
}
// Adding column colors (ANSI codes)
func (t *Table) SetFooterColor(colors ...Colors) {
if len(t.footers) != len(colors) {
panic("Number of footer colors must be equal to number of footer.")
}
for i := 0; i < len(colors); i++ {
t.footerParams = append(t.footerParams, makeSequence(colors[i]))
}
}
func Color(colors ...int) []int {
return colors
}

2140
vendor/github.com/olekukonko/tablewriter/tablewriter.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

46
vendor/github.com/olekukonko/tablewriter/tw/cell.go generated vendored Normal file
View File

@@ -0,0 +1,46 @@
package tw
// CellFormatting holds formatting options for table cells.
type CellFormatting struct {
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
AutoWrap int // Wrapping behavior (e.g., WrapTruncate, WrapNormal)
MergeMode int // Bitmask for merge behavior (e.g., MergeHorizontal, MergeVertical)
// Changed form bool to State
// See https://github.com/olekukonko/tablewriter/issues/261
AutoFormat State // Enables automatic formatting (e.g., title case for headers)
}
// CellPadding defines padding settings for table cells.
type CellPadding struct {
Global Padding // Default padding applied to all cells
PerColumn []Padding // Column-specific padding overrides
}
// CellFilter defines filtering functions for cell content.
type CellFilter struct {
Global func([]string) []string // Processes the entire row
PerColumn []func(string) string // Processes individual cells by column
}
// CellCallbacks holds callback functions for cell processing.
// Note: These are currently placeholders and not fully implemented.
type CellCallbacks struct {
Global func() // Global callback applied to all cells
PerColumn []func() // Column-specific callbacks
}
// CellConfig combines formatting, padding, and callback settings for a table section.
type CellConfig struct {
Formatting CellFormatting // Cell formatting options
Padding CellPadding // Padding configuration
Callbacks CellCallbacks // Callback functions (unused)
Filter CellFilter // Function to filter cell content (renamed from Filter Filter)
ColumnAligns []Align // Per-column alignment overrides
ColMaxWidths CellWidth // Per-column maximum width overrides
}
type CellWidth struct {
Global int
PerColumn Mapper[int, int]
}

View File

@@ -0,0 +1,137 @@
package tw
// Deprecated: SymbolASCII is deprecated; use Glyphs with StyleASCII instead.
// this will be removed soon
type SymbolASCII struct{}
// SymbolASCII symbol methods
func (s *SymbolASCII) Name() string { return StyleNameASCII.String() }
func (s *SymbolASCII) Center() string { return "+" }
func (s *SymbolASCII) Row() string { return "-" }
func (s *SymbolASCII) Column() string { return "|" }
func (s *SymbolASCII) TopLeft() string { return "+" }
func (s *SymbolASCII) TopMid() string { return "+" }
func (s *SymbolASCII) TopRight() string { return "+" }
func (s *SymbolASCII) MidLeft() string { return "+" }
func (s *SymbolASCII) MidRight() string { return "+" }
func (s *SymbolASCII) BottomLeft() string { return "+" }
func (s *SymbolASCII) BottomMid() string { return "+" }
func (s *SymbolASCII) BottomRight() string { return "+" }
func (s *SymbolASCII) HeaderLeft() string { return "+" }
func (s *SymbolASCII) HeaderMid() string { return "+" }
func (s *SymbolASCII) HeaderRight() string { return "+" }
// Deprecated: SymbolUnicode is deprecated; use Glyphs with appropriate styles (e.g., StyleLight, StyleHeavy) instead.
// this will be removed soon
type SymbolUnicode struct {
row string
column string
center string
corners [9]string // [topLeft, topMid, topRight, midLeft, center, midRight, bottomLeft, bottomMid, bottomRight]
}
// SymbolUnicode symbol methods
func (s *SymbolUnicode) Name() string { return "unicode" }
func (s *SymbolUnicode) Center() string { return s.center }
func (s *SymbolUnicode) Row() string { return s.row }
func (s *SymbolUnicode) Column() string { return s.column }
func (s *SymbolUnicode) TopLeft() string { return s.corners[0] }
func (s *SymbolUnicode) TopMid() string { return s.corners[1] }
func (s *SymbolUnicode) TopRight() string { return s.corners[2] }
func (s *SymbolUnicode) MidLeft() string { return s.corners[3] }
func (s *SymbolUnicode) MidRight() string { return s.corners[5] }
func (s *SymbolUnicode) BottomLeft() string { return s.corners[6] }
func (s *SymbolUnicode) BottomMid() string { return s.corners[7] }
func (s *SymbolUnicode) BottomRight() string { return s.corners[8] }
func (s *SymbolUnicode) HeaderLeft() string { return s.MidLeft() }
func (s *SymbolUnicode) HeaderMid() string { return s.Center() }
func (s *SymbolUnicode) HeaderRight() string { return s.MidRight() }
// Deprecated: SymbolMarkdown is deprecated; use Glyphs with StyleMarkdown instead.
// this will be removed soon
type SymbolMarkdown struct{}
// SymbolMarkdown symbol methods
func (s *SymbolMarkdown) Name() string { return StyleNameMarkdown.String() }
func (s *SymbolMarkdown) Center() string { return "|" }
func (s *SymbolMarkdown) Row() string { return "-" }
func (s *SymbolMarkdown) Column() string { return "|" }
func (s *SymbolMarkdown) TopLeft() string { return "" }
func (s *SymbolMarkdown) TopMid() string { return "" }
func (s *SymbolMarkdown) TopRight() string { return "" }
func (s *SymbolMarkdown) MidLeft() string { return "|" }
func (s *SymbolMarkdown) MidRight() string { return "|" }
func (s *SymbolMarkdown) BottomLeft() string { return "" }
func (s *SymbolMarkdown) BottomMid() string { return "" }
func (s *SymbolMarkdown) BottomRight() string { return "" }
func (s *SymbolMarkdown) HeaderLeft() string { return "|" }
func (s *SymbolMarkdown) HeaderMid() string { return "|" }
func (s *SymbolMarkdown) HeaderRight() string { return "|" }
// Deprecated: SymbolNothing is deprecated; use Glyphs with StyleNone instead.
// this will be removed soon
type SymbolNothing struct{}
// SymbolNothing symbol methods
func (s *SymbolNothing) Name() string { return StyleNameNothing.String() }
func (s *SymbolNothing) Center() string { return "" }
func (s *SymbolNothing) Row() string { return "" }
func (s *SymbolNothing) Column() string { return "" }
func (s *SymbolNothing) TopLeft() string { return "" }
func (s *SymbolNothing) TopMid() string { return "" }
func (s *SymbolNothing) TopRight() string { return "" }
func (s *SymbolNothing) MidLeft() string { return "" }
func (s *SymbolNothing) MidRight() string { return "" }
func (s *SymbolNothing) BottomLeft() string { return "" }
func (s *SymbolNothing) BottomMid() string { return "" }
func (s *SymbolNothing) BottomRight() string { return "" }
func (s *SymbolNothing) HeaderLeft() string { return "" }
func (s *SymbolNothing) HeaderMid() string { return "" }
func (s *SymbolNothing) HeaderRight() string { return "" }
// Deprecated: SymbolGraphical is deprecated; use Glyphs with StyleGraphical instead.
// this will be removed soon
type SymbolGraphical struct{}
// SymbolGraphical symbol methods
func (s *SymbolGraphical) Name() string { return StyleNameGraphical.String() }
func (s *SymbolGraphical) Center() string { return "🟧" } // Orange square (matches mid junctions)
func (s *SymbolGraphical) Row() string { return "🟥" } // Red square (matches corners)
func (s *SymbolGraphical) Column() string { return "🟦" } // Blue square (vertical line)
func (s *SymbolGraphical) TopLeft() string { return "🟥" } // Top-left corner
func (s *SymbolGraphical) TopMid() string { return "🔳" } // Top junction
func (s *SymbolGraphical) TopRight() string { return "🟥" } // Top-right corner
func (s *SymbolGraphical) MidLeft() string { return "🟧" } // Left junction
func (s *SymbolGraphical) MidRight() string { return "🟧" } // Right junction
func (s *SymbolGraphical) BottomLeft() string { return "🟥" } // Bottom-left corner
func (s *SymbolGraphical) BottomMid() string { return "🔳" } // Bottom junction
func (s *SymbolGraphical) BottomRight() string { return "🟥" } // Bottom-right corner
func (s *SymbolGraphical) HeaderLeft() string { return "🟧" } // Header left (matches mid junctions)
func (s *SymbolGraphical) HeaderMid() string { return "🟧" } // Header middle (matches mid junctions)
func (s *SymbolGraphical) HeaderRight() string { return "🟧" } // Header right (matches mid junctions)
// Deprecated: SymbolMerger is deprecated; use Glyphs with StyleMerger instead.
// this will be removed soon
type SymbolMerger struct {
row string
column string
center string
corners [9]string // [TL, TM, TR, ML, CenterIdx(unused), MR, BL, BM, BR]
}
// SymbolMerger symbol methods
func (s *SymbolMerger) Name() string { return StyleNameMerger.String() }
func (s *SymbolMerger) Center() string { return s.center } // Main crossing symbol
func (s *SymbolMerger) Row() string { return s.row }
func (s *SymbolMerger) Column() string { return s.column }
func (s *SymbolMerger) TopLeft() string { return s.corners[0] }
func (s *SymbolMerger) TopMid() string { return s.corners[1] } // LevelHeader junction
func (s *SymbolMerger) TopRight() string { return s.corners[2] }
func (s *SymbolMerger) MidLeft() string { return s.corners[3] } // Left junction
func (s *SymbolMerger) MidRight() string { return s.corners[5] } // Right junction
func (s *SymbolMerger) BottomLeft() string { return s.corners[6] }
func (s *SymbolMerger) BottomMid() string { return s.corners[7] } // LevelFooter junction
func (s *SymbolMerger) BottomRight() string { return s.corners[8] }
func (s *SymbolMerger) HeaderLeft() string { return s.MidLeft() }
func (s *SymbolMerger) HeaderMid() string { return s.Center() }
func (s *SymbolMerger) HeaderRight() string { return s.MidRight() }

362
vendor/github.com/olekukonko/tablewriter/tw/fn.go generated vendored Normal file
View File

@@ -0,0 +1,362 @@
// Package tw provides utility functions for text formatting, width calculation, and string manipulation
// specifically tailored for table rendering, including handling ANSI escape codes and Unicode text.
package tw
import (
"bytes" // For buffering string output
"github.com/mattn/go-runewidth" // For calculating display width of Unicode characters
"math" // For mathematical operations like ceiling
"regexp" // For regular expression handling of ANSI codes
"strconv" // For string-to-number conversions
"strings" // For string manipulation utilities
"unicode" // For Unicode character classification
"unicode/utf8" // For UTF-8 rune handling
)
// ansi is a compiled regex pattern used to strip ANSI escape codes.
// These codes are used in terminal output for styling and are invisible in rendered text.
var ansi = CompileANSIFilter()
// CompileANSIFilter constructs and compiles a regex for matching ANSI sequences.
// It supports both control sequences (CSI) and operating system commands (OSC) like hyperlinks.
func CompileANSIFilter() *regexp.Regexp {
var regESC = "\x1b" // ASCII escape character
var regBEL = "\x07" // ASCII bell character
// ANSI string terminator: either ESC+\ or BEL
var regST = "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")"
// Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte
var regCSI = regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]"
// Operating System Command (OSC): ESC] followed by arbitrary content until a terminator
var regOSC = regexp.QuoteMeta(regESC+"]") + ".*?" + regST
// Combine CSI and OSC patterns into a single regex
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
}
// DisplayWidth calculates the visual width of a string, excluding ANSI escape sequences.
// It uses go-runewidth to handle Unicode characters correctly.
func DisplayWidth(str string) int {
// Strip ANSI codes before calculating width to avoid counting invisible characters
return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
}
// TruncateString shortens a string to a specified maximum display width while preserving ANSI color codes.
// An optional suffix (e.g., "...") is appended if truncation occurs.
func TruncateString(s string, maxWidth int, suffix ...string) string {
// If maxWidth is 0 or negative, return an empty string
if maxWidth <= 0 {
return ""
}
// Join suffix slices into a single string and calculate its display width
suffixStr := strings.Join(suffix, " ")
suffixDisplayWidth := 0
if len(suffixStr) > 0 {
// Strip ANSI from suffix for accurate width calculation
suffixDisplayWidth = runewidth.StringWidth(ansi.ReplaceAllLiteralString(suffixStr, ""))
}
// Check if the string (without ANSI) plus suffix fits within maxWidth
strippedS := ansi.ReplaceAllLiteralString(s, "")
if runewidth.StringWidth(strippedS)+suffixDisplayWidth <= maxWidth {
// If it fits, return the original string (with ANSI) plus suffix
return s + suffixStr
}
// Handle edge case: maxWidth is too small for even the suffix
if maxWidth < suffixDisplayWidth {
// Try truncating the string without suffix
return TruncateString(s, maxWidth) // Recursive call without suffix
}
// Handle edge case: maxWidth exactly equals suffix width
if maxWidth == suffixDisplayWidth {
if runewidth.StringWidth(strippedS) > 0 {
// If there's content, it's fully truncated; return suffix
return suffixStr
}
return "" // No content and no space for content; return empty string
}
// Calculate the maximum width available for the content (excluding suffix)
targetContentDisplayWidth := maxWidth - suffixDisplayWidth
var contentBuf bytes.Buffer // Buffer for building truncated content
var currentContentDisplayWidth int // Tracks display width of content
var ansiSeqBuf bytes.Buffer // Buffer for collecting ANSI sequences
inAnsiSequence := false // Tracks if we're inside an ANSI sequence
// Iterate over runes to build content while respecting maxWidth
for _, r := range s {
if r == '\x1b' { // Start of ANSI escape sequence
if inAnsiSequence {
// Unexpected new ESC; flush existing sequence
contentBuf.Write(ansiSeqBuf.Bytes())
ansiSeqBuf.Reset()
}
inAnsiSequence = true
ansiSeqBuf.WriteRune(r)
} else if inAnsiSequence {
ansiSeqBuf.WriteRune(r)
// Detect end of common ANSI sequences (e.g., SGR 'm' or CSI terminators)
if r == 'm' || (ansiSeqBuf.Len() > 2 && ansiSeqBuf.Bytes()[1] == '[' && r >= '@' && r <= '~') {
inAnsiSequence = false
contentBuf.Write(ansiSeqBuf.Bytes()) // Append completed sequence
ansiSeqBuf.Reset()
} else if ansiSeqBuf.Len() > 128 { // Prevent buffer overflow for malformed sequences
inAnsiSequence = false
contentBuf.Write(ansiSeqBuf.Bytes())
ansiSeqBuf.Reset()
}
} else {
// Handle displayable characters
runeDisplayWidth := runewidth.RuneWidth(r)
if currentContentDisplayWidth+runeDisplayWidth > targetContentDisplayWidth {
// Adding this rune would exceed the content width; stop here
break
}
contentBuf.WriteRune(r)
currentContentDisplayWidth += runeDisplayWidth
}
}
// Append any unterminated ANSI sequence
if ansiSeqBuf.Len() > 0 {
contentBuf.Write(ansiSeqBuf.Bytes())
}
finalContent := contentBuf.String()
// Append suffix if content was truncated or if suffix is provided and content exists
if runewidth.StringWidth(ansi.ReplaceAllLiteralString(finalContent, "")) < runewidth.StringWidth(strippedS) {
// Content was truncated; append suffix
return finalContent + suffixStr
} else if len(suffixStr) > 0 && len(finalContent) > 0 {
// No truncation but suffix exists; append it
return finalContent + suffixStr
} else if len(suffixStr) > 0 && len(strippedS) == 0 {
// Original string was empty; return suffix
return suffixStr
}
// Return content as is (with preserved ANSI codes)
return finalContent
}
// Title normalizes and uppercases a label string for use in headers.
// It replaces underscores and certain dots with spaces and trims whitespace.
func Title(name string) string {
origLen := len(name)
rs := []rune(name)
for i, r := range rs {
switch r {
case '_':
rs[i] = ' ' // Replace underscores with spaces
case '.':
// Replace dots with spaces unless they are between numeric or space characters
if (i != 0 && !IsIsNumericOrSpace(rs[i-1])) || (i != len(rs)-1 && !IsIsNumericOrSpace(rs[i+1])) {
rs[i] = ' '
}
}
}
name = string(rs)
name = strings.TrimSpace(name)
// If the input was non-empty but trimmed to empty, return a single space
if len(name) == 0 && origLen > 0 {
name = " "
}
// Convert to uppercase for header formatting
return strings.ToUpper(name)
}
// PadCenter centers a string within a specified width using a padding character.
// Extra padding is split between left and right, with slight preference to left if uneven.
func PadCenter(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
// Calculate left and right padding; ceil ensures left gets extra if gap is odd
gapLeft := int(math.Ceil(float64(gap) / 2))
gapRight := gap - gapLeft
return strings.Repeat(pad, gapLeft) + s + strings.Repeat(pad, gapRight)
}
// If no padding needed or string is too wide, return as is
return s
}
// PadRight left-aligns a string within a specified width, filling remaining space on the right with padding.
func PadRight(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
// Append padding to the right
return s + strings.Repeat(pad, gap)
}
// If no padding needed or string is too wide, return as is
return s
}
// PadLeft right-aligns a string within a specified width, filling remaining space on the left with padding.
func PadLeft(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
// Prepend padding to the left
return strings.Repeat(pad, gap) + s
}
// If no padding needed or string is too wide, return as is
return s
}
// Pad aligns a string within a specified width using a padding character.
// It truncates if the string is wider than the target width.
func Pad(s string, padChar string, totalWidth int, alignment Align) string {
sDisplayWidth := DisplayWidth(s)
if sDisplayWidth > totalWidth {
return TruncateString(s, totalWidth) // Only truncate if necessary
}
switch alignment {
case AlignLeft:
return PadRight(s, padChar, totalWidth)
case AlignRight:
return PadLeft(s, padChar, totalWidth)
case AlignCenter:
return PadCenter(s, padChar, totalWidth)
default:
return PadRight(s, padChar, totalWidth)
}
}
// IsIsNumericOrSpace checks if a rune is a digit or space character.
// Used in formatting logic to determine safe character replacements.
func IsIsNumericOrSpace(r rune) bool {
return ('0' <= r && r <= '9') || r == ' '
}
// IsNumeric checks if a string represents a valid integer or floating-point number.
func IsNumeric(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
// Try parsing as integer first
if _, err := strconv.Atoi(s); err == nil {
return true
}
// Then try parsing as float
_, err := strconv.ParseFloat(s, 64)
return err == nil
}
// SplitCamelCase splits a camelCase or PascalCase string into separate words.
// It detects transitions between uppercase, lowercase, digits, and other characters.
func SplitCamelCase(src string) (entries []string) {
// Validate UTF-8 input; return as single entry if invalid
if !utf8.ValidString(src) {
return []string{src}
}
entries = []string{}
var runes [][]rune
lastClass := 0
class := 0
// Classify each rune into categories: lowercase (1), uppercase (2), digit (3), other (4)
for _, r := range src {
switch {
case unicode.IsLower(r):
class = 1
case unicode.IsUpper(r):
class = 2
case unicode.IsDigit(r):
class = 3
default:
class = 4
}
// Group consecutive runes of the same class together
if class == lastClass {
runes[len(runes)-1] = append(runes[len(runes)-1], r)
} else {
runes = append(runes, []rune{r})
}
lastClass = class
}
// Adjust for cases where an uppercase letter is followed by lowercase (e.g., CamelCase)
for i := 0; i < len(runes)-1; i++ {
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
// Move the last uppercase rune to the next group for proper word splitting
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
runes[i] = runes[i][:len(runes[i])-1]
}
}
// Convert rune groups to strings, excluding empty or whitespace-only groups
for _, s := range runes {
if len(s) > 0 && strings.TrimSpace(string(s)) != "" {
entries = append(entries, string(s))
}
}
return
}
// Or provides a ternary-like operation for strings, returning 'valid' if cond is true, else 'inValid'.
func Or(cond bool, valid, inValid string) string {
if cond {
return valid
}
return inValid
}
// Max returns the greater of two integers.
func Max(a, b int) int {
if a > b {
return a
}
return b
}
// Min returns the smaller of two integers.
func Min(a, b int) int {
if a < b {
return a
}
return b
}
// BreakPoint finds the rune index where the display width of a string first exceeds the specified limit.
// It returns the number of runes if the entire string fits, or 0 if nothing fits.
func BreakPoint(s string, limit int) int {
// If limit is 0 or negative, nothing can fit
if limit <= 0 {
return 0
}
// Empty string has a breakpoint of 0
if s == "" {
return 0
}
currentWidth := 0
runeCount := 0
// Iterate over runes, accumulating display width
for _, r := range s {
runeWidth := DisplayWidth(string(r)) // Calculate width of individual rune
if currentWidth+runeWidth > limit {
// Adding this rune would exceed the limit; breakpoint is before this rune
if currentWidth == 0 {
// First rune is too wide; allow breaking after it if limit > 0
if runeWidth > limit && limit > 0 {
return 1
}
return 0
}
return runeCount
}
currentWidth += runeWidth
runeCount++
}
// Entire string fits within the limit
return runeCount
}
func MakeAlign(l int, align Align) Alignment {
aa := make(Alignment, l)
for i := 0; i < l; i++ {
aa[i] = align
}
return aa
}

220
vendor/github.com/olekukonko/tablewriter/tw/mapper.go generated vendored Normal file
View File

@@ -0,0 +1,220 @@
package tw
import (
"fmt"
"sort"
)
// KeyValuePair represents a single key-value pair from a Mapper.
type KeyValuePair[K comparable, V any] struct {
Key K
Value V
}
// Mapper is a generic map type with comparable keys and any value type.
// It provides type-safe operations on maps with additional convenience methods.
type Mapper[K comparable, V any] map[K]V
// NewMapper creates and returns a new initialized Mapper.
func NewMapper[K comparable, V any]() Mapper[K, V] {
return make(Mapper[K, V])
}
// Get returns the value associated with the key.
// If the key doesn't exist or the map is nil, it returns the zero value for the value type.
func (m Mapper[K, V]) Get(key K) V {
if m == nil {
var zero V
return zero
}
return m[key]
}
// OK returns the value associated with the key and a boolean indicating whether the key exists.
func (m Mapper[K, V]) OK(key K) (V, bool) {
if m == nil {
var zero V
return zero, false
}
val, ok := m[key]
return val, ok
}
// Set sets the value for the specified key.
// Does nothing if the map is nil.
func (m Mapper[K, V]) Set(key K, value V) Mapper[K, V] {
if m != nil {
m[key] = value
}
return m
}
// Delete removes the specified key from the map.
// Does nothing if the key doesn't exist or the map is nil.
func (m Mapper[K, V]) Delete(key K) Mapper[K, V] {
if m != nil {
delete(m, key)
}
return m
}
// Has returns true if the key exists in the map, false otherwise.
func (m Mapper[K, V]) Has(key K) bool {
if m == nil {
return false
}
_, exists := m[key]
return exists
}
// Len returns the number of elements in the map.
// Returns 0 if the map is nil.
func (m Mapper[K, V]) Len() int {
if m == nil {
return 0
}
return len(m)
}
// Keys returns a slice containing all keys in the map.
// Returns nil if the map is nil or empty.
func (m Mapper[K, V]) Keys() []K {
if m == nil {
return nil
}
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func (m Mapper[K, V]) Clear() {
if m == nil {
return
}
for k := range m {
delete(m, k)
}
}
// Values returns a slice containing all values in the map.
// Returns nil if the map is nil or empty.
func (m Mapper[K, V]) Values() []V {
if m == nil {
return nil
}
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// Each iterates over each key-value pair in the map and calls the provided function.
// Does nothing if the map is nil.
func (m Mapper[K, V]) Each(fn func(K, V)) {
if m != nil {
for k, v := range m {
fn(k, v)
}
}
}
// Filter returns a new Mapper containing only the key-value pairs that satisfy the predicate.
func (m Mapper[K, V]) Filter(fn func(K, V) bool) Mapper[K, V] {
result := NewMapper[K, V]()
if m != nil {
for k, v := range m {
if fn(k, v) {
result[k] = v
}
}
}
return result
}
// MapValues returns a new Mapper with the same keys but values transformed by the provided function.
func (m Mapper[K, V]) MapValues(fn func(V) V) Mapper[K, V] {
result := NewMapper[K, V]()
if m != nil {
for k, v := range m {
result[k] = fn(v)
}
}
return result
}
// Clone returns a shallow copy of the Mapper.
func (m Mapper[K, V]) Clone() Mapper[K, V] {
result := NewMapper[K, V]()
if m != nil {
for k, v := range m {
result[k] = v
}
}
return result
}
// Slicer converts the Mapper to a Slicer of key-value pairs.
func (m Mapper[K, V]) Slicer() Slicer[KeyValuePair[K, V]] {
if m == nil {
return nil
}
result := make(Slicer[KeyValuePair[K, V]], 0, len(m))
for k, v := range m {
result = append(result, KeyValuePair[K, V]{Key: k, Value: v})
}
return result
}
func (m Mapper[K, V]) SortedKeys() []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
a, b := any(keys[i]), any(keys[j])
switch va := a.(type) {
case int:
if vb, ok := b.(int); ok {
return va < vb
}
case int32:
if vb, ok := b.(int32); ok {
return va < vb
}
case int64:
if vb, ok := b.(int64); ok {
return va < vb
}
case uint:
if vb, ok := b.(uint); ok {
return va < vb
}
case uint64:
if vb, ok := b.(uint64); ok {
return va < vb
}
case float32:
if vb, ok := b.(float32); ok {
return va < vb
}
case float64:
if vb, ok := b.(float64); ok {
return va < vb
}
case string:
if vb, ok := b.(string); ok {
return va < vb
}
}
// fallback to string comparison
return fmt.Sprintf("%v", a) < fmt.Sprintf("%v", b)
})
return keys
}

128
vendor/github.com/olekukonko/tablewriter/tw/renderer.go generated vendored Normal file
View File

@@ -0,0 +1,128 @@
package tw
import (
"github.com/olekukonko/ll"
"io"
)
// Renderer defines the interface for rendering tables to an io.Writer.
// Implementations must handle headers, rows, footers, and separator lines.
type Renderer interface {
Start(w io.Writer) error
Header(headers [][]string, ctx Formatting) // Renders table header
Row(row []string, ctx Formatting) // Renders a single row
Footer(footers [][]string, ctx Formatting) // Renders table footer
Line(ctx Formatting) // Renders separator line
Config() Rendition // Returns renderer config
Close() error // Gets Rendition form Blueprint
Logger(logger *ll.Logger) // send logger to renderers
}
// Rendition holds the configuration for the default renderer.
type Rendition struct {
Borders Border // Border visibility settings
Symbols Symbols // Symbols used for table drawing
Settings Settings // Rendering behavior settings
Streaming bool
}
// Renditioning has a method to update its rendition.
// Let's define an optional interface for this.
type Renditioning interface {
Rendition(r Rendition)
}
// Formatting encapsulates the complete formatting context for a table row.
// It provides all necessary information to render a row correctly within the table structure.
type Formatting struct {
Row RowContext // Detailed configuration for the row and its cells
Level Level // Hierarchical level (Header, Body, Footer) affecting line drawing
HasFooter bool // Indicates if the table includes a footer section
IsSubRow bool // Marks this as a continuation or padding line in multi-line rows
NormalizedWidths Mapper[int, int]
}
// CellContext defines the properties and formatting state of an individual table cell.
type CellContext struct {
Data string // Content to be displayed in the cell, provided by the caller
Align Align // Text alignment within the cell (Left, Right, Center, Skip)
Padding Padding // Padding characters surrounding the cell content
Width int // Suggested width (often overridden by Row.Widths)
Merge MergeState // Details about cell spanning across rows or columns
}
// MergeState captures how a cell merges across different directions.
type MergeState struct {
Vertical MergeStateOption // Properties for vertical merging (across rows)
Horizontal MergeStateOption // Properties for horizontal merging (across columns)
Hierarchical MergeStateOption // Properties for nested/hierarchical merging
}
// MergeStateOption represents common attributes for merging in a specific direction.
type MergeStateOption struct {
Present bool // True if this merge direction is active
Span int // Number of cells this merge spans
Start bool // True if this cell is the starting point of the merge
End bool // True if this cell is the ending point of the merge
}
// RowContext manages layout properties and relationships for a row and its columns.
// It maintains state about the current row and its neighbors for proper rendering.
type RowContext struct {
Position Position // Section of the table (Header, Row, Footer)
Location Location // Boundary position (First, Middle, End)
Current map[int]CellContext // Cells in this row, indexed by column
Previous map[int]CellContext // Cells from the row above; nil if none
Next map[int]CellContext // Cells from the row below; nil if none
Widths Mapper[int, int] // Computed widths for each column
ColMaxWidths CellWidth // Maximum allowed width per column
}
func (r RowContext) GetCell(col int) CellContext {
return r.Current[col]
}
// Separators controls the visibility of separators in the table.
type Separators struct {
ShowHeader State // Controls header separator visibility
ShowFooter State // Controls footer separator visibility
BetweenRows State // Determines if lines appear between rows
BetweenColumns State // Determines if separators appear between columns
}
// Lines manages the visibility of table boundary lines.
type Lines struct {
ShowTop State // Top border visibility
ShowBottom State // Bottom border visibility
ShowHeaderLine State // Header separator line visibility
ShowFooterLine State // Footer separator line visibility
}
// Settings holds configuration preferences for rendering behavior.
type Settings struct {
Separators Separators // Separator visibility settings
Lines Lines // Line visibility settings
CompactMode State // Reserved for future compact rendering (unused)
// Cushion State
}
// Border defines the visibility states of table borders.
type Border struct {
Left State // Left border visibility
Right State // Right border visibility
Top State // Top border visibility
Bottom State // Bottom border visibility
}
// BorderNone defines a border configuration with all sides disabled.
var (
PaddingNone = Padding{Left: Empty, Right: Empty, Top: Empty, Bottom: Empty}
BorderNone = Border{Left: Off, Right: Off, Top: Off, Bottom: Off}
LinesNone = Lines{ShowTop: Off, ShowBottom: Off, ShowHeaderLine: Off, ShowFooterLine: Off}
SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off}
)
type StreamConfig struct {
Enable bool
Widths CellWidth // Cell/column widths
}

147
vendor/github.com/olekukonko/tablewriter/tw/slicer.go generated vendored Normal file
View File

@@ -0,0 +1,147 @@
package tw
// Slicer is a generic slice type that provides additional methods for slice manipulation.
type Slicer[T any] []T
// NewSlicer creates and returns a new initialized Slicer.
func NewSlicer[T any]() Slicer[T] {
return make(Slicer[T], 0)
}
// Get returns the element at the specified index.
// Returns the zero value if the index is out of bounds or the slice is nil.
func (s Slicer[T]) Get(index int) T {
if s == nil || index < 0 || index >= len(s) {
var zero T
return zero
}
return s[index]
}
// GetOK returns the element at the specified index and a boolean indicating whether the index was valid.
func (s Slicer[T]) GetOK(index int) (T, bool) {
if s == nil || index < 0 || index >= len(s) {
var zero T
return zero, false
}
return s[index], true
}
// Append appends elements to the slice and returns the new slice.
func (s Slicer[T]) Append(elements ...T) Slicer[T] {
return append(s, elements...)
}
// Prepend adds elements to the beginning of the slice and returns the new slice.
func (s Slicer[T]) Prepend(elements ...T) Slicer[T] {
return append(elements, s...)
}
// Len returns the number of elements in the slice.
// Returns 0 if the slice is nil.
func (s Slicer[T]) Len() int {
if s == nil {
return 0
}
return len(s)
}
// IsEmpty returns true if the slice is nil or has zero elements.
func (s Slicer[T]) IsEmpty() bool {
return s.Len() == 0
}
// Has returns true if the index exists in the slice.
func (s Slicer[T]) Has(index int) bool {
return index >= 0 && index < s.Len()
}
// First returns the first element of the slice, or the zero value if empty.
func (s Slicer[T]) First() T {
return s.Get(0)
}
// Last returns the last element of the slice, or the zero value if empty.
func (s Slicer[T]) Last() T {
return s.Get(s.Len() - 1)
}
// Each iterates over each element in the slice and calls the provided function.
// Does nothing if the slice is nil.
func (s Slicer[T]) Each(fn func(T)) {
if s != nil {
for _, v := range s {
fn(v)
}
}
}
// Filter returns a new Slicer containing only elements that satisfy the predicate.
func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] {
result := NewSlicer[T]()
if s != nil {
for _, v := range s {
if fn(v) {
result = result.Append(v)
}
}
}
return result
}
// Map returns a new Slicer with each element transformed by the provided function.
func (s Slicer[T]) Map(fn func(T) T) Slicer[T] {
result := NewSlicer[T]()
if s != nil {
for _, v := range s {
result = result.Append(fn(v))
}
}
return result
}
// Contains returns true if the slice contains an element that satisfies the predicate.
func (s Slicer[T]) Contains(fn func(T) bool) bool {
if s != nil {
for _, v := range s {
if fn(v) {
return true
}
}
}
return false
}
// Find returns the first element that satisfies the predicate, along with a boolean indicating if it was found.
func (s Slicer[T]) Find(fn func(T) bool) (T, bool) {
if s != nil {
for _, v := range s {
if fn(v) {
return v, true
}
}
}
var zero T
return zero, false
}
// Clone returns a shallow copy of the Slicer.
func (s Slicer[T]) Clone() Slicer[T] {
result := NewSlicer[T]()
if s != nil {
result = append(result, s...)
}
return result
}
// SlicerToMapper converts a Slicer of KeyValuePair to a Mapper.
func SlicerToMapper[K comparable, V any](s Slicer[KeyValuePair[K, V]]) Mapper[K, V] {
result := make(Mapper[K, V])
if s == nil {
return result
}
for _, pair := range s {
result[pair.Key] = pair.Value
}
return result
}

51
vendor/github.com/olekukonko/tablewriter/tw/state.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
package tw
// State represents an on/off state
type State int
// Public: Methods for State type
// Enabled checks if the state is on
func (o State) Enabled() bool { return o == Success }
// Default checks if the state is unknown
func (o State) Default() bool { return o == Unknown }
// Disabled checks if the state is off
func (o State) Disabled() bool { return o == Fail }
// Toggle switches the state between on and off
func (o State) Toggle() State {
if o == Fail {
return Success
}
return Fail
}
// Cond executes a condition if the state is enabled
func (o State) Cond(c func() bool) bool {
if o.Enabled() {
return c()
}
return false
}
// Or returns this state if enabled, else the provided state
func (o State) Or(c State) State {
if o.Enabled() {
return o
}
return c
}
// String returns the string representation of the state
func (o State) String() string {
if o.Enabled() {
return "on"
}
if o.Disabled() {
return "off"
}
return "undefined"
}

1012
vendor/github.com/olekukonko/tablewriter/tw/symbols.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

105
vendor/github.com/olekukonko/tablewriter/tw/tw.go generated vendored Normal file
View File

@@ -0,0 +1,105 @@
package tw
// Operation Status Constants
// Used to indicate the success or failure of operations
const (
Pending = 0 // Operation failed
Fail = -1 // Operation failed
Success = 1 // Operation succeeded
MinimumColumnWidth = 8
)
const (
Empty = ""
Skip = ""
Space = " "
NewLine = "\n"
)
// Feature State Constants
// Represents enabled/disabled states for features
const (
Unknown State = Pending // Feature is enabled
On State = Success // Feature is enabled
Off State = Fail // Feature is disabled
)
// Table Alignment Constants
// Defines text alignment options for table content
const (
AlignNone Align = "none" // Center-aligned text
AlignCenter Align = "center" // Center-aligned text
AlignRight Align = "right" // Right-aligned text
AlignLeft Align = "left" // Left-aligned text
AlignDefault = AlignLeft // Left-aligned text
)
const (
Header Position = "header" // Table header section
Row Position = "row" // Table row section
Footer Position = "footer" // Table footer section
)
const (
LevelHeader Level = iota // Topmost line position
LevelBody // LevelBody line position
LevelFooter // LevelFooter line position
)
const (
LocationFirst Location = "first" // Topmost line position
LocationMiddle Location = "middle" // LevelBody line position
LocationEnd Location = "end" // LevelFooter line position
)
const (
SectionHeader = "heder"
SectionRow = "row"
SectionFooter = "footer"
)
// Text Wrapping Constants
// Defines text wrapping behavior in table cells
const (
WrapNone = iota // No wrapping
WrapNormal // Standard word wrapping
WrapTruncate // Truncate text with ellipsis
WrapBreak // Break words to fit
)
// Cell Merge Constants
// Specifies cell merging behavior in tables
const (
MergeNone = iota // No merging
MergeVertical // Merge cells vertically
MergeHorizontal // Merge cells horizontally
MergeBoth // Merge both vertically and horizontally
MergeHierarchical // Hierarchical merging
)
// Special Character Constants
// Defines special characters used in formatting
const (
CharEllipsis = "…" // Ellipsis character for truncation
CharBreak = "↩" // Break character for wrapping
)
type Spot int
const (
SpotNone Spot = iota
SpotTopLeft
SpotTopCenter
SpotTopRight
SpotBottomLeft
SpotBottomCenter // Default for legacy SetCaption
SpotBottomRight
SpotLeftTop
SpotLeftCenter
SpotLeftBottom
SpotRightTop
SpotRightCenter
SpotRIghtBottom
)

153
vendor/github.com/olekukonko/tablewriter/tw/types.go generated vendored Normal file
View File

@@ -0,0 +1,153 @@
// Package tw defines types and constants for table formatting and configuration,
// including validation logic for various table properties.
package tw
import (
"fmt"
"github.com/olekukonko/errors"
"strings"
) // Custom error handling library
// Position defines where formatting applies in the table (e.g., header, footer, or rows).
type Position string
// Validate checks if the Position is one of the allowed values: Header, Footer, or Row.
func (pos Position) Validate() error {
switch pos {
case Header, Footer, Row:
return nil // Valid position
}
// Return an error for any unrecognized position
return errors.New("invalid position")
}
// Filter defines a function type for processing cell content.
// It takes a slice of strings (representing cell data) and returns a processed slice.
type Filter func([]string) []string
// Formatter defines an interface for types that can format themselves into a string.
// Used for custom formatting of table cell content.
type Formatter interface {
Format() string // Returns the formatted string representation
}
// Align specifies the text alignment within a table cell.
type Align string
// Validate checks if the Align is one of the allowed values: None, Center, Left, or Right.
func (a Align) Validate() error {
switch a {
case AlignNone, AlignCenter, AlignLeft, AlignRight:
return nil // Valid alignment
}
// Return an error for any unrecognized alignment
return errors.New("invalid align")
}
type Alignment []Align
func (a Alignment) String() string {
var str strings.Builder
for i, a := range a {
if i > 0 {
str.WriteString("; ")
}
str.WriteString(fmt.Sprint(i))
str.WriteString("=")
str.WriteString(string(a))
}
return str.String()
}
func (a Alignment) Add(aligns ...Align) Alignment {
aa := make(Alignment, len(aligns))
copy(aa, aligns)
return aa
}
func (a Alignment) Set(col int, align Align) Alignment {
if col >= 0 && col < len(a) {
a[col] = align
}
return a
}
// Copy creates a new independent copy of the Alignment
func (a Alignment) Copy() Alignment {
aa := make(Alignment, len(a))
copy(aa, a)
return aa
}
// Level indicates the vertical position of a line in the table (e.g., header, body, or footer).
type Level int
// Validate checks if the Level is one of the allowed values: Header, Body, or Footer.
func (l Level) Validate() error {
switch l {
case LevelHeader, LevelBody, LevelFooter:
return nil // Valid level
}
// Return an error for any unrecognized level
return errors.New("invalid level")
}
// Location specifies the horizontal position of a cell or column within a table row.
type Location string
// Validate checks if the Location is one of the allowed values: First, Middle, or End.
func (l Location) Validate() error {
switch l {
case LocationFirst, LocationMiddle, LocationEnd:
return nil // Valid location
}
// Return an error for any unrecognized location
return errors.New("invalid location")
}
type Caption struct {
Text string
Spot Spot
Align Align
Width int
}
func (c Caption) WithText(text string) Caption {
c.Text = text
return c
}
func (c Caption) WithSpot(spot Spot) Caption {
c.Spot = spot
return c
}
func (c Caption) WithAlign(align Align) Caption {
c.Align = align
return c
}
func (c Caption) WithWidth(width int) Caption {
c.Width = width
return c
}
// Padding defines custom padding characters for a cell
type Padding struct {
Left string
Right string
Top string
Bottom string
}
type Control struct {
Hide State
}
// Behavior defines table behavior settings that control features like auto-hiding columns and trimming spaces.
type Behavior struct {
AutoHide State // Controls whether empty columns are automatically hidden (ignored in streaming mode)
TrimSpace State // Controls whether leading/trailing spaces are trimmed from cell content
Header Control
Footer Control
}

View File

@@ -1,93 +0,0 @@
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
package tablewriter
import (
"math"
"regexp"
"strings"
"github.com/mattn/go-runewidth"
)
var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
func DisplayWidth(str string) int {
return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
}
// Simple Condition for string
// Returns value based on condition
func ConditionString(cond bool, valid, inValid string) string {
if cond {
return valid
}
return inValid
}
func isNumOrSpace(r rune) bool {
return ('0' <= r && r <= '9') || r == ' '
}
// Format Table Header
// Replace _ , . and spaces
func Title(name string) string {
origLen := len(name)
rs := []rune(name)
for i, r := range rs {
switch r {
case '_':
rs[i] = ' '
case '.':
// ignore floating number 0.0
if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) {
rs[i] = ' '
}
}
}
name = string(rs)
name = strings.TrimSpace(name)
if len(name) == 0 && origLen > 0 {
// Keep at least one character. This is important to preserve
// empty lines in multi-line headers/footers.
name = " "
}
return strings.ToUpper(name)
}
// Pad String
// Attempts to place string in the center
func Pad(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
gapLeft := int(math.Ceil(float64(gap / 2)))
gapRight := gap - gapLeft
return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight)
}
return s
}
// Pad String Right position
// This would place string at the left side of the screen
func PadRight(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
return s + strings.Repeat(string(pad), gap)
}
return s
}
// Pad String Left position
// This would place string at the right side of the screen
func PadLeft(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
return strings.Repeat(string(pad), gap) + s
}
return s
}

View File

@@ -1,99 +0,0 @@
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
package tablewriter
import (
"math"
"strings"
"github.com/mattn/go-runewidth"
)
var (
nl = "\n"
sp = " "
)
const defaultPenalty = 1e5
// Wrap wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapString(s string, lim int) ([]string, int) {
words := strings.Split(strings.Replace(s, nl, sp, -1), sp)
var lines []string
max := 0
for _, v := range words {
max = runewidth.StringWidth(v)
if max > lim {
lim = max
}
}
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, strings.Join(line, sp))
}
return lines, lim
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses,
// WrapString will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each rune as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words []string, spc, lim, pen int) [][]string {
n := len(words)
length := make([][]int, n)
for i := 0; i < n; i++ {
length[i] = make([]int, n)
length[i][i] = runewidth.StringWidth(words[i])
for j := i + 1; j < n; j++ {
length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j])
}
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
for i := n - 1; i >= 0; i-- {
if length[i][n-1] <= lim {
cost[i] = 0
nbrk[i] = n
} else {
for j := i + 1; j < n; j++ {
d := lim - length[i][j-1]
c := d*d + cost[j]
if length[i][j-1] > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
}
var lines [][]string
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}
// getLines decomposes a multiline string into a slice of strings.
func getLines(s string) []string {
return strings.Split(s, nl)
}

Some files were not shown because too many files have changed in this diff Show More