Compare commits

...
32 Commits
Author SHA1 Message Date
moloch-- 13bf9d0a01 Implement --rc for scripting initial inputs 2026-01-04 17:51:59 -08:00
moloch-- dc736f6095 Update readme 2026-01-04 12:18:58 -08:00
Joe e02d53ce67 Merge pull request #2088 from BishopFox/fix/readline
Reduce flickering text in readline
2026-01-04 13:02:14 -07:00
Joe 064cd6a2cc Merge pull request #2087 from BishopFox/fix/mods3
Rename implant/sliver imports
2026-01-04 12:00:46 -07:00
moloch-- eab9d0b653 Reduce flickering text in readline 2026-01-04 10:59:20 -08:00
Joe 29c4ec0afa Merge pull request #2032 from carterburn/feature/aka-command
New command: add command aliases (aka)
2026-01-04 11:19:15 -07:00
moloch-- b7c336cb76 Rename implant/sliver imports 2026-01-04 10:17:56 -08:00
moloch-- d8ddd09c86 Update pretty table 2026-01-04 09:42:44 -08:00
Joe 04bb356207 Merge branch 'master' into feature/aka-command 2026-01-04 10:19:52 -07:00
Joe a003a5e3cf Merge pull request #2086 from AnvithLobo/master
Fix beacon panic race condition
2026-01-04 09:19:53 -07:00
Joe 75e3cad76a Merge branch 'master' into feature/aka-command 2026-01-04 09:06:36 -07:00
AnvithLobo 8aa0cbee7e Fix beacon panic race condition
Tasks are now dispatched to background goroutines, and their results are collected asynchronously via a channel. Check-ins include any pending results from previous tasks, improving responsiveness and reliability. Introduced beaconDispatchTasks to manage background execution and result collection.
2026-01-04 07:57:26 +05:30
Joe 30a50717e1 Merge pull request #2029 from BishopFox/7a58dependabot/go_modules/github.com/klauspost/compress-1.18.1
Bump github.com/klauspost/compress from 1.18.0 to 1.18.1
2026-01-02 21:34:03 -07:00
Joe f4c9e7109b Merge pull request #2084 from BishopFox/fix/mods2
Improvements to Implant Build Layout
2026-01-02 21:19:16 -07:00
moloch-- 08f3997756 Fix sliver.h -> main.h 2026-01-02 21:40:45 -06:00
moloch-- 3aebebecbb Implement RenameImport() 2026-01-02 21:22:19 -06:00
moloch-- 1d7eca8e39 Validate module path 2026-01-02 21:06:14 -06:00
moloch-- 773ee5f867 Rename sliver.go -> main.go 2026-01-02 20:55:26 -06:00
moloch-- 7608a4b0a0 Add GoPackage to ImplantConfig pb 2026-01-02 20:41:35 -06:00
moloch-- 9ecd36b7b6 Implement module rename 2026-01-02 20:33:27 -06:00
moloch-- d8fc5a6df4 Tweak project dir structure 2026-01-02 19:02:07 -06:00
moloch-- 649a6582e0 Add -mod=vendor to GoBuild 2026-01-02 18:53:31 -06:00
moloch-- 00e8a0cfca Move main implant code out of main pkg 2026-01-02 18:50:26 -06:00
moloch-- e9890bc8f9 Set limit to unauthed body req size 2026-01-02 17:32:43 -06:00
Joe 8b798f8307 Merge pull request #2083 from BishopFox/feature/mcp
Experiamental MCP Support
2026-01-02 16:25:24 -07:00
dependabot[bot] c09c57da3f Bump github.com/klauspost/compress from 1.18.0 to 1.18.1
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Changelog](https://github.com/klauspost/compress/blob/master/.goreleaser.yml)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-03 20:05:33 +00:00
Carter Burn a21b0da928 Merge branch 'BishopFox:master' into feature/aka-command 2025-10-28 21:16:15 -04:00
carterburn 8a0ba038f6 Add readme to give aka command breakdown 2025-10-27 21:29:35 -04:00
carterburn dd072bbdb6 Merge remote-tracking branch 'origin/master' into feature/aka-command 2025-10-26 21:48:59 -04:00
carterburn 1e3c736acf Update types to support storing aka aliases
Updated the way aka aliases are stored to support saving the aka aliases
to disk.

Aka aliases will automatically be saved to disk and loaded on client
startup.
2025-10-26 21:45:03 -04:00
carterburn 3d880914d9 Clean up settings and hook for no quotes
Previous commit forced the user to use quotes in the creation command
and in any arguments that include spaces when expanded a command alias.

This commit removes that requirement and allows the user to manipulate
the shell interface as if they were typing commands directly.
2025-10-23 20:39:19 -04:00
carterburn 1162de9ee3 Initial design of command aliases
Command aliases will allow a user to define "aliases" for shorthands and
to pass default arguments

Currently, aliases with more than one word commands (like a command
alias for "execute -o") require the use of single or double quotes to
pass the arguments correctly. Additionally, any use of flags in the
actual command will attempt to parse for the "aka" command and we want
the "aka create" command to be bare to just grab everything after the
command alias name.

Subsequent changes will include:
- better parsing of flags as described above
- saving command aliases to disk
- better help messages
2025-10-23 06:05:01 -04:00
134 changed files with 19028 additions and 7284 deletions

No files matched your search

+1
View File
@@ -5,6 +5,7 @@
/artifacts
/artifacts/**
*.rc
/Dockerfile.*
/*.sdk.tar.xz
-1
View File
@@ -46,4 +46,3 @@ Please take a moment and fill out [our survey](https://forms.gle/SwVsHFNh24ChG58
### License - GPLv3
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
+1
View File
@@ -51,6 +51,7 @@ func init() {
defer logFile.Close()
rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
// Create the console client, without any RPC or commands bound to it yet.
// This created before anything so that multiple commands can make use of
+9 -3
View File
@@ -40,6 +40,7 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
Short: "Start the sliver client console",
}
consoleCmd.Flags().String(RCFlagName, "", "path to rc script file")
consoleCmd.RunE, consoleCmd.PersistentPostRunE = consoleRunnerCmd(con, true)
return consoleCmd
}
@@ -47,7 +48,7 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
var ln *grpc.ClientConn
pre = func(_ *cobra.Command, _ []string) error {
pre = func(cmd *cobra.Command, _ []string) error {
configs := assets.GetConfigs()
if len(configs) == 0 {
@@ -59,13 +60,18 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
return nil
}
rcScript, err := ReadRCScript(cmd)
if err != nil {
fmt.Printf("Failed to read rc script: %s\n", err)
return nil
}
// Don't clobber output when simply running an implant command from system shell.
if run {
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
}
var rpc rpcpb.SliverRPCClient
var err error
rpc, ln, err = transport.MTLSConnect(config)
if err != nil {
@@ -76,7 +82,7 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
// Wait for any connection state changes and exit if the connection is lost.
go handleConnectionLost(ln)
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run)
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
}
// Close the RPC connection once exiting
+31
View File
@@ -0,0 +1,31 @@
package cli
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
const RCFlagName = "rc"
func ReadRCScript(cmd *cobra.Command) (string, error) {
if cmd == nil {
return "", nil
}
if cmd.Flags().Lookup(RCFlagName) == nil {
return "", nil
}
rcPath, err := cmd.Flags().GetString(RCFlagName)
if err != nil {
return "", err
}
if rcPath == "" {
return "", nil
}
data, err := os.ReadFile(rcPath)
if err != nil {
return "", fmt.Errorf("read rc script %q: %w", rcPath, err)
}
return string(data), nil
}
+1 -1
View File
@@ -8,7 +8,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
- `command.go` – Provides helpers for binding Cobra commands, flag sets, and visibility filters into the client command tree.
- `server.go` – Defines the server menu composition, wiring each server-side command group onto the root console structure at runtime.
- `sliver.go` – Builds the implant/Sliver menu, attaching per-target command groups and loading alias/extension manifests before exposure.
- `main.go` – Builds the implant/Sliver menu, attaching per-target command groups and loading alias/extension manifests before exposure.
## Sub-packages
+23
View File
@@ -0,0 +1,23 @@
# client/command/aka
## Overview
Implements the 'aka' command group for the Sliver client console. "Aka"s are
command aliases (like 'alias' in 'bash') that allow operators to specify
shorthand commands and default arguments for commonly used commands.
### Examples:
- `aka create bg background`: Allows an operator to run `bg` from the implant
menu to quickly move back to the server menu
- `aka create execw execute -o "C:\Windows\System32\cmd.exe" /c`: Shorthand for
executing binaries on Windows targets like: `execw arp -a`
## Go Files
- `commands.go` – Wires Cobra commands for listing, creating, and deleting command aliases in the client console.
- `create.go` – Handles creating command aliases to use by the client.
- `delete.go` – Removes command aliases currently tracked by the client.
- `hook.go` – The main hooking logic to intercept commands ran by the client console. The hook will intercept processing before hitting the Cobra tree to transform the command alias to a full command with default arguments from the command alias and user-provided arguments to the alias.
- `list.go` - Lists the current command aliases tracked by the client.
- `storage.go` - Supports the saving of aliases to disk to keep between runs of the client.
+59
View File
@@ -0,0 +1,59 @@
package aka
import (
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return commands(con, constants.GenericHelpGroup)
}
func ImplantCommands(con *console.SliverClient) []*cobra.Command {
return commands(con, constants.SliverCoreHelpGroup)
}
func commands(con *console.SliverClient, group string) []*cobra.Command {
akaCommand := &cobra.Command{
Use: "aka",
Short: "Manage command aliases (aka's)",
Run: func(cmd *cobra.Command, args []string) {
AkaListCmd(cmd, con, args)
},
GroupID: group,
}
createAka := &cobra.Command{
Use: "create <alias> <command> [args...]",
Short: "Create a new command alias (aka)",
Args: cobra.MinimumNArgs(2),
DisableFlagParsing: true, // No flags with aka create so we capture flags to other commands
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
AkaCreateCmd(cmd, con, args)
},
}
createAka.InitDefaultHelpFlag()
createAka.Flags().Lookup("help").Hidden = true
akaCommand.AddCommand(createAka)
deleteAka := &cobra.Command{
Use: "delete <alias>",
Short: "Delete a command alias (aka)",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
AkaDeleteCmd(cmd, con, args)
},
}
akaCommand.AddCommand(deleteAka)
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, Cmdhook)
err := LoadAkaAliases()
if err != nil {
con.PrintErrorf("Failed to load aka alises: %s\n", err)
}
return []*cobra.Command{akaCommand}
}
+46
View File
@@ -0,0 +1,46 @@
package aka
import (
"fmt"
"strings"
"github.com/bishopfox/sliver/client/console"
"github.com/spf13/cobra"
)
func AkaCreateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
aliasName := args[0]
command := args[1]
defaultArgs := args[2:] // anything else is going to be the default args to the alias
if _, exists := akaAliases[aliasName]; exists {
con.PrintErrorf("Alias '%s' already exists\n", aliasName)
return
}
var desc string
if len(defaultArgs) == 0 {
desc = fmt.Sprintf("%s", command)
} else {
desc = fmt.Sprintf("%s %s", command, strings.Join(defaultArgs, " "))
}
alias := &AkaAlias{
Name: aliasName,
Command: command,
DefaultArgs: defaultArgs,
Description: desc,
}
akaAliases[aliasName] = alias
// save this new alias to disk
err := SaveAkaAliases()
if err != nil {
con.PrintErrorf("Failed to save alias '%s' to disk: %s\n", aliasName, err)
// we still technically added it to the map, so the alias did get created in
// memory so we should still print the Info message below
}
con.PrintInfof("Create alias for '%s' -> '%v'\n", aliasName, alias.Description)
}
+25
View File
@@ -0,0 +1,25 @@
package aka
import (
"github.com/bishopfox/sliver/client/console"
"github.com/spf13/cobra"
)
func AkaDeleteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
aliasName := args[0]
if _, exists := akaAliases[aliasName]; !exists {
con.PrintErrorf("Alias '%s' does not exist\n", aliasName)
return
}
delete(akaAliases, aliasName)
// Save updated map to disk
err := SaveAkaAliases()
if err != nil {
con.PrintErrorf("Failed to save aliases to disk: %s\n", err)
return
}
con.PrintInfof("Deleted alias '%s'\n", aliasName)
}
+27
View File
@@ -0,0 +1,27 @@
package aka
import (
"strings"
)
func Cmdhook(args []string) ([]string, error) {
if len(args) == 0 {
return args, nil
}
if aliasArgs, exists := akaAliases[args[0]]; exists {
var expandedArgs []string
expandedArgs = append(expandedArgs, aliasArgs.Command)
expandedArgs = append(expandedArgs, aliasArgs.DefaultArgs...)
// join the user passed args so the shell processes it without flags if
// available
if len(args[1:]) > 0 {
expandedArgs = append(expandedArgs, strings.Join(args[1:], " "))
}
return expandedArgs, nil
}
return args, nil
}
+31
View File
@@ -0,0 +1,31 @@
package aka
import (
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
func AkaListCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if len(akaAliases) == 0 {
con.PrintInfof("No command aliases created. Use `aka create` to create one.\n")
return
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Alias",
"Command",
})
for alias, cmd := range akaAliases {
tw.AppendRow(table.Row{
alias,
cmd.Description,
})
}
con.Printf("%s\n", tw.Render())
}
+68
View File
@@ -0,0 +1,68 @@
package aka
import (
"encoding/json"
"os"
"path/filepath"
"github.com/bishopfox/sliver/client/assets"
)
type AkaAlias struct {
Name string `json:"name"` // alias Name
Command string `json:"command"` // command being aliased
DefaultArgs []string `json:"default_args"` // args passed by the user
Description string `json:"description"`
}
var akaAliases = make(map[string]*AkaAlias)
const (
akaAliasFileName = "aka-aliases.json"
)
func GetAkaAliasesFilePath() string {
return filepath.Join(assets.GetRootAppDir(), akaAliasFileName)
}
func LoadAkaAliases() error {
filepath := GetAkaAliasesFilePath()
data, err := os.ReadFile(filepath)
if err != nil {
// if file hasn't been created yet, we fail gracefully
if os.IsNotExist(err) {
return nil
}
// otherwise, return the actual error; something else went wrong
return err
}
aliases := []AkaAlias{}
err = json.Unmarshal(data, &aliases)
if err != nil {
return err
}
akaAliases = make(map[string]*AkaAlias)
for _, alias := range aliases {
akaAliases[alias.Name] = &alias
}
return nil
}
func SaveAkaAliases() error {
filepath := GetAkaAliasesFilePath()
aliases := make([]*AkaAlias, 0, len(akaAliases))
for _, alias := range akaAliases {
aliases = append(aliases, alias)
}
data, err := json.MarshalIndent(aliases, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath, data, 0o600)
}
+2
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/command/aka"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/armory"
"github.com/bishopfox/sliver/client/command/beacons"
@@ -102,6 +103,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
crack.Commands,
certificates.Commands,
clean.Command,
aka.ServerCommands,
)
// C2 Network
+2
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/aka"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/backdoor"
"github.com/bishopfox/sliver/client/command/cursed"
@@ -78,6 +79,7 @@ func SliverCommands(con *client.SliverClient) console.Commands {
// use.Commands,
tasks.Commands,
pivots.Commands,
aka.ImplantCommands,
)
// [ Info ]
+77 -3
View File
@@ -41,6 +41,7 @@ import (
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/util"
"github.com/gofrs/uuid"
"github.com/kballard/go-shellquote"
"github.com/reeflective/console"
"github.com/reeflective/readline"
"github.com/spf13/cobra"
@@ -159,7 +160,7 @@ func NewConsole(isServer bool) *SliverClient {
// Init requires a working RPC connection to the sliver server, and 2 different sets of commands.
// If run is true, the console application is started, making this call blocking. Otherwise, commands and
// RPC connection are bound to the console (making the console ready to run), but the console does not start.
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, sliverCmds console.Commands, run bool) error {
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
con.Rpc = rpc
con.IsCLI = !run
@@ -206,6 +207,13 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
con.setupAsciicastRecord(asciicastLog, asciicastStream)
}
if rcScript != "" {
originalPrintf := con.printf
con.printf = fmt.Printf
con.runRCScript(serverCmds, sliverCmds, rcScript)
con.printf = originalPrintf
}
if !con.IsCLI {
return con.App.Start()
}
@@ -213,6 +221,71 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
return nil
}
func (con *SliverClient) runRCScript(serverCmds, sliverCmds console.Commands, rcScript string) {
scanner := bufio.NewScanner(strings.NewReader(rcScript))
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if err := con.runRCLine(serverCmds, sliverCmds, line); err != nil {
con.PrintErrorf("rc line %d error: %s", lineNumber, err)
}
}
if err := scanner.Err(); err != nil {
con.PrintErrorf("rc script error: %s", err)
}
}
func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line string) error {
args, err := shellquote.Split(line)
if err != nil {
return fmt.Errorf("parse error: %w", err)
}
for _, hook := range con.App.PreCmdRunLineHooks {
args, err = hook(args)
if err != nil {
return fmt.Errorf("pre-run line error: %w", err)
}
}
if len(args) == 0 {
return nil
}
menu := con.App.ActiveMenu()
if menu.Name() == consts.ImplantMenu {
menu.Command = sliverCmds()
} else {
menu.Command = serverCmds()
}
target, _, _ := menu.Command.Find(args)
if err := menu.CheckIsAvailable(target); err != nil {
return err
}
for _, hook := range con.App.PreCmdRunHooks {
if err := hook(); err != nil {
return fmt.Errorf("pre-run error: %w", err)
}
}
menu.SetArgs(args)
menu.SetContext(context.Background())
if err := menu.Execute(); err != nil {
return err
}
return nil
}
func (con *SliverClient) startEventLoop() {
eventStream, err := con.Rpc.Events(context.Background(), &commonpb.Empty{})
if err != nil {
@@ -341,11 +414,12 @@ func (con *SliverClient) triggerReactions(event *clientpb.Event) {
con.ActiveTarget.Set(currentActiveSession, currentActiveBeacon)
}()
if event.EventType == consts.SessionOpenedEvent {
switch event.EventType {
case consts.SessionOpenedEvent:
con.ActiveTarget.Set(nil, nil)
con.ActiveTarget.Set(event.Session, nil)
} else if event.EventType == consts.BeaconRegisteredEvent {
case consts.BeaconRegisteredEvent:
con.ActiveTarget.Set(nil, nil)
beacon := &clientpb.Beacon{}
+8
View File
@@ -154,6 +154,14 @@ else
exit 1
fi
# server / gogo / goname
if go test -tags=server,$TAGS ./server/gogo/goname ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo
if go test -tags=server,$TAGS ./server/gogo ; then
:
+4 -2
View File
@@ -4,6 +4,8 @@ go 1.25.3
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
require (
filippo.io/age v1.2.1
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
@@ -25,9 +27,9 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/jedib0t/go-pretty/v6 v6.7.8
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/klauspost/compress v1.18.0
github.com/klauspost/compress v1.18.1
github.com/lesnuages/go-winio v0.4.19
github.com/lesnuages/snitch v0.6.0
github.com/mattn/go-sqlite3 v1.14.32
+6 -6
View File
@@ -270,8 +270,8 @@ github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFr
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=
github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
@@ -289,8 +289,8 @@ github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWr
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
@@ -350,6 +350,8 @@ github.com/moloch--/go-keystone v0.0.2 h1:xThbYs45CSzoDUz5kBZxAyTPIpK3t5M7NB1x+N
github.com/moloch--/go-keystone v0.0.2/go.mod h1:kpGRJpw9b7Eq3MluHrba+GPKZhSyDrvhLULSwH5nGGM=
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e h1:IkFCPlAa0iTiLxck+NqAwBx8JDlnHYm4orOQBbs4BDQ=
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e/go.mod h1:eYeI6cQ5YHhHt9i0BBW0zc1DaQnb4ZMXsSPuEV/V5Og=
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d h1:9RbKockJl6ZdgoEh2QqU3An6Ij/NWZtRWS3J6yG0+qo=
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
github.com/moloch--/sgn v0.0.4 h1:90IC5rx3TmPEfrqPnENB0yyiqcbcNk9LqezXx7uruF0=
github.com/moloch--/sgn v0.0.4/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -391,8 +393,6 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/reeflective/console v0.1.25 h1:do/r6rcKH6pCQDEzh5PhoVFcqHkqcjd0N6tNjpmYWWQ=
github.com/reeflective/console v0.1.25/go.mod h1:It/IqEjo0PowaBuxBS9pEi5EYUgmtvQd0ytMtlS77kk=
github.com/reeflective/readline v1.1.3 h1:meGkuEmujZHmalJ9eT3pYkwtkufH5EwYFPTnaph0T0s=
github.com/reeflective/readline v1.1.3/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+1 -1
View File
@@ -6,7 +6,7 @@ Core Go implementation of the Sliver implant runtime and supporting subsystems.
## Go Files
- `sliver.go` – Implant entry point that wires transports, handlers, and the task runner together at startup.
- `main.go` – Implant entry point that wires transports, handlers, and the task runner together at startup.
## Sub-packages
@@ -1,4 +1,4 @@
#include "sliver.h"
#include "main.h"
#ifdef __WIN32
#include <windows.h>
+47
View File
@@ -0,0 +1,47 @@
package main
/*
Sliver Implant Framework
Copyright (C) 2019 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// {{if or .Config.IsSharedLib }}
//#include "main.h"
import "C"
// {{end}}
import (
"github.com/bishopfox/sliver/implant/sliver/runner"
)
// {{if or .Config.IsSharedLib .Config.IsShellcode}}
var isRunning bool = false
// {{range .Config.Exports}}
//export {{.}}
func {{.}} () {
if !isRunning {
isRunning = true
main()
}
}
// {{end}}
// {{end}}
func main() {
runner.Main()
}
File renamed without changes.
@@ -1,4 +1,4 @@
package main
package runner
/*
Sliver Implant Framework
@@ -18,12 +18,6 @@ package main
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// {{if or .Config.IsSharedLib }}
//#include "sliver.h"
import "C"
// {{end}}
import (
"errors"
@@ -105,26 +99,11 @@ func (serv *sliverService) Execute(args []string, r <-chan svc.ChangeRequest, ch
}
}
}
return
}
// {{end}}
// {{if or .Config.IsSharedLib .Config.IsShellcode}}
var isRunning bool = false
// {{range .Config.Exports}}
//export {{.}}
func {{.}} () {
if !isRunning {
isRunning = true
main()
}
}
// {{end}}
// {{end}}
func main() {
func Main() {
// {{if .Config.Debug}}
log.SetFlags(log.LstdFlags | log.Lshortfile)
@@ -272,17 +251,19 @@ func beaconMainLoop(beacon *transports.Beacon) error {
time.Sleep(time.Second)
beacon.Close()
// BeaconMain - Is executed in it's own goroutine as the function will block
// until all tasks complete (in success or failure), if a task handler blocks
// forever it will simply block this set of tasks instead of the entire beacon
// BeaconMain - Tasks run in background goroutines, check-ins happen on schedule
// Results are collected and sent on subsequent check-ins
pendingResults := make(chan *sliverpb.Envelope, 100)
errors := make(chan error)
shortCircuit := make(chan struct{})
for {
duration := beacon.Duration()
nextCheckin = time.Now().Add(duration)
go func() {
oldInterval := beacon.Interval()
err := beaconMain(beacon, nextCheckin)
err := beaconMain(beacon, nextCheckin, pendingResults)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[beacon] main error: %v", nextCheckin)
@@ -310,7 +291,7 @@ func beaconMainLoop(beacon *transports.Beacon) error {
return nil
}
func beaconMain(beacon *transports.Beacon, nextCheckin time.Time) error {
func beaconMain(beacon *transports.Beacon, nextCheckin time.Time, pendingResults chan *sliverpb.Envelope) error {
err := beacon.Start()
if err != nil {
// {{if .Config.Debug}}
@@ -325,12 +306,26 @@ func beaconMain(beacon *transports.Beacon, nextCheckin time.Time) error {
time.Sleep(time.Second)
beacon.Close()
}()
// Collect any pending results from background tasks
var completedResults []*sliverpb.Envelope
collecting:
for {
select {
case result := <-pendingResults:
completedResults = append(completedResults, result)
default:
break collecting
}
}
// {{if .Config.Debug}}
log.Printf("[beacon] sending check in ...")
log.Printf("[beacon] sending check in with %d pending results...", len(completedResults))
// {{end}}
err = beacon.Send(wrapEnvelope(sliverpb.MsgBeaconTasks, &sliverpb.BeaconTasks{
ID: InstanceID,
NextCheckin: int64(beacon.Duration().Seconds()),
Tasks: completedResults,
}))
if err != nil {
// {{if .Config.Debug}}
@@ -382,30 +377,81 @@ func beaconMain(beacon *transports.Beacon, nextCheckin time.Time) error {
}
}
// ensure extensions are registered before they are called
var results []*sliverpb.Envelope
// Dispatch tasks to background - results sent to pendingResults as each completes
// Extensions must be registered synchronously before other tasks can use them
for _, r := range beaconHandleTasklist(tasksExtensionRegister) {
results = append(results, r)
}
for _, r := range beaconHandleTasklist(tasksOther) {
results = append(results, r)
pendingResults <- r
}
err = beacon.Send(wrapEnvelope(sliverpb.MsgBeaconTasks, &sliverpb.BeaconTasks{
ID: InstanceID,
Tasks: results,
}))
if err != nil {
// {{if .Config.Debug}}
log.Printf("[beacon] error sending results %s", err)
// {{end}}
}
// Dispatch other tasks individually - each sends its result when done
beaconDispatchTasks(tasksOther, pendingResults)
// {{if .Config.Debug}}
log.Printf("[beacon] all results sent to server, cleanup ...")
log.Printf("[beacon] tasks dispatched to background, check-in complete")
// {{end}}
return nil
}
// beaconDispatchTasks dispatches tasks to run in background, sending results as each completes
func beaconDispatchTasks(tasks []*sliverpb.Envelope, pendingResults chan *sliverpb.Envelope) {
sysHandlers := handlers.GetSystemHandlers()
specHandlers := handlers.GetKillHandlers()
for _, task := range tasks {
// {{if .Config.Debug}}
log.Printf("[beacon] dispatching task %d", task.Type)
// {{end}}
if handler, ok := sysHandlers[task.Type]; ok {
data := task.Data
taskID := task.ID
// {{if eq .Config.GOOS "windows" }}
go func() {
handlers.WrapperHandler(handler, data, func(data []byte, err error) {
// {{if .Config.Debug}}
if err != nil {
log.Printf("[beacon] handler function returned an error: %s", err)
}
log.Printf("[beacon] task completed (id: %d)", taskID)
// {{end}}
pendingResults <- &sliverpb.Envelope{
ID: taskID,
Data: data,
}
})
}()
// {{else}}
go func() {
handler(data, func(data []byte, err error) {
// {{if .Config.Debug}}
if err != nil {
log.Printf("[beacon] handler function returned an error: %s", err)
}
log.Printf("[beacon] task completed (id: %d)", taskID)
// {{end}}
pendingResults <- &sliverpb.Envelope{
ID: taskID,
Data: data,
}
})
}()
// {{end}}
} else if task.Type == sliverpb.MsgOpenSession {
go openSessionHandler(task.Data)
pendingResults <- &sliverpb.Envelope{
ID: task.ID,
Data: []byte{},
}
} else if handler, ok := specHandlers[task.Type]; ok {
go handler(task.Data, nil)
} else {
pendingResults <- &sliverpb.Envelope{
ID: task.ID,
UnknownMessageType: true,
}
}
}
}
func beaconHandleTasklist(tasks []*sliverpb.Envelope) []*sliverpb.Envelope {
results := []*sliverpb.Envelope{}
resultsMutex := &sync.Mutex{}
File diff suppressed because it is too large. Load diff
+1
View File
@@ -148,6 +148,7 @@ message ImplantConfig {
bool ObfuscateSymbols = 12;
string TemplateName = 13;
bool SGNEnabled = 14;
string GoPackage = 15;
bool IncludeMTLS = 53;
bool IncludeHTTP = 16;
+151 -61
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc v5.29.3
// protoc-gen-go v1.26.0
// protoc v6.33.2
// source: commonpb/common.proto
package commonpb
@@ -21,16 +21,18 @@ const (
)
type Empty struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Empty) Reset() {
*x = Empty{}
mi := &file_commonpb_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_commonpb_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Empty) String() string {
@@ -41,7 +43,7 @@ func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_commonpb_common_proto_msgTypes[0]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -58,20 +60,23 @@ func (*Empty) Descriptor() ([]byte, []int) {
// Request - Common fields used in all gRPC requests
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
Async bool `protobuf:"varint,1,opt,name=Async,proto3" json:"Async,omitempty"`
Timeout int64 `protobuf:"varint,2,opt,name=Timeout,proto3" json:"Timeout,omitempty"`
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
SessionID string `protobuf:"bytes,9,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Async bool `protobuf:"varint,1,opt,name=Async,proto3" json:"Async,omitempty"`
Timeout int64 `protobuf:"varint,2,opt,name=Timeout,proto3" json:"Timeout,omitempty"`
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
SessionID string `protobuf:"bytes,9,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_commonpb_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_commonpb_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Request) String() string {
@@ -82,7 +87,7 @@ func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_commonpb_common_proto_msgTypes[1]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -126,24 +131,26 @@ func (x *Request) GetSessionID() string {
}
// Response - Common fields used in all gRPC responses. Note that the Err field
//
// only used when the implant needs to return an error to the server.
// Client<->Server comms should use normal gRPC error handling.
// only used when the implant needs to return an error to the server.
// Client<->Server comms should use normal gRPC error handling.
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
Err string `protobuf:"bytes,1,opt,name=Err,proto3" json:"Err,omitempty"`
Async bool `protobuf:"varint,2,opt,name=Async,proto3" json:"Async,omitempty"`
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
TaskID string `protobuf:"bytes,9,opt,name=TaskID,proto3" json:"TaskID,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Err string `protobuf:"bytes,1,opt,name=Err,proto3" json:"Err,omitempty"`
Async bool `protobuf:"varint,2,opt,name=Async,proto3" json:"Async,omitempty"`
BeaconID string `protobuf:"bytes,8,opt,name=BeaconID,proto3" json:"BeaconID,omitempty"`
TaskID string `protobuf:"bytes,9,opt,name=TaskID,proto3" json:"TaskID,omitempty"`
}
func (x *Response) Reset() {
*x = Response{}
mi := &file_commonpb_common_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_commonpb_common_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Response) String() string {
@@ -154,7 +161,7 @@ func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_commonpb_common_proto_msgTypes[2]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -199,18 +206,21 @@ func (x *Response) GetTaskID() string {
// File - A basic file data type
type File struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
}
func (x *File) Reset() {
*x = File{}
mi := &file_commonpb_common_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_commonpb_common_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *File) String() string {
@@ -221,7 +231,7 @@ func (*File) ProtoMessage() {}
func (x *File) ProtoReflect() protoreflect.Message {
mi := &file_commonpb_common_proto_msgTypes[3]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -252,23 +262,26 @@ func (x *File) GetData() []byte {
// Process - A basic process data type
type Process struct {
state protoimpl.MessageState `protogen:"open.v1"`
Pid int32 `protobuf:"varint,1,opt,name=Pid,proto3" json:"Pid,omitempty"`
Ppid int32 `protobuf:"varint,2,opt,name=Ppid,proto3" json:"Ppid,omitempty"`
Executable string `protobuf:"bytes,3,opt,name=Executable,proto3" json:"Executable,omitempty"`
Owner string `protobuf:"bytes,4,opt,name=Owner,proto3" json:"Owner,omitempty"`
Architecture string `protobuf:"bytes,7,opt,name=Architecture,proto3" json:"Architecture,omitempty"`
SessionID int32 `protobuf:"varint,5,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
CmdLine []string `protobuf:"bytes,6,rep,name=CmdLine,proto3" json:"CmdLine,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Pid int32 `protobuf:"varint,1,opt,name=Pid,proto3" json:"Pid,omitempty"`
Ppid int32 `protobuf:"varint,2,opt,name=Ppid,proto3" json:"Ppid,omitempty"`
Executable string `protobuf:"bytes,3,opt,name=Executable,proto3" json:"Executable,omitempty"`
Owner string `protobuf:"bytes,4,opt,name=Owner,proto3" json:"Owner,omitempty"`
Architecture string `protobuf:"bytes,7,opt,name=Architecture,proto3" json:"Architecture,omitempty"`
SessionID int32 `protobuf:"varint,5,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
CmdLine []string `protobuf:"bytes,6,rep,name=CmdLine,proto3" json:"CmdLine,omitempty"`
}
func (x *Process) Reset() {
*x = Process{}
mi := &file_commonpb_common_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_commonpb_common_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Process) String() string {
@@ -279,7 +292,7 @@ func (*Process) ProtoMessage() {}
func (x *Process) ProtoReflect() protoreflect.Message {
mi := &file_commonpb_common_proto_msgTypes[4]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -345,18 +358,21 @@ func (x *Process) GetCmdLine() []string {
// EnvVar - Environment variable K/V
type EnvVar struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=Key,proto3" json:"Key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=Key,proto3" json:"Key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"`
}
func (x *EnvVar) Reset() {
*x = EnvVar{}
mi := &file_commonpb_common_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_commonpb_common_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *EnvVar) String() string {
@@ -367,7 +383,7 @@ func (*EnvVar) ProtoMessage() {}
func (x *EnvVar) ProtoReflect() protoreflect.Message {
mi := &file_commonpb_common_proto_msgTypes[5]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -453,7 +469,7 @@ func file_commonpb_common_proto_rawDescGZIP() []byte {
}
var file_commonpb_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_commonpb_common_proto_goTypes = []any{
var file_commonpb_common_proto_goTypes = []interface{}{
(*Empty)(nil), // 0: commonpb.Empty
(*Request)(nil), // 1: commonpb.Request
(*Response)(nil), // 2: commonpb.Response
@@ -474,6 +490,80 @@ func file_commonpb_common_proto_init() {
if File_commonpb_common_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_commonpb_common_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Empty); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_commonpb_common_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Request); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_commonpb_common_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Response); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_commonpb_common_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*File); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_commonpb_common_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Process); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_commonpb_common_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*EnvVar); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
+37 -18
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc v5.29.3
// protoc-gen-go v1.26.0
// protoc v6.33.2
// source: dnspb/dns.proto
package dnspb
@@ -87,27 +87,32 @@ func (DNSMessageType) EnumDescriptor() ([]byte, []int) {
return file_dnspb_dns_proto_rawDescGZIP(), []int{0}
}
// NOTE: DNS is very space sensitive so certain fields are re-purposed
// depending on the DNSMessageType as noted below:
//
// [Type TOTP]: ID field is used for the TOTP code
//NOTE: DNS is very space sensitive so certain fields are re-purposed
//depending on the DNSMessageType as noted below:
//
//[Type TOTP]: ID field is used for the TOTP code
//
type DNSMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type DNSMessageType `protobuf:"varint,1,opt,name=Type,proto3,enum=dnspb.DNSMessageType" json:"Type,omitempty"`
ID uint32 `protobuf:"varint,2,opt,name=ID,proto3" json:"ID,omitempty"` // 8 bit message id + 24 bit dns session ID
Start uint32 `protobuf:"varint,3,opt,name=Start,proto3" json:"Start,omitempty"` // Bytes start at
Stop uint32 `protobuf:"varint,4,opt,name=Stop,proto3" json:"Stop,omitempty"` // Bytes stop at
Size uint32 `protobuf:"varint,5,opt,name=Size,proto3" json:"Size,omitempty"` // Total size
Data []byte `protobuf:"bytes,6,opt,name=Data,proto3" json:"Data,omitempty"` // Actual data
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type DNSMessageType `protobuf:"varint,1,opt,name=Type,proto3,enum=dnspb.DNSMessageType" json:"Type,omitempty"`
ID uint32 `protobuf:"varint,2,opt,name=ID,proto3" json:"ID,omitempty"` // 8 bit message id + 24 bit dns session ID
Start uint32 `protobuf:"varint,3,opt,name=Start,proto3" json:"Start,omitempty"` // Bytes start at
Stop uint32 `protobuf:"varint,4,opt,name=Stop,proto3" json:"Stop,omitempty"` // Bytes stop at
Size uint32 `protobuf:"varint,5,opt,name=Size,proto3" json:"Size,omitempty"` // Total size
Data []byte `protobuf:"bytes,6,opt,name=Data,proto3" json:"Data,omitempty"` // Actual data
}
func (x *DNSMessage) Reset() {
*x = DNSMessage{}
mi := &file_dnspb_dns_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
if protoimpl.UnsafeEnabled {
mi := &file_dnspb_dns_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DNSMessage) String() string {
@@ -118,7 +123,7 @@ func (*DNSMessage) ProtoMessage() {}
func (x *DNSMessage) ProtoReflect() protoreflect.Message {
mi := &file_dnspb_dns_proto_msgTypes[0]
if x != nil {
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -218,7 +223,7 @@ func file_dnspb_dns_proto_rawDescGZIP() []byte {
var file_dnspb_dns_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_dnspb_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_dnspb_dns_proto_goTypes = []any{
var file_dnspb_dns_proto_goTypes = []interface{}{
(DNSMessageType)(0), // 0: dnspb.DNSMessageType
(*DNSMessage)(nil), // 1: dnspb.DNSMessage
}
@@ -236,6 +241,20 @@ func file_dnspb_dns_proto_init() {
if File_dnspb_dns_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_dnspb_dns_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DNSMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
+3 -3
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc v5.29.3
// protoc-gen-go v1.26.0
// protoc v6.33.2
// source: rpcpb/services.proto
package rpcpb
@@ -724,7 +724,7 @@ var file_rpcpb_services_proto_rawDesc = []byte{
0x70, 0x63, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var file_rpcpb_services_proto_goTypes = []any{
var file_rpcpb_services_proto_goTypes = []interface{}{
(*commonpb.Empty)(nil), // 0: commonpb.Empty
(*clientpb.ClientLogData)(nil), // 1: clientpb.ClientLogData
(*sliverpb.KillReq)(nil), // 2: sliverpb.KillReq
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -277,7 +277,7 @@ func SetupGoPath(goPathSrc string, includeDNS bool) error {
}
sliverpbGoSrc = xorPBRawBytes(sliverpbGoSrc)
sliverpbGoSrc = stripSliverpb(sliverpbGoSrc)
sliverpbDir := filepath.Join(goPathSrc, "github.com", "bishopfox", "sliver", "protobuf", "sliverpb")
sliverpbDir := filepath.Join(goPathSrc, "protobuf", "sliverpb")
setupLog.Infof("Creating sliverpb directory: %s", sliverpbDir)
os.MkdirAll(sliverpbDir, 0700)
os.WriteFile(filepath.Join(sliverpbDir, "sliver.pb.go"), sliverpbGoSrc, 0600)
@@ -290,7 +290,7 @@ func SetupGoPath(goPathSrc string, includeDNS bool) error {
return err
}
commonpbSrc = xorPBRawBytes(commonpbSrc)
commonpbDir := filepath.Join(goPathSrc, "github.com", "bishopfox", "sliver", "protobuf", "commonpb")
commonpbDir := filepath.Join(goPathSrc, "protobuf", "commonpb")
os.MkdirAll(commonpbDir, 0700)
os.WriteFile(filepath.Join(commonpbDir, "common.pb.go"), commonpbSrc, 0600)
@@ -302,7 +302,7 @@ func SetupGoPath(goPathSrc string, includeDNS bool) error {
return err
}
dnspbSrc = xorPBRawBytes(dnspbSrc)
dnspbDir := filepath.Join(goPathSrc, "github.com", "bishopfox", "sliver", "protobuf", "dnspb")
dnspbDir := filepath.Join(goPathSrc, "protobuf", "dnspb")
os.MkdirAll(dnspbDir, 0700)
os.WriteFile(filepath.Join(dnspbDir, "dns.pb.go"), dnspbSrc, 0600)
}
+5 -1
View File
@@ -69,6 +69,7 @@ var (
const (
DefaultMaxBodyLength = 2 * 1024 * 1024 * 1024 // 2Gb
DefaultMaxUnauthBodyLength = 8 * 1024 * 1024 // 8Mb
DefaultHTTPTimeout = time.Minute
DefaultLongPollTimeout = time.Second
DefaultLongPollJitter = time.Second
@@ -564,7 +565,10 @@ func (s *SliverHTTPC2) anonymousHandler(resp http.ResponseWriter, req *http.Requ
func (s *SliverHTTPC2) startSessionHandler(resp http.ResponseWriter, req *http.Request, encoder sliverEncoders.Encoder) {
httpLog.Debug("Start http session request")
body, err := io.ReadAll(req.Body)
body, err := io.ReadAll(&io.LimitedReader{
R: req.Body,
N: int64(DefaultMaxUnauthBodyLength),
})
if err != nil {
httpLog.Errorf("Failed to read body %s", err)
s.defaultHandler(resp, req)
+10 -1
View File
@@ -28,6 +28,7 @@ import (
"github.com/spf13/cobra"
clientcli "github.com/bishopfox/sliver/client/cli"
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/c2"
"github.com/bishopfox/sliver/server/certs"
@@ -72,6 +73,8 @@ func initConsoleLogging(appDir string) *os.File {
}
func init() {
rootCmd.Flags().String(clientcli.RCFlagName, "", "path to rc script file")
// Unpack
unpackCmd.Flags().BoolP(forceFlagStr, "f", false, "Force unpack and overwrite")
rootCmd.AddCommand(unpackCmd)
@@ -147,8 +150,14 @@ var rootCmd = &cobra.Command{
if serverConfig.DaemonMode {
daemon.Start(daemon.BlankHost, daemon.BlankPort, serverConfig.DaemonConfig.Tailscale)
} else {
rcScript, err := clientcli.ReadRCScript(cmd)
if err != nil {
fmt.Printf("Failed to read rc script: %s\n", err)
return
}
os.Args = os.Args[:1] // Hide cli from grumble console
console.Start()
console.Start(rcScript)
}
},
}
+2 -2
View File
@@ -38,7 +38,7 @@ import (
)
// Start - Starts the server console
func Start() {
func Start(rcScript string) {
_, ln, _ := transport.LocalListener()
ctxDialer := grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return ln.Dial()
@@ -57,7 +57,7 @@ func Start() {
defer conn.Close()
localRPC := rpcpb.NewSliverRPCClient(conn)
con := console.NewConsole(false)
console.StartClient(con, localRPC, command.ServerCommands(con, serverOnlyCmds), command.SliverCommands(con), true)
console.StartClient(con, localRPC, command.ServerCommands(con, serverOnlyCmds), command.SliverCommands(con), true, rcScript)
con.App.Start()
}
+34 -3
View File
@@ -40,9 +40,11 @@ import (
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/encoders"
"github.com/bishopfox/sliver/server/gogo"
"github.com/bishopfox/sliver/server/gogo/goname"
"github.com/bishopfox/sliver/server/log"
"github.com/bishopfox/sliver/util"
utilEncoders "github.com/bishopfox/sliver/util/encoders"
"golang.org/x/mod/module"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
@@ -360,7 +362,7 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
return "", err
}
sliverPkgDir := filepath.Join(srcDir, "github.com", "bishopfox", "sliver") // "main"
sliverPkgDir := filepath.Join(srcDir) // "main"
err = os.MkdirAll(sliverPkgDir, 0700)
if err != nil {
return "", nil
@@ -380,14 +382,14 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
sliverGoCode := string(sliverGoCodeRaw)
// Skip dllmain files for anything non windows
if f.Name() == "sliver.c" || f.Name() == "sliver.h" {
if f.Name() == "main.c" || f.Name() == "main.h" {
if !config.IsSharedLib && !config.IsShellcode {
return nil
}
}
var sliverCodePath string
if f.Name() == "sliver.go" || f.Name() == "sliver.c" || f.Name() == "sliver.h" {
if f.Name() == "main.go" || f.Name() == "main.c" || f.Name() == "main.h" {
sliverCodePath = filepath.Join(sliverPkgDir, f.Name())
} else {
sliverCodePath = filepath.Join(sliverPkgDir, "implant", fsPath)
@@ -521,6 +523,35 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
}
buildLog.Debugf("Created %s", goModPath)
if !config.Debug {
goPackage := "github.com/google/gvisor"
if config.GoPackage != "" {
err := module.CheckPath(config.GoPackage)
if err != nil {
buildLog.Warnf("Invalid Go package path '%s', using default '%s'", config.GoPackage, goPackage)
} else {
goPackage = config.GoPackage
}
}
modResult, err := goname.RenameModule(sliverPkgDir, goPackage)
if err != nil {
buildLog.Errorf("Failed to rename module: %s", err)
return "", err
}
buildLog.Infof("Renamed module: %s -> %s (go.mod updated: %v)", modResult.OldModule, modResult.NewModule, modResult.GoModUpdated)
// Rename Import Paths
buildLog.Infof("Renaming import paths ...")
oldImportPrefix := path.Join(modResult.NewModule, "implant", "sliver")
newImportPrefix := path.Join(modResult.NewModule, "runc", "cgroup")
importResult, err := goname.RenameImport(sliverPkgDir, oldImportPrefix, newImportPrefix)
if err != nil {
buildLog.Errorf("Failed to rename imports: %s", err)
return "", err
}
buildLog.Infof("Renamed imports (%d files updated)", importResult.FilesUpdated)
}
return sliverPkgDir, nil
}
+1
View File
@@ -183,6 +183,7 @@ func GoBuild(config GoConfig, src string, dest string, buildmode string, tags []
var goCommand = []string{"build"}
goCommand = append(goCommand, "-trimpath") // remove absolute paths from any compiled binary
goCommand = append(goCommand, "-mod=vendor")
if 0 < len(tags) {
goCommand = append(goCommand, "-tags")
+161
View File
@@ -0,0 +1,161 @@
package goname
import (
"bytes"
"errors"
"fmt"
"go/format"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"golang.org/x/mod/modfile"
)
// Result captures changes made by RenameModule.
type Result struct {
OldModule string
NewModule string
GoModUpdated bool
FilesUpdated int
ImportsUpdated int
}
// RenameModule updates go.mod and rewrites import paths within the module tree.
func RenameModule(dir, newModule string) (*Result, error) {
newModule = strings.TrimSpace(newModule)
if newModule == "" {
return nil, errors.New("new module path is required")
}
goModPath := filepath.Join(dir, "go.mod")
modBytes, err := os.ReadFile(goModPath)
if err != nil {
return nil, fmt.Errorf("read go.mod: %w", err)
}
modFile, err := modfile.Parse(goModPath, modBytes, nil)
if err != nil {
return nil, fmt.Errorf("parse go.mod: %w", err)
}
if modFile.Module == nil || modFile.Module.Mod.Path == "" {
return nil, errors.New("go.mod is missing a module path")
}
oldModule := modFile.Module.Mod.Path
result := &Result{
OldModule: oldModule,
NewModule: newModule,
}
if oldModule == newModule {
return result, nil
}
if err := modFile.AddModuleStmt(newModule); err != nil {
return result, fmt.Errorf("update module path: %w", err)
}
formatted, err := modFile.Format()
if err != nil {
return result, fmt.Errorf("format go.mod: %w", err)
}
if err := writeFilePreservePerm(goModPath, formatted); err != nil {
return result, fmt.Errorf("write go.mod: %w", err)
}
result.GoModUpdated = true
if err := rewriteImports(dir, oldModule, newModule, result); err != nil {
return result, err
}
return result, nil
}
func rewriteImports(dir, oldModule, newModule string, result *Result) error {
return filepath.WalkDir(dir, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
name := entry.Name()
if name == "vendor" || name == ".git" {
return fs.SkipDir
}
return nil
}
if filepath.Ext(path) != ".go" {
return nil
}
changed, importsUpdated, err := rewriteFileImports(path, oldModule, newModule)
if err != nil {
return err
}
if changed {
result.FilesUpdated++
result.ImportsUpdated += importsUpdated
}
return nil
})
}
func rewriteFileImports(path, oldModule, newModule string) (bool, int, error) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return false, 0, fmt.Errorf("parse %s: %w", path, err)
}
changed := false
importsUpdated := 0
for _, imp := range file.Imports {
importPath, err := strconv.Unquote(imp.Path.Value)
if err != nil {
return false, 0, fmt.Errorf("parse import in %s: %w", path, err)
}
if !matchesModule(importPath, oldModule) {
continue
}
newPath := newModule + strings.TrimPrefix(importPath, oldModule)
if newPath == importPath {
continue
}
imp.Path.Value = strconv.Quote(newPath)
changed = true
importsUpdated++
}
if !changed {
return false, 0, nil
}
var buf bytes.Buffer
if err := format.Node(&buf, fset, file); err != nil {
return false, 0, fmt.Errorf("format %s: %w", path, err)
}
if err := writeFilePreservePerm(path, buf.Bytes()); err != nil {
return false, 0, fmt.Errorf("write %s: %w", path, err)
}
return true, importsUpdated, nil
}
func matchesModule(importPath, modulePath string) bool {
return importPath == modulePath || strings.HasPrefix(importPath, modulePath+"/")
}
func writeFilePreservePerm(path string, data []byte) error {
perm := fs.FileMode(0644)
if info, err := os.Stat(path); err == nil {
perm = info.Mode().Perm()
}
return os.WriteFile(path, data, perm)
}
+139
View File
@@ -0,0 +1,139 @@
package goname
import (
"go/parser"
"go/token"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"golang.org/x/mod/modfile"
)
func TestRenameModule(t *testing.T) {
fixture := filepath.Join("testdata", "basic")
tmp := t.TempDir()
if err := copyDir(fixture, tmp); err != nil {
t.Fatalf("copy fixture: %v", err)
}
result, err := RenameModule(tmp, "example.net/new")
if err != nil {
t.Fatalf("RenameModule: %v", err)
}
if result.OldModule != "example.com/old" {
t.Fatalf("old module mismatch: %s", result.OldModule)
}
if result.NewModule != "example.net/new" {
t.Fatalf("new module mismatch: %s", result.NewModule)
}
if !result.GoModUpdated {
t.Fatalf("expected go.mod update")
}
if result.FilesUpdated != 2 {
t.Fatalf("files updated mismatch: %d", result.FilesUpdated)
}
if result.ImportsUpdated != 3 {
t.Fatalf("imports updated mismatch: %d", result.ImportsUpdated)
}
modBytes, err := os.ReadFile(filepath.Join(tmp, "go.mod"))
if err != nil {
t.Fatalf("read go.mod: %v", err)
}
modFile, err := modfile.Parse("go.mod", modBytes, nil)
if err != nil {
t.Fatalf("parse go.mod: %v", err)
}
if modFile.Module == nil || modFile.Module.Mod.Path != "example.net/new" {
t.Fatalf("module path not updated: %#v", modFile.Module)
}
mainImports := importPaths(t, filepath.Join(tmp, "main.go"))
for _, want := range []string{"example.net/new", "example.net/new/pkg/foo", "example.com/oldish/pkg"} {
if !mainImports[want] {
t.Fatalf("main.go missing import: %s", want)
}
}
if mainImports["example.com/old"] || mainImports["example.com/old/pkg/foo"] {
t.Fatalf("main.go still references old module")
}
fooImports := importPaths(t, filepath.Join(tmp, "pkg", "foo", "foo.go"))
if !fooImports["example.net/new/pkg/bar"] {
t.Fatalf("foo.go import not updated")
}
vendorImports := importPaths(t, filepath.Join(tmp, "vendor", "example.com", "old", "vendor.go"))
if !vendorImports["example.com/old/pkg/foo"] {
t.Fatalf("vendor import should remain unchanged")
}
mainBytes, err := os.ReadFile(filepath.Join(tmp, "main.go"))
if err != nil {
t.Fatalf("read main.go: %v", err)
}
if !strings.Contains(string(mainBytes), "const example = \"example.com/old/pkg/foo\"") {
t.Fatalf("string literal should remain unchanged")
}
}
func TestRenameModuleMissingGoMod(t *testing.T) {
_, err := RenameModule(t.TempDir(), "example.com/new")
if err == nil {
t.Fatalf("expected error for missing go.mod")
}
}
func importPaths(t *testing.T, path string) map[string]bool {
t.Helper()
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", path, err)
}
imports := make(map[string]bool)
for _, imp := range file.Imports {
pathValue, err := strconv.Unquote(imp.Path.Value)
if err != nil {
t.Fatalf("unquote import in %s: %v", path, err)
}
imports[pathValue] = true
}
return imports
}
func copyDir(src, dst string) error {
return filepath.WalkDir(src, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
target := filepath.Join(dst, rel)
if entry.IsDir() {
info, err := entry.Info()
if err != nil {
return err
}
return os.MkdirAll(target, info.Mode().Perm())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
info, err := entry.Info()
if err != nil {
return err
}
return os.WriteFile(target, data, info.Mode().Perm())
})
}
+134
View File
@@ -0,0 +1,134 @@
package goname
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
)
// ImportResult captures changes made by RenameImport.
type ImportResult struct {
OldImportPrefix string
NewImportPrefix string
FilesUpdated int
ImportsUpdated int
}
// RenameImport rewrites import paths that match oldImportPrefix within the directory tree.
func RenameImport(dir, oldImportPrefix, newImportPrefix string) (*ImportResult, error) {
oldImportPrefix = strings.TrimSpace(oldImportPrefix)
newImportPrefix = strings.TrimSpace(newImportPrefix)
if oldImportPrefix == "" {
return nil, errors.New("old import prefix is required")
}
if newImportPrefix == "" {
return nil, errors.New("new import prefix is required")
}
result := &ImportResult{
OldImportPrefix: oldImportPrefix,
NewImportPrefix: newImportPrefix,
}
if err := rewriteImportPrefixes(dir, oldImportPrefix, newImportPrefix, result); err != nil {
return result, err
}
if result.ImportsUpdated == 0 {
return result, errors.New("no imports updated (check old/new import prefixes)")
}
if err := renameImportDir(dir, oldImportPrefix, newImportPrefix); err != nil {
return result, err
}
return result, nil
}
func rewriteImportPrefixes(dir, oldImportPrefix, newImportPrefix string, result *ImportResult) error {
return filepath.WalkDir(dir, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
name := entry.Name()
if name == "vendor" || strings.HasPrefix(name, ".") {
return fs.SkipDir
}
return nil
}
if filepath.Ext(path) != ".go" {
return nil
}
changed, importsUpdated, err := rewriteFileImports(path, oldImportPrefix, newImportPrefix)
if err != nil {
return err
}
if changed {
result.FilesUpdated++
result.ImportsUpdated += importsUpdated
}
return nil
})
}
func renameImportDir(dir, oldImportPrefix, newImportPrefix string) error {
oldParts := splitImportPrefix(oldImportPrefix)
newParts := splitImportPrefix(newImportPrefix)
if len(oldParts) == 0 || len(newParts) == 0 {
return nil
}
for i := 0; i < len(oldParts); i++ {
suffixParts := oldParts[i:]
oldDir := filepath.Join(append([]string{dir}, suffixParts...)...)
info, err := os.Stat(oldDir)
if err != nil {
if os.IsNotExist(err) {
continue
}
return err
}
if !info.IsDir() {
continue
}
if len(newParts) < len(suffixParts) {
return fmt.Errorf("new import prefix has fewer path segments than old import prefix")
}
newSuffixParts := newParts[len(newParts)-len(suffixParts):]
newDir := filepath.Join(append([]string{dir}, newSuffixParts...)...)
if oldDir == newDir {
return nil
}
if _, err := os.Stat(newDir); err == nil {
return fmt.Errorf("rename import dir: target exists: %s", newDir)
} else if !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(filepath.Dir(newDir), 0700); err != nil {
return err
}
return os.Rename(oldDir, newDir)
}
return nil
}
func splitImportPrefix(prefix string) []string {
cleaned := path.Clean(strings.TrimSpace(prefix))
if cleaned == "." || cleaned == "/" {
return nil
}
cleaned = strings.TrimPrefix(cleaned, "/")
if cleaned == "" {
return nil
}
return strings.Split(cleaned, "/")
}
+95
View File
@@ -0,0 +1,95 @@
package goname
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestRenameImportBasic(t *testing.T) {
fixture := filepath.Join("testdata", "rename-import", "basic")
tmp := t.TempDir()
if err := copyDir(fixture, tmp); err != nil {
t.Fatalf("copy fixture: %v", err)
}
result, err := RenameImport(tmp, "foo/bar", "acme/corp")
if err != nil {
t.Fatalf("RenameImport: %v", err)
}
if result.OldImportPrefix != "foo/bar" {
t.Fatalf("old prefix mismatch: %s", result.OldImportPrefix)
}
if result.NewImportPrefix != "acme/corp" {
t.Fatalf("new prefix mismatch: %s", result.NewImportPrefix)
}
if result.FilesUpdated != 2 {
t.Fatalf("files updated mismatch: %d", result.FilesUpdated)
}
if result.ImportsUpdated != 4 {
t.Fatalf("imports updated mismatch: %d", result.ImportsUpdated)
}
mainImports := importPaths(t, filepath.Join(tmp, "main.go"))
for _, want := range []string{"acme/corp", "acme/corp/baz", "acme/corp/qux", "foo/barista", "other/pkg"} {
if !mainImports[want] {
t.Fatalf("main.go missing import: %s", want)
}
}
if mainImports["foo/bar"] || mainImports["foo/bar/baz"] || mainImports["foo/bar/qux"] {
t.Fatalf("main.go still references old prefix")
}
barImports := importPaths(t, filepath.Join(tmp, "acme", "corp", "bar.go"))
if !barImports["acme/corp/baz"] {
t.Fatalf("bar.go import not updated")
}
if _, err := os.Stat(filepath.Join(tmp, "foo", "bar")); !os.IsNotExist(err) {
t.Fatalf("expected foo/bar to be renamed")
}
vendorImports := importPaths(t, filepath.Join(tmp, "vendor", "foo", "bar", "vendor.go"))
if !vendorImports["foo/bar/baz"] {
t.Fatalf("vendor import should remain unchanged")
}
hiddenImports := importPaths(t, filepath.Join(tmp, ".hidden", "hidden.go"))
if !hiddenImports["foo/bar/baz"] {
t.Fatalf("hidden import should remain unchanged")
}
mainBytes, err := os.ReadFile(filepath.Join(tmp, "main.go"))
if err != nil {
t.Fatalf("read main.go: %v", err)
}
if !strings.Contains(string(mainBytes), "const example = \"foo/bar/baz\"") {
t.Fatalf("string literal should remain unchanged")
}
}
func TestRenameImportNoChanges(t *testing.T) {
fixture := filepath.Join("testdata", "rename-import", "basic")
tmp := t.TempDir()
if err := copyDir(fixture, tmp); err != nil {
t.Fatalf("copy fixture: %v", err)
}
_, err := RenameImport(tmp, "missing/prefix", "acme/corp")
if err == nil {
t.Fatalf("expected error for missing prefix")
}
}
func TestRenameImportInvalidArgs(t *testing.T) {
_, err := RenameImport(t.TempDir(), "", "acme/corp")
if err == nil {
t.Fatalf("expected error for empty old prefix")
}
_, err = RenameImport(t.TempDir(), "foo/bar", "")
if err == nil {
t.Fatalf("expected error for empty new prefix")
}
}
+5
View File
@@ -0,0 +1,5 @@
module example.com/old
go 1.22
require example.com/other v1.2.3
+14
View File
@@ -0,0 +1,14 @@
package main
import (
"fmt"
"example.com/old"
oldpkg "example.com/old/pkg/foo"
_ "example.com/oldish/pkg"
)
const example = "example.com/old/pkg/foo"
func main() {
fmt.Println(oldpkg.Name, example)
}
+3
View File
@@ -0,0 +1,3 @@
package bar
const Name = "bar"
+5
View File
@@ -0,0 +1,5 @@
package foo
import "example.com/old/pkg/bar"
const Name = bar.Name
+5
View File
@@ -0,0 +1,5 @@
package vendor
import "example.com/old/pkg/foo"
const Vendor = foo.Name
@@ -0,0 +1,3 @@
package hidden
import _ "foo/bar/baz"
@@ -0,0 +1,9 @@
package bar
import "foo/bar/baz"
const Name = "bar"
func Use() string {
return baz.Name
}
@@ -0,0 +1,3 @@
package baz
const Name = "baz"
@@ -0,0 +1,3 @@
package qux
const Name = "qux"
@@ -0,0 +1,3 @@
package barista
const Name = "barista"
@@ -0,0 +1,3 @@
module example.com/demo
go 1.22
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"fmt"
bar "foo/bar"
_ "foo/bar/baz"
_ "foo/bar/qux"
_ "foo/barista"
"other/pkg"
)
const example = "foo/bar/baz"
func main() {
fmt.Println(bar.Name, example)
}
@@ -0,0 +1,3 @@
package vendor
import _ "foo/bar/baz"
+650
View File
@@ -0,0 +1,650 @@
# Examples
All the examples below are going to start with the following block, although
nothing except a single Row is mandatory for the `Render()` function to render
something:
```golang
package main
import (
"os"
"github.com/jedib0t/go-pretty/v6/table"
)
func main() {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
t.AppendRows([]table.Row{
{1, "Arya", "Stark", 3000},
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
})
t.AppendSeparator()
t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000})
t.AppendFooter(table.Row{"", "", "Total", 10000})
t.Render()
}
```
Running the above will result in:
```
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 1 | Arya | Stark | 3000 | |
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
+-----+------------+-----------+--------+-----------------------------+
| 300 | Tyrion | Lannister | 5000 | |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
```
---
<details>
<summary><strong>Ready-to-use Styles</strong></summary>
Table comes with a bunch of ready-to-use Styles that make the table look really
good. Set or Change the style using:
```golang
t.SetStyle(table.StyleLight)
t.Render()
```
to get:
```
┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
│ # │ FIRST NAME │ LAST NAME │ SALARY │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ 1 │ Arya │ Stark │ 3000 │ │
│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ 300 │ Tyrion │ Lannister │ 5000 │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ │ │ TOTAL │ 10000 │ │
└─────┴────────────┴───────────┴────────┴─────────────────────────────┘
```
Or if you want to use a full-color mode, and don't care for boxes, use:
```golang
t.SetStyle(table.StyleColoredBright)
t.Render()
```
to get:
<img src="images/table-StyleColoredBright.png" width="640px" alt="Colored Table"/>
</details>
---
<details>
<summary><strong>Roll your own Style</strong></summary>
You can also roll your own style:
```golang
t.SetStyle(table.Style{
Name: "myNewStyle",
Box: table.BoxStyle{
BottomLeft: "\\",
BottomRight: "/",
BottomSeparator: "v",
Left: "[",
LeftSeparator: "{",
MiddleHorizontal: "-",
MiddleSeparator: "+",
MiddleVertical: "|",
PaddingLeft: "<",
PaddingRight: ">",
Right: "]",
RightSeparator: "}",
TopLeft: "(",
TopRight: ")",
TopSeparator: "^",
UnfinishedRow: " ~~~",
},
Color: table.ColorOptions{
IndexColumn: text.Colors{text.BgCyan, text.FgBlack},
Footer: text.Colors{text.BgCyan, text.FgBlack},
Header: text.Colors{text.BgHiCyan, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
},
Format: table.FormatOptions{
Footer: text.FormatUpper,
Header: text.FormatUpper,
Row: text.FormatDefault,
},
Options: table.Options{
DrawBorder: true,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
},
})
```
Or you can use one of the ready-to-use Styles, and just make a few tweaks:
```golang
t.SetStyle(table.StyleLight)
t.Style().Color.Header = text.Colors{text.BgHiCyan, text.FgBlack}
t.Style().Color.IndexColumn = text.Colors{text.BgHiCyan, text.FgBlack}
t.Style().Format.Footer = text.FormatLower
t.Style().Options.DrawBorder = false
```
</details>
---
<details>
<summary><strong>Customize Horizontal Separators</strong></summary>
For more granular control over horizontal lines in your table, you can use `BoxStyleHorizontal` to customize different separator types independently. This allows you to have different horizontal line styles for titles, headers, rows, and footers.
The `BoxStyleHorizontal` struct provides 10 customizable separator types:
- `TitleTop` / `TitleBottom` - Lines above/below the title
- `HeaderTop` / `HeaderMiddle` / `HeaderBottom` - Lines in the header section
- `RowTop` / `RowMiddle` / `RowBottom` - Lines in the data rows section
- `FooterTop` / `FooterMiddle` / `FooterBottom` - Lines in the footer section
You can customize each separator type using:
```golang
tw := table.NewWriter()
tw.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
tw.AppendRows([]table.Row{
{1, "Arya", "Stark", 3000},
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
{300, "Tyrion", "Lannister", 5000},
})
tw.AppendFooter(table.Row{"", "", "Total", 10000})
tw.SetStyle(table.StyleDefault)
tw.Style().Box.Horizontal = &table.BoxStyleHorizontal{
HeaderTop: "=", // Thicker line above header
HeaderMiddle: "-",
HeaderBottom: "~", // Thicker line below header
RowTop: "-",
RowMiddle: "- ",
RowBottom: "-",
FooterTop: "~", // Thicker line above footer
FooterMiddle: "-",
FooterBottom: "=", // Thicker line below footer
}
tw.Style().Options.SeparateRows = true
fmt.Println(tw.Render())
```
to get something like:
```
+=====+============+===========+========+=============================+
| # | FIRST NAME | LAST NAME | SALARY | |
+~~~~~+~~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| 1 | Arya | Stark | 3000 | |
+- - -+- - - - - - +- - - - - -+- - - - +- - - - - - - - - - - - - - -+
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
+- - -+- - - - - - +- - - - - -+- - - - +- - - - - - - - - - - - - - -+
| 300 | Tyrion | Lannister | 5000 | |
+~~~~~+~~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| | | TOTAL | 10000 | |
+=====+============+===========+========+=============================+
```
When `BoxStyle.Horizontal` is set to a non-nil value, it overrides the `MiddleHorizontal` string for all horizontal separators. If `Horizontal` is nil, the table will automatically use `MiddleHorizontal` for all separator types.
</details>
---
<details>
<summary><strong>Auto-Merge</strong></summary>
You can auto-merge cells horizontally and vertically, but you have request for
it specifically for each row/column using `RowConfig` or `ColumnConfig`.
```golang
rowConfigAutoMerge := table.RowConfig{AutoMerge: true}
t := table.NewWriter()
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge)
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "EXE", "RUN"})
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge)
t.AppendFooter(table.Row{"", "", "", 7, 5, 3})
t.SetAutoIndex(true)
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
{Number: 2, AutoMerge: true},
{Number: 3, AutoMerge: true},
{Number: 4, AutoMerge: true},
{Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
{Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
})
t.SetStyle(table.StyleLight)
t.Style().Options.SeparateRows = true
fmt.Println(t.Render())
```
to get:
```
┌───┬─────────┬────────┬───────────┬───────────┬───────────┐
│ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │
│ │ │ │ │ ├─────┬─────┤
│ │ │ │ │ │ EXE │ RUN │
├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤
│ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │
├───┤ │ │ ├───────────┼─────┬─────┤
│ 2 │ │ │ │ C 2 │ Y │ N │
├───┤ │ ├───────────┼───────────┼─────┴─────┤
│ 3 │ │ │ NS 1B │ C 3 │ N │
├───┤ ├────────┼───────────┼───────────┼───────────┤
│ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │
├───┤ │ │ ├───────────┼─────┬─────┤
│ 5 │ │ │ │ C 5 │ Y │ N │
├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤
│ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │
├───┤ │ │ ├───────────┼───────────┤
│ 7 │ │ │ │ C 7 │ Y │
├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤
│ │ │ │ │ 7 │ 5 │ 3 │
└───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘
```
</details>
---
<details>
<summary><strong>Paging</strong></summary>
You can limit the number of lines rendered in a single "Page". This logic
can handle rows with multiple lines too. The recommended way is to use the
`Pager()` interface:
```golang
pager := t.Pager(PageSize(1))
pager.Render() // Render first page
pager.Next() // Move to next page and render
pager.Prev() // Move to previous page and render
pager.GoTo(3) // Jump to page 3
pager.Location() // Get current page number
```
Or use the deprecated `SetPageSize()` method for simple cases:
```golang
t.SetPageSize(1)
t.Render()
```
to get:
```
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 1 | Arya | Stark | 3000 | |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 300 | Tyrion | Lannister | 5000 | |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
```
</details>
---
<details>
<summary><strong>Filtering</strong></summary>
Filtering can be done on one or more columns. All filters are applied with AND logic (all must match).
Filters are applied before sorting.
```golang
t.FilterBy([]table.FilterBy{
{Name: "Salary", Operator: table.GreaterThan, Value: 2000},
{Name: "First Name", Operator: table.Contains, Value: "on"},
})
```
The `Operator` field in `FilterBy` supports various filtering operators:
- `Equal` / `NotEqual` - Exact match
- `GreaterThan` / `GreaterThanOrEqual` - Numeric comparisons
- `LessThan` / `LessThanOrEqual` - Numeric comparisons
- `Contains` / `NotContains` - String search
- `StartsWith` / `EndsWith` - String prefix/suffix matching
- `RegexMatch` / `RegexNotMatch` - Regular expression matching
You can make string comparisons case-insensitive by setting `IgnoreCase: true`:
```golang
t.FilterBy([]table.FilterBy{
{Name: "First Name", Operator: table.Equal, Value: "JON", IgnoreCase: true},
})
```
For advanced filtering requirements, you can provide a custom filter function:
```golang
t.FilterBy([]table.FilterBy{
{
Number: 2,
CustomFilter: func(cellValue string) bool {
// Custom logic: include rows where first name length > 3
return len(cellValue) > 3
},
},
})
```
Example: Filter by salary and name
```golang
t := table.NewWriter()
t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
t.AppendRows([]table.Row{
{1, "Arya", "Stark", 3000},
{20, "Jon", "Snow", 2000},
{300, "Tyrion", "Lannister", 5000},
{400, "Sansa", "Stark", 2500},
})
t.FilterBy([]table.FilterBy{
{Number: 4, Operator: table.GreaterThan, Value: 2000},
{Number: 3, Operator: table.Contains, Value: "Stark"},
})
t.Render()
```
to get:
```
+-----+------------+-----------+--------+
| # | FIRST NAME | LAST NAME | SALARY |
+-----+------------+-----------+--------+
| 1 | Arya | Stark | 3000 |
| 400 | Sansa | Stark | 2500 |
+-----+------------+-----------+--------+
```
</details>
---
<details>
<summary><strong>Sorting</strong></summary>
Sorting can be done on one or more columns. The following code will make the
rows be sorted first by "First Name" and then by "Last Name" (in case of similar
"First Name" entries).
```golang
t.SortBy([]table.SortBy{
{Name: "First Name", Mode: table.Asc},
{Name: "Last Name", Mode: table.Asc},
})
```
The `Mode` field in `SortBy` supports various sorting modes:
- `Asc` / `Dsc` - Alphabetical ascending/descending
- `AscNumeric` / `DscNumeric` - Numerical ascending/descending
- `AscAlphaNumeric` / `DscAlphaNumeric` - Alphabetical first, then numerical
- `AscNumericAlpha` / `DscNumericAlpha` - Numerical first, then alphabetical
You can also make sorting case-insensitive by setting `IgnoreCase: true`:
```golang
t.SortBy([]table.SortBy{
{Name: "First Name", Mode: table.Asc, IgnoreCase: true},
})
```
</details>
---
<details>
<summary><strong>Sorting Customization</strong></summary>
For advanced sorting requirements, you can provide a custom comparison function
using `CustomLess`. This function overrides the `Mode` and `IgnoreCase` settings
and gives you full control over the sorting logic.
The `CustomLess` function receives two string values (the cell contents converted
to strings) and must return:
- `-1` when the first value should come before the second
- `0` when the values are considered equal (sorting continues to the next column)
- `1` when the first value should come after the second
Example: Custom numeric sorting that handles string numbers correctly.
```golang
t.SortBy([]table.SortBy{
{
Number: 1,
CustomLess: func(iStr string, jStr string) int {
iNum, iErr := strconv.Atoi(iStr)
jNum, jErr := strconv.Atoi(jStr)
if iErr != nil || jErr != nil {
// Fallback to string comparison if not numeric
if iStr < jStr {
return -1
}
if iStr > jStr {
return 1
}
return 0
}
if iNum < jNum {
return -1
}
if iNum > jNum {
return 1
}
return 0
},
},
})
```
Example: Combining custom sorting with default sorting modes.
```golang
t.SortBy([]table.SortBy{
{
Number: 1,
CustomLess: func(iStr string, jStr string) int {
// Custom logic: "same" values come first
if iStr == "same" && jStr != "same" {
return -1
}
if iStr != "same" && jStr == "same" {
return 1
}
return 0 // Equal, continue to next column
},
},
{Number: 2, Mode: table.Asc}, // Default alphabetical sort
{Number: 3, Mode: table.AscNumeric}, // Default numeric sort
})
```
</details>
---
<details>
<summary><strong>Wrapping (or) Row/Column Width restrictions</strong></summary>
You can restrict the maximum (text) width for a Row:
```golang
t.SetAllowedRowLength(50)
t.Render()
```
to get:
```
+-----+------------+-----------+--------+------- ~
| # | FIRST NAME | LAST NAME | SALARY | ~
+-----+------------+-----------+--------+------- ~
| 1 | Arya | Stark | 3000 | ~
| 20 | Jon | Snow | 2000 | You kn ~
+-----+------------+-----------+--------+------- ~
| 300 | Tyrion | Lannister | 5000 | ~
+-----+------------+-----------+--------+------- ~
| | | TOTAL | 10000 | ~
+-----+------------+-----------+--------+------- ~
```
</details>
---
<details>
<summary><strong>Column Control - Alignment, Colors, Width and more</strong></summary>
You can control a lot of things about individual cells/columns which overrides
global properties/styles using the `SetColumnConfig()` interface:
- Alignment (horizontal & vertical)
- Colorization
- Transform individual cells based on the content
- Visibility
- Width (minimum & maximum)
```golang
nameTransformer := text.Transformer(func(val interface{}) string {
return text.Bold.Sprint(val)
})
t.SetColumnConfigs([]ColumnConfig{
{
Name: "First Name",
Align: text.AlignLeft,
AlignFooter: text.AlignLeft,
AlignHeader: text.AlignLeft,
Colors: text.Colors{text.BgBlack, text.FgRed},
ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold},
ColorsFooter: text.Colors{text.BgRed, text.FgBlack},
Hidden: false,
Transformer: nameTransformer,
TransformerFooter: nameTransformer,
TransformerHeader: nameTransformer,
VAlign: text.VAlignMiddle,
VAlignFooter: text.VAlignTop,
VAlignHeader: text.VAlignBottom,
WidthMin: 6,
WidthMax: 64,
}
})
```
</details>
---
<details>
<summary><strong>CSV</strong></summary>
```golang
t.RenderCSV()
```
to get:
```
,First Name,Last Name,Salary,
1,Arya,Stark,3000,
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
300,Tyrion,Lannister,5000,
,,Total,10000,
```
</details>
---
<details>
<summary><strong>HTML Table</strong></summary>
```golang
t.Style().HTML = table.HTMLOptions{
CSSClass: "game-of-thrones",
EmptyColumn: "&nbsp;",
EscapeText: true,
Newline: "<br/>",
ConvertColorsToSpans: true, // Convert ANSI escape sequences to HTML <span> tags with CSS classes
}
t.RenderHTML()
```
to get:
```html
<table class="game-of-thrones">
<thead>
<tr>
<th align="right">#</th>
<th>First Name</th>
<th>Last Name</th>
<th align="right">Salary</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">1</td>
<td>Arya</td>
<td>Stark</td>
<td align="right">3000</td>
<td>&nbsp;</td>
</tr>
<tr>
<td align="right">20</td>
<td>Jon</td>
<td>Snow</td>
<td align="right">2000</td>
<td>You know nothing, Jon Snow!</td>
</tr>
<tr>
<td align="right">300</td>
<td>Tyrion</td>
<td>Lannister</td>
<td align="right">5000</td>
<td>&nbsp;</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align="right">&nbsp;</td>
<td>&nbsp;</td>
<td>Total</td>
<td align="right">10000</td>
<td>&nbsp;</td>
</tr>
</tfoot>
</table>
```
</details>
---
<details>
<summary><strong>Markdown Table</strong></summary>
```golang
t.RenderMarkdown()
```
to get:
```markdown
| # | First Name | Last Name | Salary | |
| ---:| --- | --- | ---:| --- |
| 1 | Arya | Stark | 3000 | |
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
| 300 | Tyrion | Lannister | 5000 | |
| | | Total | 10000 | |
```
</details>
---
+101 -412
View File
@@ -3,42 +3,7 @@
Pretty-print tables into ASCII/Unicode strings.
- Add Rows one-by-one or as a group (`AppendRow`/`AppendRows`)
- Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`)
- Add a Separator manually after any Row (`AppendSeparator`)
- Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`)
- Auto Merge (_not supported in CSV/Markdown/TSV modes_)
- Cells in a Row (`RowConfig.AutoMerge`)
- Columns (`ColumnConfig.AutoMerge`) (_not supported in HTML mode_)
- Limit the length of
- Rows (`SetAllowedRowLength`)
- Columns (`ColumnConfig.Width*`)
- Auto-size Rows (`Style().Size.WidthMin` and `Style().Size.WidthMax`)
- Page results by a specified number of Lines (`SetPageSize`)
- Alignment - Horizontal & Vertical
- Auto (horizontal) Align (numeric columns aligned Right)
- Custom (horizontal) Align per column (`ColumnConfig.Align*`)
- Custom (vertical) VAlign per column with multi-line cell support (`ColumnConfig.VAlign*`)
- Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`)
- Sort by one or more Columns (`SortBy`)
- Suppress/hide columns with no content (`SuppressEmptyColumns`)
- Suppress trailing spaces in the last column (`SupressTrailingSpaces`)
- Customizable Cell rendering per Column (`ColumnConfig.Transformer*`)
- Hide any columns that you don't want displayed (`ColumnConfig.Hidden`)
- Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`)
- Completely customizable styles (`SetStyle`/`Style`)
- Many ready-to-use styles: [style.go](style.go)
- Colorize Headers/Body/Footers using [../text/color.go](../text/color.go)
- Custom text-case for Headers/Body/Footers
- Enable separators between each row
- Render table without a Border
- and a lot more...
- Render as:
- (ASCII/Unicode) Table
- CSV
- HTML Table (with custom CSS Class)
- Markdown Table
- TSV
## Sample Table Rendering
```
+---------------------------------------------------------------------+
@@ -57,396 +22,120 @@ Pretty-print tables into ASCII/Unicode strings.
A demonstration of all the capabilities can be found here:
[../cmd/demo-table](../cmd/demo-table)
If you want very specific examples, read ahead.
If you want very specific examples, look at the [EXAMPLES.md](EXAMPLES.md) file.
**Hint**: I've tried to ensure that almost all supported use-cases are covered
by unit-tests and that they print the table rendered. Run
`go test -v github.com/jedib0t/go-pretty/v6/table` to see the test outputs and
help you figure out how to do something.
## Features
# Examples
### Core Table Building
All the examples below are going to start with the following block, although
nothing except a single Row is mandatory for the `Render()` function to render
something:
```golang
package main
- Add Rows one-by-one or as a group (`AppendRow`/`AppendRows`)
- Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`)
- Add a Separator manually after any Row (`AppendSeparator`)
- Add Title above the table (`SetTitle`)
- Add Caption below the table (`SetCaption`)
- Import 1D or 2D arrays/grids as rows (`ImportGrid`)
- Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`)
import (
"os"
### Indexing & Navigation
"github.com/jedib0t/go-pretty/v6/table"
)
- Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`)
- Set which column is the index column (`SetIndexColumn`)
- Pager interface for navigating through paged output (`Pager()`)
- `GoTo(pageNum)` - Jump to specific page
- `Next()` - Move to next page
- `Prev()` - Move to previous page
- `Location()` - Get current page number
- `Render()` - Render current page
- `SetOutputMirror()` - Mirror output to io.Writer
func main() {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
t.AppendRows([]table.Row{
{1, "Arya", "Stark", 3000},
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
})
t.AppendSeparator()
t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000})
t.AppendFooter(table.Row{"", "", "Total", 10000})
t.Render()
}
```
Running the above will result in:
```
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 1 | Arya | Stark | 3000 | |
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
+-----+------------+-----------+--------+-----------------------------+
| 300 | Tyrion | Lannister | 5000 | |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
```
### Auto Merge
## Styles
- Auto Merge cells (_not supported in CSV/Markdown/TSV modes_)
- Cells in a Row (`RowConfig.AutoMerge`)
- Columns (`ColumnConfig.AutoMerge`) (_not supported in HTML mode_)
- Custom alignment for merged cells (`RowConfig.AutoMergeAlign`)
You can customize almost every single thing about the table above. The previous
example just defaulted to `StyleDefault` during `Render()`. You can use a
ready-to-use style (as in [style.go](style.go)) or customize it as you want.
### Size & Width Control
### Ready-to-use Styles
- Limit the length of Rows (`SetAllowedRowLength` or `Style().Size.WidthMax`)
- Auto-size Rows (`Style().Size.WidthMin` and `Style().Size.WidthMax`)
- Column width control (`ColumnConfig.WidthMin` and `ColumnConfig.WidthMax`)
- Custom width enforcement functions (`ColumnConfig.WidthMaxEnforcer`)
- Default: `text.WrapText`
- Options: `text.WrapSoft`, `text.WrapHard`, `text.Trim`, or custom function
Table comes with a bunch of ready-to-use Styles that make the table look really
good. Set or Change the style using:
```golang
t.SetStyle(table.StyleLight)
t.Render()
```
to get:
```
┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
│ # │ FIRST NAME │ LAST NAME │ SALARY │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ 1 │ Arya │ Stark │ 3000 │ │
│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ 300 │ Tyrion │ Lannister │ 5000 │ │
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
│ │ │ TOTAL │ 10000 │ │
└─────┴────────────┴───────────┴────────┴─────────────────────────────┘
```
### Alignment
Or if you want to use a full-color mode, and don't care for boxes, use:
```golang
t.SetStyle(table.StyleColoredBright)
t.Render()
```
to get:
- **Horizontal Alignment**
- Auto (numeric columns aligned Right, text aligned Left)
- Custom per column (`ColumnConfig.Align`, `AlignHeader`, `AlignFooter`)
- Options: Left, Center, Right, Justify, Auto
- **Vertical Alignment**
- Custom per column with multi-line cell support (`ColumnConfig.VAlign`, `VAlignHeader`, `VAlignFooter`)
- Options: Top, Middle, Bottom
<img src="images/table-StyleColoredBright.png" width="640px" alt="Colored Table"/>
### Sorting & Filtering
### Roll your own Style
- **Sorting**
- Sort by one or more Columns (`SortBy`)
- Multiple column sorting support
- Various sort modes: Alphabetical, Numeric, Alpha-numeric, Numeric-alpha
- Case-insensitive sorting option (`IgnoreCase`)
- Custom sorting functions (`CustomLess`) for advanced sorting logic
- **Filtering**
- Filter by one or more Columns (`FilterBy`)
- Multiple filters with AND logic (all must match)
- Various filter operators:
- Equality: Equal, NotEqual
- Numeric: GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual
- String: Contains, NotContains, StartsWith, EndsWith
- Regex: RegexMatch, RegexNotMatch
- Case-insensitive filtering option (`IgnoreCase`)
- Custom filter functions (`CustomFilter`) for advanced filtering logic
- Filters are applied before sorting
- Suppress/hide columns with no content (`SuppressEmptyColumns`)
- Hide specific columns (`ColumnConfig.Hidden`)
- Suppress trailing spaces in the last column (`SuppressTrailingSpaces`)
You can also roll your own style:
```golang
t.SetStyle(table.Style{
Name: "myNewStyle",
Box: table.BoxStyle{
BottomLeft: "\\",
BottomRight: "/",
BottomSeparator: "v",
Left: "[",
LeftSeparator: "{",
MiddleHorizontal: "-",
MiddleSeparator: "+",
MiddleVertical: "|",
PaddingLeft: "<",
PaddingRight: ">",
Right: "]",
RightSeparator: "}",
TopLeft: "(",
TopRight: ")",
TopSeparator: "^",
UnfinishedRow: " ~~~",
},
Color: table.ColorOptions{
IndexColumn: text.Colors{text.BgCyan, text.FgBlack},
Footer: text.Colors{text.BgCyan, text.FgBlack},
Header: text.Colors{text.BgHiCyan, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
},
Format: table.FormatOptions{
Footer: text.FormatUpper,
Header: text.FormatUpper,
Row: text.FormatDefault,
},
Options: table.Options{
DrawBorder: true,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
},
})
```
### Customization & Styling
Or you can use one of the ready-to-use Styles, and just make a few tweaks:
```golang
t.SetStyle(table.StyleLight)
t.Style().Color.Header = text.Colors{text.BgHiCyan, text.FgBlack}
t.Style().Color.IndexColumn = text.Colors{text.BgHiCyan, text.FgBlack}
t.Style().Format.Footer = text.FormatLower
t.Style().Options.DrawBorder = false
```
- **Row Coloring**
- Custom row painter function (`SetRowPainter`)
- Row painter with attributes (`RowPainterWithAttributes`)
- Access to row number and sorted position
- **Cell Transformation**
- Customizable Cell rendering per Column (`ColumnConfig.Transformer`, `TransformerHeader`, `TransformerFooter`)
- Use built-in transformers from `text` package (Number, JSON, Time, URL, etc.)
- **Column Styling**
- Per-column colors (`ColumnConfig.Colors`, `ColorsHeader`, `ColorsFooter`)
- Per-column alignment (horizontal and vertical)
- Per-column width constraints
- **Completely customizable styles** (`SetStyle`/`Style`)
- Many ready-to-use styles: [style.go](style.go)
- `StyleDefault` - Classic ASCII borders
- `StyleLight` - Light box-drawing characters
- `StyleBold` - Bold box-drawing characters
- `StyleDouble` - Double box-drawing characters
- `StyleRounded` - Rounded box-drawing characters
- `StyleColoredBright` - Bright colors, no borders
- `StyleColoredDark` - Dark colors, no borders
- Many more colored variants (Blue, Cyan, Green, Magenta, Red, Yellow)
- Colorize Headers/Body/Footers using [../text/color.go](../text/color.go)
- Custom text-case for Headers/Body/Footers
- Enable/disable separators between rows
- Render table with or without borders
- Customize box-drawing characters
- Horizontal separators per section (title, header, rows, footer) using `BoxStyleHorizontal`
- Title and caption styling options
- HTML rendering options (CSS class, escaping, newlines, color conversion)
- Bidirectional text support (`Style().Format.Direction`)
## Auto-Merge
### Output Formats
You can auto-merge cells horizontally and vertically, but you have request for
it specifically for each row/column using `RowConfig` or `ColumnConfig`.
```golang
rowConfigAutoMerge := table.RowConfig{AutoMerge: true}
t := table.NewWriter()
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge)
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "EXE", "RUN"})
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge)
t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge)
t.AppendFooter(table.Row{"", "", "", 7, 5, 3})
t.SetAutoIndex(true)
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
{Number: 2, AutoMerge: true},
{Number: 3, AutoMerge: true},
{Number: 4, AutoMerge: true},
{Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
{Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
})
t.SetStyle(table.StyleLight)
t.Style().Options.SeparateRows = true
fmt.Println(t.Render())
```
to get:
```
┌───┬─────────┬────────┬───────────┬───────────┬───────────┐
│ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │
│ │ │ │ │ ├─────┬─────┤
│ │ │ │ │ │ EXE │ RUN │
├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤
│ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │
├───┤ │ │ ├───────────┼─────┬─────┤
│ 2 │ │ │ │ C 2 │ Y │ N │
├───┤ │ ├───────────┼───────────┼─────┴─────┤
│ 3 │ │ │ NS 1B │ C 3 │ N │
├───┤ ├────────┼───────────┼───────────┼───────────┤
│ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │
├───┤ │ │ ├───────────┼─────┬─────┤
│ 5 │ │ │ │ C 5 │ Y │ N │
├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤
│ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │
├───┤ │ │ ├───────────┼───────────┤
│ 7 │ │ │ │ C 7 │ Y │
├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤
│ │ │ │ │ 7 │ 5 │ 3 │
└───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘
```
## Paging
You can limit then number of lines rendered in a single "Page". This logic
can handle rows with multiple lines too. Here is a simple example:
```golang
t.SetPageSize(1)
t.Render()
```
to get:
```
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 1 | Arya | Stark | 3000 | |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
+-----+------------+-----------+--------+-----------------------------+
| # | FIRST NAME | LAST NAME | SALARY | |
+-----+------------+-----------+--------+-----------------------------+
| 300 | Tyrion | Lannister | 5000 | |
+-----+------------+-----------+--------+-----------------------------+
| | | TOTAL | 10000 | |
+-----+------------+-----------+--------+-----------------------------+
```
## Sorting
Sorting can be done on one or more columns. The following code will make the
rows be sorted first by "First Name" and then by "Last Name" (in case of similar
"First Name" entries).
```golang
t.SortBy([]table.SortBy{
{Name: "First Name", Mode: table.Asc},
{Name: "Last Name", Mode: table.Asc},
})
```
## Wrapping (or) Row/Column Width restrictions
You can restrict the maximum (text) width for a Row:
```golang
t.SetAllowedRowLength(50)
t.Render()
```
to get:
```
+-----+------------+-----------+--------+------- ~
| # | FIRST NAME | LAST NAME | SALARY | ~
+-----+------------+-----------+--------+------- ~
| 1 | Arya | Stark | 3000 | ~
| 20 | Jon | Snow | 2000 | You kn ~
+-----+------------+-----------+--------+------- ~
| 300 | Tyrion | Lannister | 5000 | ~
+-----+------------+-----------+--------+------- ~
| | | TOTAL | 10000 | ~
+-----+------------+-----------+--------+------- ~
```
## Column Control - Alignment, Colors, Width and more
You can control a lot of things about individual cells/columns which overrides
global properties/styles using the `SetColumnConfig()` interface:
- Alignment (horizontal & vertical)
- Colorization
- Transform individual cells based on the content
- Visibility
- Width (minimum & maximum)
```golang
nameTransformer := text.Transformer(func(val interface{}) string {
return text.Bold.Sprint(val)
})
t.SetColumnConfigs([]ColumnConfig{
{
Name: "First Name",
Align: text.AlignLeft,
AlignFooter: text.AlignLeft,
AlignHeader: text.AlignLeft,
Colors: text.Colors{text.BgBlack, text.FgRed},
ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold},
ColorsFooter: text.Colors{text.BgRed, text.FgBlack},
Hidden: false,
Transformer: nameTransformer,
TransformerFooter: nameTransformer,
TransformerHeader: nameTransformer,
VAlign: text.VAlignMiddle,
VAlignFooter: text.VAlignTop,
VAlignHeader: text.VAlignBottom,
WidthMin: 6,
WidthMax: 64,
}
})
```
## Render As ...
Tables can be rendered in other common formats such as:
### ... CSV
```golang
t.RenderCSV()
```
to get:
```
,First Name,Last Name,Salary,
1,Arya,Stark,3000,
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
300,Tyrion,Lannister,5000,
,,Total,10000,
```
### ... HTML Table
```golang
t.Style().HTML = table.HTMLOptions{
CSSClass: "game-of-thrones",
EmptyColumn: "&nbsp;",
EscapeText: true,
Newline: "<br/>",
}
t.RenderHTML()
```
to get:
```html
<table class="game-of-thrones">
<thead>
<tr>
<th align="right">#</th>
<th>First Name</th>
<th>Last Name</th>
<th align="right">Salary</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">1</td>
<td>Arya</td>
<td>Stark</td>
<td align="right">3000</td>
<td>&nbsp;</td>
</tr>
<tr>
<td align="right">20</td>
<td>Jon</td>
<td>Snow</td>
<td align="right">2000</td>
<td>You know nothing, Jon Snow!</td>
</tr>
<tr>
<td align="right">300</td>
<td>Tyrion</td>
<td>Lannister</td>
<td align="right">5000</td>
<td>&nbsp;</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align="right">&nbsp;</td>
<td>&nbsp;</td>
<td>Total</td>
<td align="right">10000</td>
<td>&nbsp;</td>
</tr>
</tfoot>
</table>
```
### ... Markdown Table
```golang
t.RenderMarkdown()
```
to get:
```markdown
| # | First Name | Last Name | Salary | |
| ---:| --- | --- | ---:| --- |
| 1 | Arya | Stark | 3000 | |
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
| 300 | Tyrion | Lannister | 5000 | |
| | | Total | 10000 | |
```
- **Render as:**
- (ASCII/Unicode) Table - Human-readable pretty format
- CSV - Comma-separated values
- HTML Table - With custom CSS Class and options
- Markdown Table - Markdown-compatible format
- TSV - Tab-separated values
- Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`)
+250
View File
@@ -0,0 +1,250 @@
package table
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// FilterBy defines what to filter (Column Name or Number), how to filter (Operator),
// and the value to compare against.
type FilterBy struct {
// Name is the name of the Column as it appears in the first Header row.
// If a Header is not provided, or the name is not found in the header, this
// will not work.
Name string
// Number is the Column # from left. When specified, it overrides the Name
// property. If you know the exact Column number, use this instead of Name.
Number int
// Operator defines how to compare the column value against the Value.
Operator FilterOperator
// Value is the value to compare against. The type should match the expected
// comparison type (string for string operations, numeric for numeric operations).
// For Contains, StartsWith, EndsWith, and RegexMatch, Value should be a string.
// For numeric comparisons (Equal, NotEqual, GreaterThan, etc.), Value can be
// a number (int, float64) or a string representation of a number.
Value interface{}
// IgnoreCase makes string comparisons case-insensitive (only applies to
// string-based operators).
IgnoreCase bool
// CustomFilter is a function that can be used to filter rows in a custom
// manner. Note that:
// * This overrides and ignores the Operator, Value, and IgnoreCase settings
// * This is called after the column contents are converted to string form
// * This function is expected to return:
// * true => include the row
// * false => exclude the row
//
// Use this when the default filtering logic is not sufficient.
CustomFilter func(cellValue string) bool
}
// FilterOperator defines how to filter.
type FilterOperator int
const (
// Equal filters rows where the column value equals the Value.
Equal FilterOperator = iota
// NotEqual filters rows where the column value does not equal the Value.
NotEqual
// GreaterThan filters rows where the column value is greater than the Value.
GreaterThan
// GreaterThanOrEqual filters rows where the column value is greater than or equal to the Value.
GreaterThanOrEqual
// LessThan filters rows where the column value is less than the Value.
LessThan
// LessThanOrEqual filters rows where the column value is less than or equal to the Value.
LessThanOrEqual
// Contains filters rows where the column value contains the Value (string search).
Contains
// NotContains filters rows where the column value does not contain the Value (string search).
NotContains
// StartsWith filters rows where the column value starts with the Value.
StartsWith
// EndsWith filters rows where the column value ends with the Value.
EndsWith
// RegexMatch filters rows where the column value matches the Value as a regular expression.
RegexMatch
// RegexNotMatch filters rows where the column value does not match the Value as a regular expression.
RegexNotMatch
)
func (t *Table) parseFilterBy(filterBy []FilterBy) []FilterBy {
var resFilterBy []FilterBy
for _, filter := range filterBy {
colNum := 0
if filter.Number > 0 && filter.Number <= t.numColumns {
colNum = filter.Number
} else if filter.Name != "" && len(t.rowsHeaderRaw) > 0 {
// Parse from raw header rows
for idx, colName := range t.rowsHeaderRaw[0] {
if fmt.Sprint(colName) == filter.Name {
colNum = idx + 1
break
}
}
}
if colNum > 0 {
resFilterBy = append(resFilterBy, FilterBy{
Name: filter.Name,
Number: colNum,
Operator: filter.Operator,
Value: filter.Value,
IgnoreCase: filter.IgnoreCase,
CustomFilter: filter.CustomFilter,
})
}
}
return resFilterBy
}
func (t *Table) matchesFiltersRaw(row Row, filters []FilterBy) bool {
// All filters must match (AND logic)
for _, filter := range filters {
if !t.matchesFilterRaw(row, filter) {
return false
}
}
return true
}
func (t *Table) matchesFilterRaw(row Row, filter FilterBy) bool {
colIdx := filter.Number - 1
if colIdx < 0 || colIdx >= len(row) {
return false
}
cellValue := row[colIdx]
cellValueStr := fmt.Sprint(cellValue)
// Use custom filter if provided
if filter.CustomFilter != nil {
return filter.CustomFilter(cellValueStr)
}
// Use operator-based filtering
return t.matchesOperator(cellValueStr, filter)
}
func (t *Table) matchesOperator(cellValue string, filter FilterBy) bool {
switch filter.Operator {
case Equal:
return t.compareEqual(cellValue, filter.Value, filter.IgnoreCase)
case NotEqual:
return !t.compareEqual(cellValue, filter.Value, filter.IgnoreCase)
case GreaterThan:
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a > b })
case GreaterThanOrEqual:
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a >= b })
case LessThan:
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a < b })
case LessThanOrEqual:
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a <= b })
case Contains:
return t.compareContains(cellValue, filter.Value, filter.IgnoreCase)
case NotContains:
return !t.compareContains(cellValue, filter.Value, filter.IgnoreCase)
case StartsWith:
return t.compareStartsWith(cellValue, filter.Value, filter.IgnoreCase)
case EndsWith:
return t.compareEndsWith(cellValue, filter.Value, filter.IgnoreCase)
case RegexMatch:
return t.compareRegexMatch(cellValue, filter.Value, filter.IgnoreCase)
case RegexNotMatch:
return !t.compareRegexMatch(cellValue, filter.Value, filter.IgnoreCase)
default:
return false
}
}
func (t *Table) compareEqual(cellValue string, filterValue interface{}, ignoreCase bool) bool {
filterStr := fmt.Sprint(filterValue)
if ignoreCase {
return strings.EqualFold(cellValue, filterStr)
}
return cellValue == filterStr
}
func (t *Table) compareNumeric(cellValue string, filterValue interface{}, compareFunc func(float64, float64) bool) bool {
cellNum, cellErr := strconv.ParseFloat(cellValue, 64)
if cellErr != nil {
return false
}
var filterNum float64
switch v := filterValue.(type) {
case int:
filterNum = float64(v)
case int64:
filterNum = float64(v)
case float64:
filterNum = v
case float32:
filterNum = float64(v)
case string:
var err error
filterNum, err = strconv.ParseFloat(v, 64)
if err != nil {
return false
}
default:
// Try to convert to string and parse
filterStr := fmt.Sprint(filterValue)
var err error
filterNum, err = strconv.ParseFloat(filterStr, 64)
if err != nil {
return false
}
}
return compareFunc(cellNum, filterNum)
}
func (t *Table) compareContains(cellValue string, filterValue interface{}, ignoreCase bool) bool {
filterStr := fmt.Sprint(filterValue)
if ignoreCase {
return strings.Contains(strings.ToLower(cellValue), strings.ToLower(filterStr))
}
return strings.Contains(cellValue, filterStr)
}
func (t *Table) compareStartsWith(cellValue string, filterValue interface{}, ignoreCase bool) bool {
filterStr := fmt.Sprint(filterValue)
if ignoreCase {
return strings.HasPrefix(strings.ToLower(cellValue), strings.ToLower(filterStr))
}
return strings.HasPrefix(cellValue, filterStr)
}
func (t *Table) compareEndsWith(cellValue string, filterValue interface{}, ignoreCase bool) bool {
filterStr := fmt.Sprint(filterValue)
if ignoreCase {
return strings.HasSuffix(strings.ToLower(cellValue), strings.ToLower(filterStr))
}
return strings.HasSuffix(cellValue, filterStr)
}
func (t *Table) compareRegexMatch(cellValue string, filterValue interface{}, ignoreCase bool) bool {
filterStr := fmt.Sprint(filterValue)
// Compile the regex pattern
var pattern *regexp.Regexp
var err error
if ignoreCase {
pattern, err = regexp.Compile("(?i)" + filterStr)
} else {
pattern, err = regexp.Compile(filterStr)
}
if err != nil {
// If regex compilation fails, fall back to simple string matching
return t.compareEqual(cellValue, filterValue, ignoreCase)
}
return pattern.MatchString(cellValue)
}
+64 -23
View File
@@ -20,7 +20,7 @@ import (
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
func (t *Table) Render() string {
t.initForRender()
t.initForRender(renderModeDefault)
var out strings.Builder
if t.numColumns > 0 {
@@ -50,6 +50,7 @@ func (t *Table) Render() string {
return t.render(&out)
}
//gocyclo:ignore
func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int {
numColumnsRendered := 1
@@ -93,11 +94,10 @@ func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxCo
numColumnsRendered++
}
}
colStr = align.Apply(colStr, maxColumnLength)
// pad both sides of the column
if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) {
colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight
colStr = t.style.Box.PaddingLeft + align.Apply(colStr, maxColumnLength) + t.style.Box.PaddingRight
}
t.renderColumnColorized(out, colIdx, colStr, hint)
@@ -112,9 +112,9 @@ func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) {
if hint.isSeparatorRow {
numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) +
utf8.RuneCountInString(t.style.Box.PaddingRight)
chars := t.style.Box.MiddleHorizontal
chars := t.style.Box.middleHorizontal(hint.separatorType)
if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() {
chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal))
chars = text.RepeatAndTrim(" ", len(chars))
}
outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars))
} else {
@@ -239,7 +239,7 @@ func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Bu
}
func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) {
out.WriteString(t.style.Format.Direction.Modifier())
out.WriteString(t.directionModifier)
if t.style.Options.DrawBorder {
border := t.getBorderLeft(hint)
colors := t.getBorderColors(hint)
@@ -304,8 +304,10 @@ func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) {
} else if hint.isFooterRow && !t.style.Options.SeparateFooter {
return
}
hint.isSeparatorRow = true
t.renderLine(out, t.rowSeparator, hint)
separator := t.rowSeparatorStrings[hint.separatorType]
t.renderLine(out, t.rowSeparators[separator], hint)
}
func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
@@ -316,8 +318,17 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint)
t.renderRow(out, row, hint)
if t.shouldSeparateRows(rowIdx, len(rows)) {
hint.isFirstRow = false
t.renderRowSeparator(out, hint)
hintSep := hint
hintSep.isFirstRow = false
hintSep.isSeparatorRow = true
if hintSep.isHeaderRow {
hintSep.separatorType = separatorTypeHeaderMiddle
} else if hintSep.isFooterRow {
hintSep.separatorType = separatorTypeFooterMiddle
} else {
hintSep.separatorType = separatorTypeRowMiddle
}
t.renderRowSeparator(out, hintSep)
}
}
}
@@ -328,46 +339,69 @@ func (t *Table) renderRowsBorderBottom(out *strings.Builder) {
isBorderBottom: true,
isFooterRow: true,
rowNumber: len(t.rowsFooter),
separatorType: separatorTypeFooterBottom,
})
} else {
t.renderRowSeparator(out, renderHint{
isBorderBottom: true,
isFooterRow: false,
rowNumber: len(t.rows),
separatorType: separatorTypeRowBottom,
})
}
}
func (t *Table) renderRowsBorderTop(out *strings.Builder) {
st := separatorTypeHeaderTop
if t.title != "" {
st = separatorTypeTitleBottom
} else if len(t.rowsHeader) == 0 && !t.autoIndex {
st = separatorTypeRowTop
}
if len(t.rowsHeader) > 0 || t.autoIndex {
t.renderRowSeparator(out, renderHint{
isBorderTop: true,
isHeaderRow: true,
rowNumber: 0,
isBorderTop: true,
isHeaderRow: true,
isSeparatorRow: true,
rowNumber: 0,
separatorType: st,
})
} else {
t.renderRowSeparator(out, renderHint{
isBorderTop: true,
isHeaderRow: false,
rowNumber: 0,
isBorderTop: true,
isHeaderRow: false,
isSeparatorRow: true,
rowNumber: 0,
separatorType: st,
})
}
}
func (t *Table) renderRowsFooter(out *strings.Builder) {
if len(t.rowsFooter) > 0 {
t.renderRowSeparator(out, renderHint{
isFooterRow: true,
isFirstRow: true,
isSeparatorRow: true,
})
// Only add separator before footer if there are data rows.
// Otherwise, renderRowsHeader already added one.
if len(t.rows) > 0 {
t.renderRowSeparator(out, renderHint{
isFooterRow: true,
isFirstRow: true,
isSeparatorRow: true,
separatorType: separatorTypeFooterTop,
})
}
t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
}
}
func (t *Table) renderRowsHeader(out *strings.Builder) {
if len(t.rowsHeader) > 0 || t.autoIndex {
hintSeparator := renderHint{isHeaderRow: true, isLastRow: true, isSeparatorRow: true}
hintSeparator := renderHint{
isHeaderRow: true,
isLastRow: true,
isSeparatorRow: true,
separatorType: separatorTypeHeaderMiddle,
}
if len(t.rowsHeader) > 0 {
t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
@@ -376,7 +410,13 @@ func (t *Table) renderRowsHeader(out *strings.Builder) {
t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
hintSeparator.rowNumber = 1
}
t.renderRowSeparator(out, hintSeparator)
// Only add separator after header if there are data rows or footer rows.
// Otherwise, the bottom border is rendered directly.
if len(t.rows) > 0 || len(t.rowsFooter) > 0 || !t.style.Options.DoNotRenderSeparatorWhenEmpty {
hintSeparator.separatorType = separatorTypeHeaderBottom
t.renderRowSeparator(out, hintSeparator)
}
}
}
@@ -393,8 +433,9 @@ func (t *Table) renderTitle(out *strings.Builder) {
}
if t.style.Options.DrawBorder {
lenBorder := rowLength - text.StringWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight)
middleHorizontal := t.style.Box.middleHorizontal(separatorTypeTitleTop)
out.WriteString(colorsBorder.Sprint(t.style.Box.TopLeft))
out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder)))
out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(middleHorizontal, lenBorder)))
out.WriteString(colorsBorder.Sprint(t.style.Box.TopRight))
}
+4 -4
View File
@@ -14,7 +14,7 @@ import (
// 300,Tyrion,Lannister,5000,
// ,,Total,10000,
func (t *Table) RenderCSV() string {
t.initForRender()
t.initForRender(renderModeCSV)
var out strings.Builder
if t.numColumns > 0 {
@@ -36,11 +36,11 @@ func (t *Table) RenderCSV() string {
}
func (t *Table) csvFixCommas(str string) string {
return strings.Replace(str, ",", "\\,", -1)
return strings.ReplaceAll(str, ",", "\\,")
}
func (t *Table) csvFixDoubleQuotes(str string) string {
return strings.Replace(str, "\"", "\\\"", -1)
return strings.ReplaceAll(str, "\"", "\\\"")
}
func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
@@ -54,7 +54,7 @@ func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint)
// auto-index column
if colIdx == 0 && t.autoIndex {
if hint.isRegularRow() {
out.WriteString(fmt.Sprint(hint.rowNumber))
fmt.Fprint(out, hint.rowNumber)
}
out.WriteRune(',')
}
+11
View File
@@ -15,6 +15,7 @@ type renderHint struct {
isTitleRow bool // title row?
rowLineNumber int // the line number for a multi-line row
rowNumber int // the row number/index
separatorType separatorType
}
func (h *renderHint) isBorderOrSeparator() bool {
@@ -37,3 +38,13 @@ func (h *renderHint) isHeaderOrFooterSeparator() bool {
func (h *renderHint) isLastLineOfLastRow() bool {
return h.isLastLineOfRow && h.isLastRow
}
type renderMode string
const (
renderModeDefault renderMode = "default"
renderModeCSV renderMode = "csv"
renderModeMarkdown renderMode = "markdown"
renderModeTSV renderMode = "tsv"
renderModeHTML renderMode = "html"
)
+10 -6
View File
@@ -60,7 +60,7 @@ const (
// </tfoot>
// </table>
func (t *Table) RenderHTML() string {
t.initForRender()
t.initForRender(renderModeHTML)
var out strings.Builder
if t.numColumns > 0 {
@@ -106,11 +106,15 @@ func (t *Table) htmlRenderCaption(out *strings.Builder) {
}
func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) {
if t.style.HTML.EscapeText {
// convertEscSequencesToSpans already escapes text content, so skip
// EscapeText if ConvertColorsToSpans is true
if t.style.HTML.ConvertColorsToSpans {
colStr = convertEscSequencesToSpans(colStr)
} else if t.style.HTML.EscapeText {
colStr = html.EscapeString(colStr)
}
if t.style.HTML.Newline != "\n" {
colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1)
colStr = strings.ReplaceAll(colStr, "\n", t.style.HTML.Newline)
}
out.WriteString(colStr)
}
@@ -147,7 +151,7 @@ func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint)
out.WriteString("</td>\n")
} else {
out.WriteString(" <td align=\"right\">")
out.WriteString(fmt.Sprint(hint.rowNumber))
fmt.Fprint(out, hint.rowNumber)
out.WriteString("</td>\n")
}
}
@@ -187,10 +191,10 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint)
t.htmlRenderColumnAttributes(out, colIdx, hint, align)
if extraColumnsRendered > 0 {
out.WriteString(" colspan=")
out.WriteString(fmt.Sprint(extraColumnsRendered + 1))
fmt.Fprint(out, extraColumnsRendered+1)
} else if rowSpan := t.shouldMergeCellsVerticallyBelow(colIdx, hint); rowSpan > 1 {
out.WriteString(" rowspan=")
out.WriteString(fmt.Sprint(rowSpan))
fmt.Fprint(out, rowSpan)
}
out.WriteString(">")
if len(colStr) == 0 {
+154 -30
View File
@@ -43,11 +43,15 @@ func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint rend
} else if colStrVal, ok := col.(string); ok {
colStr = colStrVal
} else {
colStr = fmt.Sprint(col)
colStr = convertValueToString(col)
}
colStr = strings.ReplaceAll(colStr, "\t", " ")
colStr = text.ProcessCRLF(colStr)
return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr)
// Avoid fmt.Sprintf when direction modifier is empty (most common case)
if t.directionModifier == "" {
return colStr
}
return t.directionModifier + colStr
}
func (t *Table) extractMaxColumnLengths(rows []rowStr, hint renderHint) {
@@ -149,13 +153,18 @@ func (t *Table) reBalanceMaxMergedColumnLengths() {
}
}
func (t *Table) initForRender() {
func (t *Table) initForRender(mode renderMode) {
t.renderMode = mode
// pick a default style if none was set until now
t.Style()
// reset rendering state
t.reset()
// cache the direction modifier to avoid repeated calls
t.directionModifier = t.style.Format.Direction.Modifier()
// initialize the column configs and normalize them
t.initForRenderColumnConfigs()
@@ -280,11 +289,15 @@ func (t *Table) initForRenderPaddedColumns() {
}
func (t *Table) initForRenderRows() {
// auto-index: calc the index column's max length
t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw)))
// filter the rows as requested (before stringification and sorting)
t.initForRenderFilterRows()
// stringify all the rows to make it easy to render
t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{})
// auto-index: calc the index column's max length
t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRawFiltered)))
// stringify the filtered rows
t.numColumns = 0
t.rows = t.initForRenderRowsStringify(t.rowsRawFiltered, renderHint{})
t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true})
t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true})
@@ -301,6 +314,60 @@ func (t *Table) initForRenderRows() {
t.initForRenderHideColumns()
}
// initForRenderFilterRows filters the raw rows by removing non-matching rows from t.rowsRawFiltered.
func (t *Table) initForRenderFilterRows() {
// Restore original rows before filtering (in case of multiple renders with different filters)
if len(t.rowsRaw) > 0 {
t.rowsRawFiltered = make([]Row, len(t.rowsRaw))
for i, row := range t.rowsRaw {
rowCopy := make(Row, len(row))
copy(rowCopy, row)
t.rowsRawFiltered[i] = rowCopy
}
}
if len(t.filterBy) == 0 {
// No filters, nothing to do
return
}
// Store original separators and track which rows are kept
originalSeparators := make(map[int]bool)
for k, v := range t.separators {
originalSeparators[k] = v
}
// Calculate numColumns from raw rows/headers for filter parsing
t.calculateNumColumnsFromRaw()
parsedFilterBy := t.parseFilterBy(t.filterBy)
if len(parsedFilterBy) == 0 {
// No valid filters, nothing to do
return
}
// Filter rows in place and track which original rows were kept
filteredRows := t.rowsRawFiltered[:0]
keptIndices := make([]int, 0, len(t.rowsRawFiltered))
for origIdx, row := range t.rowsRawFiltered {
if t.matchesFiltersRaw(row, parsedFilterBy) {
filteredRows = append(filteredRows, row)
keptIndices = append(keptIndices, origIdx)
}
}
t.rowsRawFiltered = filteredRows
// Update separators map to reflect filtered rows
if len(originalSeparators) > 0 {
newSeparators := make(map[int]bool)
for newIdx, origIdx := range keptIndices {
if originalSeparators[origIdx] {
newSeparators[newIdx] = true
}
}
t.separators = newSeparators
}
}
func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr {
rowsStr := make([]rowStr, len(rows))
for idx, row := range rows {
@@ -315,36 +382,92 @@ func (t *Table) initForRenderRowPainterColors() {
return
}
// generate the colors
t.rowsColors = make([]text.Colors, len(t.rowsRaw))
for idx, row := range t.rowsRaw {
idxColors := idx
// generate the colors for the final rows (after filtering and sorting)
// rowsColors will be indexed by the final position in t.rows
t.rowsColors = make([]text.Colors, len(t.rows))
// For each final position, find the row index in t.rowsRawFiltered (which is already filtered)
for finalPos := range t.rows {
var rowIdx int
if len(t.sortedRowIndices) > 0 {
// override with the sorted row index
for j := 0; j < len(t.sortedRowIndices); j++ {
if t.sortedRowIndices[j] == idx {
idxColors = j
break
}
}
// Rows were sorted: finalPos -> sortedRowIndices[finalPos] -> rowIdx in t.rowsRawFiltered
rowIdx = t.sortedRowIndices[finalPos]
} else {
// No sorting: finalPos -> rowIdx in t.rowsRawFiltered
rowIdx = finalPos
}
if t.rowPainter != nil {
t.rowsColors[idxColors] = t.rowPainter(row)
} else if t.rowPainterWithAttributes != nil {
t.rowsColors[idxColors] = t.rowPainterWithAttributes(row, RowAttributes{
Number: idx + 1,
NumberSorted: idxColors + 1,
})
if rowIdx >= 0 && rowIdx < len(t.rowsRawFiltered) {
row := t.rowsRawFiltered[rowIdx]
if t.rowPainter != nil {
t.rowsColors[finalPos] = t.rowPainter(row)
} else if t.rowPainterWithAttributes != nil {
t.rowsColors[finalPos] = t.rowPainterWithAttributes(row, RowAttributes{
Number: rowIdx + 1,
NumberSorted: finalPos + 1,
})
}
}
}
}
func (t *Table) initForRenderRowSeparator() {
t.rowSeparator = make(rowStr, t.numColumns)
for colIdx, maxColumnLength := range t.maxColumnLengths {
maxColumnLength += text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
// this is needed only for default render mode
if t.renderMode != renderModeDefault {
return
}
// init the separatorType -> separator-string map
t.initForRenderRowSeparatorStrings()
// init the separator-string -> separator-row map
t.rowSeparators = make(map[string]rowStr, len(t.rowSeparatorStrings))
paddingLength := text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
for _, separator := range t.rowSeparatorStrings {
t.rowSeparators[separator] = make(rowStr, t.numColumns)
for colIdx, maxColumnLength := range t.maxColumnLengths {
t.rowSeparators[separator][colIdx] = text.RepeatAndTrim(separator, maxColumnLength+paddingLength)
}
}
}
func (t *Table) initForRenderRowSeparatorStrings() {
// allocate and init only the separators that are needed
t.rowSeparatorStrings = make(map[separatorType]string)
addSeparatorType := func(st separatorType) {
t.rowSeparatorStrings[st] = t.style.Box.middleHorizontal(st)
}
// for other render modes, we need all the separators
if t.title != "" {
addSeparatorType(separatorTypeTitleTop)
addSeparatorType(separatorTypeTitleBottom)
}
if len(t.rowsHeader) > 0 || t.autoIndex {
addSeparatorType(separatorTypeHeaderTop)
addSeparatorType(separatorTypeHeaderBottom)
if len(t.rowsHeader) > 1 {
addSeparatorType(separatorTypeHeaderMiddle)
}
}
if len(t.rows) > 0 {
addSeparatorType(separatorTypeRowTop)
addSeparatorType(separatorTypeRowBottom)
if len(t.rows) > 1 {
addSeparatorType(separatorTypeRowMiddle)
}
} else if len(t.rowsHeader) > 0 || t.autoIndex {
// When there are headers but no data rows, we still need separatorTypeRowBottom
// for the bottom border.
addSeparatorType(separatorTypeRowBottom)
}
if len(t.rowsFooter) > 0 || t.autoIndex {
addSeparatorType(separatorTypeFooterTop)
addSeparatorType(separatorTypeFooterBottom)
if len(t.rowsFooter) > 1 {
addSeparatorType(separatorTypeFooterMiddle)
}
}
}
@@ -402,9 +525,10 @@ func (t *Table) reset() {
t.maxRowLength = 0
t.numColumns = 0
t.numLinesRendered = 0
t.rowSeparator = nil
t.rowSeparators = nil
t.rows = nil
t.rowsColors = nil
t.rowsFooter = nil
t.rowsHeader = nil
t.sortedRowIndices = nil
}
+26 -15
View File
@@ -14,7 +14,7 @@ import (
// | 300 | Tyrion | Lannister | 5000 | |
// | | | Total | 10000 | |
func (t *Table) RenderMarkdown() string {
t.initForRender()
t.initForRender(renderModeMarkdown)
var out strings.Builder
if t.numColumns > 0 {
@@ -47,19 +47,15 @@ func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderH
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
t.markdownRenderRowAutoIndex(out, colIdx, hint)
if hint.isSeparatorRow {
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
} else {
var colStr string
if colIdx < len(row) {
colStr = row[colIdx]
}
out.WriteRune(' ')
colStr = strings.ReplaceAll(colStr, "|", "\\|")
colStr = strings.ReplaceAll(colStr, "\n", "<br/>")
out.WriteString(colStr)
out.WriteRune(' ')
var colStr string
if colIdx < len(row) {
colStr = row[colIdx]
}
out.WriteRune(' ')
colStr = strings.ReplaceAll(colStr, "|", "\\|")
colStr = strings.ReplaceAll(colStr, "\n", "<br/>")
out.WriteString(colStr)
out.WriteRune(' ')
out.WriteRune('|')
}
}
@@ -70,7 +66,7 @@ func (t *Table) markdownRenderRowAutoIndex(out *strings.Builder, colIdx int, hin
if hint.isSeparatorRow {
out.WriteString("---:")
} else if hint.isRegularRow() {
out.WriteString(fmt.Sprintf("%d ", hint.rowNumber))
fmt.Fprintf(out, "%d ", hint.rowNumber)
}
out.WriteRune('|')
}
@@ -83,7 +79,7 @@ func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint ren
t.markdownRenderRow(out, row, hint)
if idx == len(rows)-1 && hint.isHeaderRow {
t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true})
t.markdownRenderSeparator(out, renderHint{isSeparatorRow: true})
}
}
}
@@ -101,6 +97,21 @@ func (t *Table) markdownRenderRowsHeader(out *strings.Builder) {
}
}
func (t *Table) markdownRenderSeparator(out *strings.Builder, hint renderHint) {
// when working on line number 2 or more, insert a newline first
if out.Len() > 0 {
out.WriteRune('\n')
}
out.WriteRune('|')
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
t.markdownRenderRowAutoIndex(out, colIdx, hint)
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
out.WriteRune('|')
}
}
func (t *Table) markdownRenderTitle(out *strings.Builder) {
if t.title != "" {
out.WriteString("# ")
+3 -3
View File
@@ -6,7 +6,7 @@ import (
)
func (t *Table) RenderTSV() string {
t.initForRender()
t.initForRender(renderModeTSV)
var out strings.Builder
@@ -40,7 +40,7 @@ func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint)
for idx, col := range row {
if idx == 0 && t.autoIndex {
if hint.isRegularRow() {
out.WriteString(fmt.Sprint(hint.rowNumber))
fmt.Fprint(out, hint.rowNumber)
}
out.WriteRune('\t')
}
@@ -51,7 +51,7 @@ func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint)
if strings.ContainsAny(col, "\t\n\"") || strings.Contains(col, " ") {
col = strings.ReplaceAll(col, "\"", "\"\"") // fix double-quotes
out.WriteString(fmt.Sprintf("\"%s\"", col))
fmt.Fprintf(out, "\"%s\"", col)
} else {
out.WriteString(col)
}
+64 -64
View File
@@ -21,6 +21,18 @@ type SortBy struct {
// IgnoreCase makes sorting case-insensitive
IgnoreCase bool
// CustomLess is a function that can be used to sort the column in a custom
// manner. Note that:
// * This overrides and ignores the Mode and IgnoreCase settings
// * This is called after the column contents are converted to string form
// * This function is expected to return:
// * -1 => when iStr comes before jStr
// * 0 => when iStr and jStr are considered equal
// * 1 => when iStr comes after jStr
//
// Use this when the default sorting logic is not sufficient.
CustomLess func(iStr string, jStr string) int
}
// SortMode defines How to sort.
@@ -49,12 +61,6 @@ const (
DscNumericAlpha
)
type rowsSorter struct {
rows []rowStr
sortBy []SortBy
sortedIndices []int
}
// getSortedRowIndices sorts and returns the row indices in Sorted order as
// directed by Table.sortBy which can be set using Table.SortBy(...)
func (t *Table) getSortedRowIndices() []int {
@@ -63,11 +69,31 @@ func (t *Table) getSortedRowIndices() []int {
sortedIndices[idx] = idx
}
if t.sortBy != nil && len(t.sortBy) > 0 {
sort.Sort(rowsSorter{
rows: t.rows,
sortBy: t.parseSortBy(t.sortBy),
sortedIndices: sortedIndices,
if len(t.sortBy) > 0 {
parsedSortBy := t.parseSortBy(t.sortBy)
sort.Slice(sortedIndices, func(i, j int) bool {
isEqual, isLess := false, false
realI, realJ := sortedIndices[i], sortedIndices[j]
for _, sortBy := range parsedSortBy {
// extract the values/cells from the rows for comparison
rowI, rowJ, colIdx := t.rows[realI], t.rows[realJ], sortBy.Number-1
iVal, jVal := "", ""
if colIdx < len(rowI) {
iVal = rowI[colIdx]
}
if colIdx < len(rowJ) {
jVal = rowJ[colIdx]
}
// compare and choose whether to continue
isEqual, isLess = less(iVal, jVal, sortBy)
// if the values are not equal, return the result immediately
if !isEqual {
return isLess
}
// if the values are equal, continue to the next column
}
return isLess
})
}
@@ -94,48 +120,32 @@ func (t *Table) parseSortBy(sortBy []SortBy) []SortBy {
Number: colNum,
Mode: col.Mode,
IgnoreCase: col.IgnoreCase,
CustomLess: col.CustomLess,
})
}
}
return resSortBy
}
func (rs rowsSorter) Len() int {
return len(rs.rows)
}
func (rs rowsSorter) Swap(i, j int) {
rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i]
}
func (rs rowsSorter) Less(i, j int) bool {
shouldContinue, returnValue := false, false
realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j]
for _, sortBy := range rs.sortBy {
// extract the values/cells from the rows for comparison
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], sortBy.Number-1
iVal, jVal := "", ""
if colIdx < len(rowI) {
iVal = rowI[colIdx]
}
if colIdx < len(rowJ) {
jVal = rowJ[colIdx]
}
// compare and choose whether to continue
shouldContinue, returnValue = less(iVal, jVal, sortBy)
if !shouldContinue {
break
func less(iVal string, jVal string, sb SortBy) (bool, bool) {
if sb.CustomLess != nil {
// use the custom less function to compare the values
rc := sb.CustomLess(iVal, jVal)
if rc < 0 {
return false, true
} else if rc > 0 {
return false, false
} else { // rc == 0
return true, false
}
}
return returnValue
}
func less(iVal string, jVal string, sb SortBy) (bool, bool) {
// if the values are equal, return fast to continue to next column
if iVal == jVal {
return true, false
}
// otherwise, use the default sorting logic defined by Mode and IgnoreCase
switch sb.Mode {
case Asc, Dsc:
return lessAlphabetic(iVal, jVal, sb)
@@ -168,37 +178,27 @@ func lessAlphabetic(iVal string, jVal string, sb SortBy) (bool, bool) {
}
}
func lessAlphaNumericI(sb SortBy) (bool, bool) {
// i == "abc"; j == 5
switch sb.Mode {
case AscAlphaNumeric, DscAlphaNumeric:
return false, true
default: // AscNumericAlpha, DscNumericAlpha
return false, false
}
}
func lessAlphaNumericJ(sb SortBy) (bool, bool) {
// i == 5; j == "abc"
switch sb.Mode {
case AscAlphaNumeric, DscAlphaNumeric:
return false, false
default: // AscNumericAlpha, DscNumericAlpha:
return false, true
}
}
func lessMixedMode(iVal string, jVal string, sb SortBy) (bool, bool) {
iNumVal, iErr := strconv.ParseFloat(iVal, 64)
jNumVal, jErr := strconv.ParseFloat(jVal, 64)
if iErr != nil && jErr != nil { // both are alphanumeric
return lessAlphabetic(iVal, jVal, sb)
}
if iErr != nil { // iVal is alphabetic, jVal is numeric
return lessAlphaNumericI(sb)
if iErr != nil { // iVal == "abc"; jVal == 5
switch sb.Mode {
case AscAlphaNumeric, DscAlphaNumeric:
return false, true
default: // AscNumericAlpha, DscNumericAlpha
return false, false
}
}
if jErr != nil { // iVal is numeric, jVal is alphabetic
return lessAlphaNumericJ(sb)
if jErr != nil { // iVal == 5; jVal == "abc"
switch sb.Mode {
case AscAlphaNumeric, DscAlphaNumeric:
return false, false
default: // AscNumericAlpha, DscNumericAlpha:
return false, true
}
}
// both values numeric
return lessNumericVal(iNumVal, jNumVal, sb)
-592
View File
@@ -1,9 +1,5 @@
package table
import (
"github.com/jedib0t/go-pretty/v6/text"
)
// Style declares how to render the Table and provides very fine-grained control
// on how the Table gets rendered on the Console.
type Style struct {
@@ -340,591 +336,3 @@ var (
Title: TitleOptionsDefault,
}
)
// BoxStyle defines the characters/strings to use to render the borders and
// separators for the Table.
type BoxStyle struct {
BottomLeft string
BottomRight string
BottomSeparator string
EmptySeparator string
Left string
LeftSeparator string
MiddleHorizontal string
MiddleSeparator string
MiddleVertical string
PaddingLeft string
PaddingRight string
PageSeparator string
Right string
RightSeparator string
TopLeft string
TopRight string
TopSeparator string
UnfinishedRow string
}
var (
// StyleBoxDefault defines a Boxed-Table like below:
// +-----+------------+-----------+--------+-----------------------------+
// | # | FIRST NAME | LAST NAME | SALARY | |
// +-----+------------+-----------+--------+-----------------------------+
// | 1 | Arya | Stark | 3000 | |
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
// | 300 | Tyrion | Lannister | 5000 | |
// +-----+------------+-----------+--------+-----------------------------+
// | | | TOTAL | 10000 | |
// +-----+------------+-----------+--------+-----------------------------+
StyleBoxDefault = BoxStyle{
BottomLeft: "+",
BottomRight: "+",
BottomSeparator: "+",
EmptySeparator: " ",
Left: "|",
LeftSeparator: "+",
MiddleHorizontal: "-",
MiddleSeparator: "+",
MiddleVertical: "|",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "|",
RightSeparator: "+",
TopLeft: "+",
TopRight: "+",
TopSeparator: "+",
UnfinishedRow: " ~",
}
// StyleBoxBold defines a Boxed-Table like below:
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
StyleBoxBold = BoxStyle{
BottomLeft: "┗",
BottomRight: "┛",
BottomSeparator: "┻",
EmptySeparator: " ",
Left: "┃",
LeftSeparator: "┣",
MiddleHorizontal: "━",
MiddleSeparator: "╋",
MiddleVertical: "┃",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "┃",
RightSeparator: "┫",
TopLeft: "┏",
TopRight: "┓",
TopSeparator: "┳",
UnfinishedRow: " ≈",
}
// StyleBoxDouble defines a Boxed-Table like below:
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
// ║ ║ ║ TOTAL ║ 10000 ║ ║
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
StyleBoxDouble = BoxStyle{
BottomLeft: "╚",
BottomRight: "╝",
BottomSeparator: "╩",
EmptySeparator: " ",
Left: "║",
LeftSeparator: "╠",
MiddleHorizontal: "═",
MiddleSeparator: "╬",
MiddleVertical: "║",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "║",
RightSeparator: "╣",
TopLeft: "╔",
TopRight: "╗",
TopSeparator: "╦",
UnfinishedRow: " ≈",
}
// StyleBoxLight defines a Boxed-Table like below:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
StyleBoxLight = BoxStyle{
BottomLeft: "└",
BottomRight: "┘",
BottomSeparator: "┴",
EmptySeparator: " ",
Left: "│",
LeftSeparator: "├",
MiddleHorizontal: "─",
MiddleSeparator: "┼",
MiddleVertical: "│",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "│",
RightSeparator: "┤",
TopLeft: "┌",
TopRight: "┐",
TopSeparator: "┬",
UnfinishedRow: " ≈",
}
// StyleBoxRounded defines a Boxed-Table like below:
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
StyleBoxRounded = BoxStyle{
BottomLeft: "╰",
BottomRight: "╯",
BottomSeparator: "┴",
EmptySeparator: " ",
Left: "│",
LeftSeparator: "├",
MiddleHorizontal: "─",
MiddleSeparator: "┼",
MiddleVertical: "│",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "│",
RightSeparator: "┤",
TopLeft: "╭",
TopRight: "╮",
TopSeparator: "┬",
UnfinishedRow: " ≈",
}
// styleBoxTest defines a Boxed-Table like below:
// (-----^------------^-----------^--------^-----------------------------)
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
// {-----+------------+-----------+--------+-----------------------------}
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
// {-----+------------+-----------+--------+-----------------------------}
// [< >|< >|<TOTAL >|< 10000>|< >]
// \-----v------------v-----------v--------v-----------------------------/
styleBoxTest = BoxStyle{
BottomLeft: "\\",
BottomRight: "/",
BottomSeparator: "v",
EmptySeparator: " ",
Left: "[",
LeftSeparator: "{",
MiddleHorizontal: "--",
MiddleSeparator: "+",
MiddleVertical: "|",
PaddingLeft: "<",
PaddingRight: ">",
PageSeparator: "\n",
Right: "]",
RightSeparator: "}",
TopLeft: "(",
TopRight: ")",
TopSeparator: "^",
UnfinishedRow: " ~~~",
}
)
// ColorOptions defines the ANSI colors to use for parts of the Table.
type ColorOptions struct {
Border text.Colors // borders (if nil, uses one of the below)
Footer text.Colors // footer row(s) colors
Header text.Colors // header row(s) colors
IndexColumn text.Colors // index-column colors (row #, etc.)
Row text.Colors // regular row(s) colors
RowAlternate text.Colors // regular row(s) colors for the even-numbered rows
Separator text.Colors // separators (if nil, uses one of the above)
}
var (
// ColorOptionsDefault defines sensible ANSI color options - basically NONE.
ColorOptionsDefault = ColorOptions{}
// ColorOptionsBright renders dark text on bright background.
ColorOptionsBright = ColorOptionsBlackOnCyanWhite
// ColorOptionsDark renders bright text on dark background.
ColorOptionsDark = ColorOptionsCyanWhiteOnBlack
// ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background.
ColorOptionsBlackOnBlueWhite = ColorOptions{
Footer: text.Colors{text.BgBlue, text.FgBlack},
Header: text.Colors{text.BgHiBlue, text.FgBlack},
IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background.
ColorOptionsBlackOnCyanWhite = ColorOptions{
Footer: text.Colors{text.BgCyan, text.FgBlack},
Header: text.Colors{text.BgHiCyan, text.FgBlack},
IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnGreenWhite renders Black text on Green/White
// background.
ColorOptionsBlackOnGreenWhite = ColorOptions{
Footer: text.Colors{text.BgGreen, text.FgBlack},
Header: text.Colors{text.BgHiGreen, text.FgBlack},
IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White
// background.
ColorOptionsBlackOnMagentaWhite = ColorOptions{
Footer: text.Colors{text.BgMagenta, text.FgBlack},
Header: text.Colors{text.BgHiMagenta, text.FgBlack},
IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnRedWhite renders Black text on Red/White background.
ColorOptionsBlackOnRedWhite = ColorOptions{
Footer: text.Colors{text.BgRed, text.FgBlack},
Header: text.Colors{text.BgHiRed, text.FgBlack},
IndexColumn: text.Colors{text.BgHiRed, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White
// background.
ColorOptionsBlackOnYellowWhite = ColorOptions{
Footer: text.Colors{text.BgYellow, text.FgBlack},
Header: text.Colors{text.BgHiYellow, text.FgBlack},
IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background.
ColorOptionsBlueWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgBlue, text.BgHiBlack},
Header: text.Colors{text.FgHiBlue, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background.
ColorOptionsCyanWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgCyan, text.BgHiBlack},
Header: text.Colors{text.FgHiCyan, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsGreenWhiteOnBlack renders Green/White text on Black
// background.
ColorOptionsGreenWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgGreen, text.BgHiBlack},
Header: text.Colors{text.FgHiGreen, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black
// background.
ColorOptionsMagentaWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgMagenta, text.BgHiBlack},
Header: text.Colors{text.FgHiMagenta, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsRedWhiteOnBlack renders Red/White text on Black background.
ColorOptionsRedWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgRed, text.BgHiBlack},
Header: text.Colors{text.FgHiRed, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black
// background.
ColorOptionsYellowWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgYellow, text.BgHiBlack},
Header: text.Colors{text.FgHiYellow, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
)
// FormatOptions defines the text-formatting to perform on parts of the Table.
type FormatOptions struct {
Direction text.Direction // (forced) BiDi direction for each Column
Footer text.Format // default text format
FooterAlign text.Align // default horizontal align
FooterVAlign text.VAlign // default vertical align
Header text.Format // default text format
HeaderAlign text.Align // default horizontal align
HeaderVAlign text.VAlign // default vertical align
Row text.Format // default text format
RowAlign text.Align // default horizontal align
RowVAlign text.VAlign // default vertical align
}
// FormatOptionsDefault defines sensible formatting options.
var FormatOptionsDefault = FormatOptions{
Footer: text.FormatUpper,
FooterAlign: text.AlignDefault,
FooterVAlign: text.VAlignDefault,
Header: text.FormatUpper,
HeaderAlign: text.AlignDefault,
HeaderVAlign: text.VAlignDefault,
Row: text.FormatDefault,
RowAlign: text.AlignDefault,
RowVAlign: text.VAlignDefault,
}
// HTMLOptions defines the global options to control HTML rendering.
type HTMLOptions struct {
CSSClass string // CSS class to set on the overall <table> tag
EmptyColumn string // string to replace "" columns with (entire content being "")
EscapeText bool // escape text into HTML-safe content?
Newline string // string to replace "\n" characters with
}
// DefaultHTMLOptions defines sensible HTML rendering defaults.
var DefaultHTMLOptions = HTMLOptions{
CSSClass: DefaultHTMLCSSClass,
EmptyColumn: "&nbsp;",
EscapeText: true,
Newline: "<br/>",
}
// Options defines the global options that determine how the Table is
// rendered.
type Options struct {
// DoNotColorBordersAndSeparators disables coloring all the borders and row
// or column separators.
DoNotColorBordersAndSeparators bool
// DrawBorder enables or disables drawing the border around the Table.
// Example of a table where it is disabled:
// # │ FIRST NAME │ LAST NAME │ SALARY │
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
// 1 │ Arya │ Stark │ 3000 │
// 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow!
// 300 │ Tyrion │ Lannister │ 5000 │
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
// │ │ TOTAL │ 10000 │
DrawBorder bool
// SeparateColumns enables or disable drawing border between columns.
// Example of a table where it is disabled:
// ┌─────────────────────────────────────────────────────────────────┐
// │ # FIRST NAME LAST NAME SALARY │
// ├─────────────────────────────────────────────────────────────────┤
// │ 1 Arya Stark 3000 │
// │ 20 Jon Snow 2000 You know nothing, Jon Snow! │
// │ 300 Tyrion Lannister 5000 │
// │ TOTAL 10000 │
// └─────────────────────────────────────────────────────────────────┘
SeparateColumns bool
// SeparateFooter enables or disable drawing border between the footer and
// the rows. Example of a table where it is disabled:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
SeparateFooter bool
// SeparateHeader enables or disable drawing border between the header and
// the rows. Example of a table where it is disabled:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
SeparateHeader bool
// SeparateRows enables or disables drawing separators between each row.
// Example of a table where it is enabled:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
SeparateRows bool
}
var (
// OptionsDefault defines sensible global options.
OptionsDefault = Options{
DrawBorder: true,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
}
// OptionsNoBorders sets up a table without any borders.
OptionsNoBorders = Options{
DrawBorder: false,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
}
// OptionsNoBordersAndSeparators sets up a table without any borders or
// separators.
OptionsNoBordersAndSeparators = Options{
DrawBorder: false,
SeparateColumns: false,
SeparateFooter: false,
SeparateHeader: false,
SeparateRows: false,
}
)
// SizeOptions defines the way to control the width of the table output.
type SizeOptions struct {
// WidthMax is the maximum allotted width for the full row;
// any content beyond this will be truncated using the text
// in Style.Box.UnfinishedRow
WidthMax int
// WidthMin is the minimum allotted width for the full row;
// columns will be auto-expanded until the overall width
// is met
WidthMin int
}
var (
// SizeOptionsDefault defines sensible size options - basically NONE.
SizeOptionsDefault = SizeOptions{
WidthMax: 0,
WidthMin: 0,
}
)
// TitleOptions defines the way the title text is to be rendered.
type TitleOptions struct {
Align text.Align
Colors text.Colors
Format text.Format
}
var (
// TitleOptionsDefault defines sensible title options - basically NONE.
TitleOptionsDefault = TitleOptions{}
// TitleOptionsBright renders Bright Bold text on Dark background.
TitleOptionsBright = TitleOptionsBlackOnCyan
// TitleOptionsDark renders Dark Bold text on Bright background.
TitleOptionsDark = TitleOptionsCyanOnBlack
// TitleOptionsBlackOnBlue renders Black text on Blue background.
TitleOptionsBlackOnBlue = TitleOptions{
Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold),
}
// TitleOptionsBlackOnCyan renders Black Bold text on Cyan background.
TitleOptionsBlackOnCyan = TitleOptions{
Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold),
}
// TitleOptionsBlackOnGreen renders Black Bold text onGreen background.
TitleOptionsBlackOnGreen = TitleOptions{
Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold),
}
// TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background.
TitleOptionsBlackOnMagenta = TitleOptions{
Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold),
}
// TitleOptionsBlackOnRed renders Black Bold text on Red background.
TitleOptionsBlackOnRed = TitleOptions{
Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold),
}
// TitleOptionsBlackOnYellow renders Black Bold text on Yellow background.
TitleOptionsBlackOnYellow = TitleOptions{
Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold),
}
// TitleOptionsBlueOnBlack renders Blue Bold text on Black background.
TitleOptionsBlueOnBlack = TitleOptions{
Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsCyanOnBlack renders Cyan Bold text on Black background.
TitleOptionsCyanOnBlack = TitleOptions{
Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsGreenOnBlack renders Green Bold text on Black background.
TitleOptionsGreenOnBlack = TitleOptions{
Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background.
TitleOptionsMagentaOnBlack = TitleOptions{
Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsRedOnBlack renders Red Bold text on Black background.
TitleOptionsRedOnBlack = TitleOptions{
Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsYellowOnBlack renders Yellow Bold text on Black background.
TitleOptionsYellowOnBlack = TitleOptions{
Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold),
}
)
+303
View File
@@ -0,0 +1,303 @@
package table
// BoxStyle defines the characters/strings to use to render the borders and
// separators for the Table.
type BoxStyle struct {
BottomLeft string
BottomRight string
BottomSeparator string
EmptySeparator string
Left string
LeftSeparator string
MiddleHorizontal string
MiddleSeparator string
MiddleVertical string
PaddingLeft string
PaddingRight string
PageSeparator string
Right string
RightSeparator string
TopLeft string
TopRight string
TopSeparator string
UnfinishedRow string
// Horizontal lets you customize the horizontal lines for the Table
// in a more granular way than the MiddleHorizontal string. Setting
// this to a non-nil value will override MiddleHorizontal.
Horizontal *BoxStyleHorizontal
}
// BoxStyleHorizontal defines the characters/strings to use to render the
// horizontal lines for the Table.
type BoxStyleHorizontal struct {
TitleTop string
TitleBottom string // overrides HeaderTop/RowTop
HeaderTop string
HeaderMiddle string
HeaderBottom string // overrides RowTop
RowTop string
RowMiddle string
RowBottom string
FooterTop string // overrides RowBottom
FooterMiddle string
FooterBottom string
}
// NewBoxStyleHorizontal creates a new BoxStyleHorizontal with the given
// horizontal string.
func NewBoxStyleHorizontal(horizontal string) *BoxStyleHorizontal {
return &BoxStyleHorizontal{
TitleTop: horizontal,
TitleBottom: horizontal,
HeaderTop: horizontal,
HeaderMiddle: horizontal,
HeaderBottom: horizontal,
RowTop: horizontal,
RowMiddle: horizontal,
RowBottom: horizontal,
FooterTop: horizontal,
FooterMiddle: horizontal,
FooterBottom: horizontal,
}
}
var (
// StyleBoxDefault defines a Boxed-Table like below:
// +-----+------------+-----------+--------+-----------------------------+
// | # | FIRST NAME | LAST NAME | SALARY | |
// +-----+------------+-----------+--------+-----------------------------+
// | 1 | Arya | Stark | 3000 | |
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
// | 300 | Tyrion | Lannister | 5000 | |
// +-----+------------+-----------+--------+-----------------------------+
// | | | TOTAL | 10000 | |
// +-----+------------+-----------+--------+-----------------------------+
StyleBoxDefault = BoxStyle{
BottomLeft: "+",
BottomRight: "+",
BottomSeparator: "+",
EmptySeparator: " ",
Left: "|",
LeftSeparator: "+",
MiddleHorizontal: "-",
MiddleSeparator: "+",
MiddleVertical: "|",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "|",
RightSeparator: "+",
TopLeft: "+",
TopRight: "+",
TopSeparator: "+",
UnfinishedRow: " ~",
}
// StyleBoxBold defines a Boxed-Table like below:
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
StyleBoxBold = BoxStyle{
BottomLeft: "┗",
BottomRight: "┛",
BottomSeparator: "┻",
EmptySeparator: " ",
Left: "┃",
LeftSeparator: "┣",
MiddleHorizontal: "━",
MiddleSeparator: "╋",
MiddleVertical: "┃",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "┃",
RightSeparator: "┫",
TopLeft: "┏",
TopRight: "┓",
TopSeparator: "┳",
UnfinishedRow: " ≈",
}
// StyleBoxDouble defines a Boxed-Table like below:
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
// ║ ║ ║ TOTAL ║ 10000 ║ ║
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
StyleBoxDouble = BoxStyle{
BottomLeft: "╚",
BottomRight: "╝",
BottomSeparator: "╩",
EmptySeparator: " ",
Left: "║",
LeftSeparator: "╠",
MiddleHorizontal: "═",
MiddleSeparator: "╬",
MiddleVertical: "║",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "║",
RightSeparator: "╣",
TopLeft: "╔",
TopRight: "╗",
TopSeparator: "╦",
UnfinishedRow: " ≈",
}
// StyleBoxLight defines a Boxed-Table like below:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
StyleBoxLight = BoxStyle{
BottomLeft: "└",
BottomRight: "┘",
BottomSeparator: "┴",
EmptySeparator: " ",
Left: "│",
LeftSeparator: "├",
MiddleHorizontal: "─",
MiddleSeparator: "┼",
MiddleVertical: "│",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "│",
RightSeparator: "┤",
TopLeft: "┌",
TopRight: "┐",
TopSeparator: "┬",
UnfinishedRow: " ≈",
}
// StyleBoxRounded defines a Boxed-Table like below:
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
StyleBoxRounded = BoxStyle{
BottomLeft: "╰",
BottomRight: "╯",
BottomSeparator: "┴",
EmptySeparator: " ",
Left: "│",
LeftSeparator: "├",
MiddleHorizontal: "─",
MiddleSeparator: "┼",
MiddleVertical: "│",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "\n",
Right: "│",
RightSeparator: "┤",
TopLeft: "╭",
TopRight: "╮",
TopSeparator: "┬",
UnfinishedRow: " ≈",
}
// styleBoxTest defines a Boxed-Table like below:
// (-----^------------^-----------^--------^-----------------------------)
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
// {-----+------------+-----------+--------+-----------------------------}
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
// {-----+------------+-----------+--------+-----------------------------}
// [< >|< >|<TOTAL >|< 10000>|< >]
// \-----v------------v-----------v--------v-----------------------------/
styleBoxTest = BoxStyle{
BottomLeft: "\\",
BottomRight: "/",
BottomSeparator: "v",
EmptySeparator: " ",
Left: "[",
LeftSeparator: "{",
MiddleHorizontal: "--",
MiddleSeparator: "+",
MiddleVertical: "|",
PaddingLeft: "<",
PaddingRight: ">",
PageSeparator: "\n",
Right: "]",
RightSeparator: "}",
TopLeft: "(",
TopRight: ")",
TopSeparator: "^",
UnfinishedRow: " ~~~",
}
)
type separatorType int
const (
separatorTypeTitleTop separatorType = iota
separatorTypeTitleBottom
separatorTypeHeaderTop
separatorTypeHeaderMiddle
separatorTypeHeaderBottom
separatorTypeRowTop
separatorTypeRowMiddle
separatorTypeRowBottom
separatorTypeFooterTop
separatorTypeFooterMiddle
separatorTypeFooterBottom
separatorTypeCount // this should be the last value
)
func (bs *BoxStyle) ensureHorizontalInitialized() {
if bs.Horizontal == nil {
bs.Horizontal = NewBoxStyleHorizontal(bs.MiddleHorizontal)
}
}
func (bs *BoxStyle) middleHorizontal(st separatorType) string {
bs.ensureHorizontalInitialized()
switch st {
case separatorTypeTitleTop:
return bs.Horizontal.TitleTop
case separatorTypeTitleBottom:
return bs.Horizontal.TitleBottom
case separatorTypeHeaderTop:
return bs.Horizontal.HeaderTop
case separatorTypeHeaderMiddle:
return bs.Horizontal.HeaderMiddle
case separatorTypeHeaderBottom:
return bs.Horizontal.HeaderBottom
case separatorTypeRowTop:
return bs.Horizontal.RowTop
case separatorTypeRowBottom:
return bs.Horizontal.RowBottom
case separatorTypeFooterTop:
return bs.Horizontal.FooterTop
case separatorTypeFooterMiddle:
return bs.Horizontal.FooterMiddle
case separatorTypeFooterBottom:
return bs.Horizontal.FooterBottom
default:
return bs.Horizontal.RowMiddle
}
}
+139
View File
@@ -0,0 +1,139 @@
package table
import "github.com/jedib0t/go-pretty/v6/text"
// ColorOptions defines the ANSI colors to use for parts of the Table.
type ColorOptions struct {
Border text.Colors // borders (if nil, uses one of the below)
Footer text.Colors // footer row(s) colors
Header text.Colors // header row(s) colors
IndexColumn text.Colors // index-column colors (row #, etc.)
Row text.Colors // regular row(s) colors
RowAlternate text.Colors // regular row(s) colors for the even-numbered rows
Separator text.Colors // separators (if nil, uses one of the above)
}
var (
// ColorOptionsDefault defines sensible ANSI color options - basically NONE.
ColorOptionsDefault = ColorOptions{}
// ColorOptionsBright renders dark text on bright background.
ColorOptionsBright = ColorOptionsBlackOnCyanWhite
// ColorOptionsDark renders bright text on dark background.
ColorOptionsDark = ColorOptionsCyanWhiteOnBlack
// ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background.
ColorOptionsBlackOnBlueWhite = ColorOptions{
Footer: text.Colors{text.BgBlue, text.FgBlack},
Header: text.Colors{text.BgHiBlue, text.FgBlack},
IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background.
ColorOptionsBlackOnCyanWhite = ColorOptions{
Footer: text.Colors{text.BgCyan, text.FgBlack},
Header: text.Colors{text.BgHiCyan, text.FgBlack},
IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnGreenWhite renders Black text on Green/White
// background.
ColorOptionsBlackOnGreenWhite = ColorOptions{
Footer: text.Colors{text.BgGreen, text.FgBlack},
Header: text.Colors{text.BgHiGreen, text.FgBlack},
IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White
// background.
ColorOptionsBlackOnMagentaWhite = ColorOptions{
Footer: text.Colors{text.BgMagenta, text.FgBlack},
Header: text.Colors{text.BgHiMagenta, text.FgBlack},
IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnRedWhite renders Black text on Red/White background.
ColorOptionsBlackOnRedWhite = ColorOptions{
Footer: text.Colors{text.BgRed, text.FgBlack},
Header: text.Colors{text.BgHiRed, text.FgBlack},
IndexColumn: text.Colors{text.BgHiRed, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White
// background.
ColorOptionsBlackOnYellowWhite = ColorOptions{
Footer: text.Colors{text.BgYellow, text.FgBlack},
Header: text.Colors{text.BgHiYellow, text.FgBlack},
IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack},
Row: text.Colors{text.BgHiWhite, text.FgBlack},
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
}
// ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background.
ColorOptionsBlueWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgBlue, text.BgHiBlack},
Header: text.Colors{text.FgHiBlue, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background.
ColorOptionsCyanWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgCyan, text.BgHiBlack},
Header: text.Colors{text.FgHiCyan, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsGreenWhiteOnBlack renders Green/White text on Black
// background.
ColorOptionsGreenWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgGreen, text.BgHiBlack},
Header: text.Colors{text.FgHiGreen, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black
// background.
ColorOptionsMagentaWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgMagenta, text.BgHiBlack},
Header: text.Colors{text.FgHiMagenta, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsRedWhiteOnBlack renders Red/White text on Black background.
ColorOptionsRedWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgRed, text.BgHiBlack},
Header: text.Colors{text.FgHiRed, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
// ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black
// background.
ColorOptionsYellowWhiteOnBlack = ColorOptions{
Footer: text.Colors{text.FgYellow, text.BgHiBlack},
Header: text.Colors{text.FgHiYellow, text.BgHiBlack},
IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack},
Row: text.Colors{text.FgHiWhite, text.BgBlack},
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
}
)
+32
View File
@@ -0,0 +1,32 @@
package table
import "github.com/jedib0t/go-pretty/v6/text"
// FormatOptions defines the text-formatting to perform on parts of the Table.
type FormatOptions struct {
Direction text.Direction // (forced) BiDi direction for each Column
Footer text.Format // default text format
FooterAlign text.Align // default horizontal align
FooterVAlign text.VAlign // default vertical align
Header text.Format // default text format
HeaderAlign text.Align // default horizontal align
HeaderVAlign text.VAlign // default vertical align
Row text.Format // default text format
RowAlign text.Align // default horizontal align
RowVAlign text.VAlign // default vertical align
}
var (
// FormatOptionsDefault defines sensible formatting options.
FormatOptionsDefault = FormatOptions{
Footer: text.FormatUpper,
FooterAlign: text.AlignDefault,
FooterVAlign: text.VAlignDefault,
Header: text.FormatUpper,
HeaderAlign: text.AlignDefault,
HeaderVAlign: text.VAlignDefault,
Row: text.FormatDefault,
RowAlign: text.AlignDefault,
RowVAlign: text.VAlignDefault,
}
)
+21
View File
@@ -0,0 +1,21 @@
package table
// HTMLOptions defines the global options to control HTML rendering.
type HTMLOptions struct {
ConvertColorsToSpans bool // convert ANSI escape sequences to HTML <span> tags with CSS classes? EscapeText will be true if this is true.
CSSClass string // CSS class to set on the overall <table> tag
EmptyColumn string // string to replace "" columns with (entire content being "")
EscapeText bool // escape text into HTML-safe content?
Newline string // string to replace "\n" characters with
}
var (
// DefaultHTMLOptions defines sensible HTML rendering defaults.
DefaultHTMLOptions = HTMLOptions{
ConvertColorsToSpans: true,
CSSClass: DefaultHTMLCSSClass,
EmptyColumn: "&nbsp;",
EscapeText: true,
Newline: "<br/>",
}
)
+109
View File
@@ -0,0 +1,109 @@
package table
// Options defines the global options that determine how the Table is
// rendered.
type Options struct {
// DoNotColorBordersAndSeparators disables coloring all the borders and row
// or column separators.
DoNotColorBordersAndSeparators bool
// DoNotRenderSeparatorWhenEmpty disables rendering the separator row after
// headers when there are no data rows (for example when only headers and/or
// footers are present).
DoNotRenderSeparatorWhenEmpty bool
// DrawBorder enables or disables drawing the border around the Table.
// Example of a table where it is disabled:
// # │ FIRST NAME │ LAST NAME │ SALARY │
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
// 1 │ Arya │ Stark │ 3000 │
// 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow!
// 300 │ Tyrion │ Lannister │ 5000 │
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
// │ │ TOTAL │ 10000 │
DrawBorder bool
// SeparateColumns enables or disable drawing border between columns.
// Example of a table where it is disabled:
// ┌─────────────────────────────────────────────────────────────────┐
// │ # FIRST NAME LAST NAME SALARY │
// ├─────────────────────────────────────────────────────────────────┤
// │ 1 Arya Stark 3000 │
// │ 20 Jon Snow 2000 You know nothing, Jon Snow! │
// │ 300 Tyrion Lannister 5000 │
// │ TOTAL 10000 │
// └─────────────────────────────────────────────────────────────────┘
SeparateColumns bool
// SeparateFooter enables or disable drawing border between the footer and
// the rows. Example of a table where it is disabled:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
SeparateFooter bool
// SeparateHeader enables or disable drawing border between the header and
// the rows. Example of a table where it is disabled:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// │ 1 │ Arya │ Stark │ 3000 │ │
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
SeparateHeader bool
// SeparateRows enables or disables drawing separators between each row.
// Example of a table where it is enabled:
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 1 │ Arya │ Stark │ 3000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
// │ │ │ TOTAL │ 10000 │ │
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
SeparateRows bool
}
var (
// OptionsDefault defines sensible global options.
OptionsDefault = Options{
DoNotColorBordersAndSeparators: false,
DrawBorder: true,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
}
// OptionsNoBorders sets up a table without any borders.
OptionsNoBorders = Options{
DoNotColorBordersAndSeparators: false,
DrawBorder: false,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
}
// OptionsNoBordersAndSeparators sets up a table without any borders or
// separators.
OptionsNoBordersAndSeparators = Options{
DoNotColorBordersAndSeparators: false,
DrawBorder: false,
SeparateColumns: false,
SeparateFooter: false,
SeparateHeader: false,
SeparateRows: false,
}
)
+21
View File
@@ -0,0 +1,21 @@
package table
// SizeOptions defines the way to control the width of the table output.
type SizeOptions struct {
// WidthMax is the maximum allotted width for the full row;
// any content beyond this will be truncated using the text
// in Style.Box.UnfinishedRow
WidthMax int
// WidthMin is the minimum allotted width for the full row;
// columns will be auto-expanded until the overall width
// is met
WidthMin int
}
var (
// SizeOptionsDefault defines sensible size options - basically NONE.
SizeOptionsDefault = SizeOptions{
WidthMax: 0,
WidthMin: 0,
}
)
+81
View File
@@ -0,0 +1,81 @@
package table
import "github.com/jedib0t/go-pretty/v6/text"
// TitleOptions defines the way the title text is to be rendered.
type TitleOptions struct {
Align text.Align
Colors text.Colors
Format text.Format
}
var (
// TitleOptionsDefault defines sensible title options - basically NONE.
TitleOptionsDefault = TitleOptions{}
// TitleOptionsBright renders Bright Bold text on Dark background.
TitleOptionsBright = TitleOptionsBlackOnCyan
// TitleOptionsDark renders Dark Bold text on Bright background.
TitleOptionsDark = TitleOptionsCyanOnBlack
// TitleOptionsBlackOnBlue renders Black text on Blue background.
TitleOptionsBlackOnBlue = TitleOptions{
Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold),
}
// TitleOptionsBlackOnCyan renders Black Bold text on Cyan background.
TitleOptionsBlackOnCyan = TitleOptions{
Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold),
}
// TitleOptionsBlackOnGreen renders Black Bold text onGreen background.
TitleOptionsBlackOnGreen = TitleOptions{
Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold),
}
// TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background.
TitleOptionsBlackOnMagenta = TitleOptions{
Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold),
}
// TitleOptionsBlackOnRed renders Black Bold text on Red background.
TitleOptionsBlackOnRed = TitleOptions{
Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold),
}
// TitleOptionsBlackOnYellow renders Black Bold text on Yellow background.
TitleOptionsBlackOnYellow = TitleOptions{
Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold),
}
// TitleOptionsBlueOnBlack renders Blue Bold text on Black background.
TitleOptionsBlueOnBlack = TitleOptions{
Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsCyanOnBlack renders Cyan Bold text on Black background.
TitleOptionsCyanOnBlack = TitleOptions{
Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsGreenOnBlack renders Green Bold text on Black background.
TitleOptionsGreenOnBlack = TitleOptions{
Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background.
TitleOptionsMagentaOnBlack = TitleOptions{
Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsRedOnBlack renders Red Bold text on Black background.
TitleOptionsRedOnBlack = TitleOptions{
Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold),
}
// TitleOptionsYellowOnBlack renders Yellow Bold text on Black background.
TitleOptionsYellowOnBlack = TitleOptions{
Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold),
}
)
+62 -16
View File
@@ -28,6 +28,8 @@ type Table struct {
// columnConfigMap stores the custom-configuration by column
// number and is generated before rendering
columnConfigMap map[int]ColumnConfig
// directionModifier caches the direction modifier string to avoid repeated calls
directionModifier string
// firstRowOfPage tells if the renderer is on the first row of a page?
firstRowOfPage bool
// htmlCSSClass stores the HTML CSS Class to use on the <table> node
@@ -50,6 +52,8 @@ type Table struct {
outputMirror io.Writer
// pager controls how the output is separated into pages
pager pager
// renderMode contains the type of table to render
renderMode renderMode
// rows stores the rows that make up the body (in string form)
rows []rowStr
// rowsColors stores the text.Colors over-rides for each row as defined by
@@ -59,6 +63,8 @@ type Table struct {
rowsConfigMap map[int]RowConfig
// rowsRaw stores the rows that make up the body
rowsRaw []Row
// rowsRawFiltered is the filtered version of rowsRaw
rowsRawFiltered []Row
// rowsFooter stores the rows that make up the footer (in string form)
rowsFooter []rowStr
// rowsFooterConfigs stores RowConfig for each footer row
@@ -76,9 +82,11 @@ type Table struct {
rowPainter RowPainter
// rowPainterWithAttributes is same as rowPainter, but with attributes
rowPainterWithAttributes RowPainterWithAttributes
// rowSeparator is a dummy row that contains the separator columns (dashes
// that make up the separator between header/body/footer
rowSeparator rowStr
// rowSeparators contains the separator columns (dashes that make up the
// separators between title/header/body/footer
rowSeparators map[string]rowStr
// rowSeparatorStrings contains the separator strings for each separator type
rowSeparatorStrings map[separatorType]string
// separators is used to keep track of all rowIndices after which a
// separator has to be rendered
separators map[int]bool
@@ -86,6 +94,8 @@ type Table struct {
sortBy []SortBy
// sortedRowIndices is the output of sorting
sortedRowIndices []int
// filterBy stores the filter criteria
filterBy []FilterBy
// style contains all the strings used to draw the table, and more
style *Style
// suppressEmptyColumns hides columns which have no content on all regular
@@ -127,12 +137,16 @@ func (t *Table) AppendHeader(row Row, config ...RowConfig) {
//
// Only the first item in the "config" will be tagged against this row.
func (t *Table) AppendRow(row Row, config ...RowConfig) {
t.rowsRaw = append(t.rowsRaw, row)
t.rowsRawFiltered = append(t.rowsRawFiltered, row)
// Keep original rows in sync for filtering
rowCopy := make(Row, len(row))
copy(rowCopy, row)
t.rowsRaw = append(t.rowsRaw, rowCopy)
if len(config) > 0 {
if t.rowsConfigMap == nil {
t.rowsConfigMap = make(map[int]RowConfig)
}
t.rowsConfigMap[len(t.rowsRaw)-1] = config[0]
t.rowsConfigMap[len(t.rowsRawFiltered)-1] = config[0]
}
}
@@ -164,11 +178,17 @@ func (t *Table) AppendSeparator() {
if t.separators == nil {
t.separators = make(map[int]bool)
}
if len(t.rowsRaw) > 0 {
t.separators[len(t.rowsRaw)-1] = true
if len(t.rowsRawFiltered) > 0 {
t.separators[len(t.rowsRawFiltered)-1] = true
}
}
// FilterBy sets the rules for filtering the Rows. All filters are applied with
// AND logic (all must match). Filters are applied before sorting.
func (t *Table) FilterBy(filterBy []FilterBy) {
t.filterBy = filterBy
}
// ImportGrid helps import 1d or 2d arrays as rows.
func (t *Table) ImportGrid(grid interface{}) bool {
rows := objAsSlice(grid)
@@ -190,7 +210,7 @@ func (t *Table) ImportGrid(grid interface{}) bool {
// Length returns the number of rows to be rendered.
func (t *Table) Length() int {
return len(t.rowsRaw)
return len(t.rowsRawFiltered)
}
// Pager returns an object that splits the table output into pages and
@@ -231,6 +251,7 @@ func (t *Table) ResetHeaders() {
// ResetRows resets and clears all the rows appended earlier.
func (t *Table) ResetRows() {
t.rowsRawFiltered = nil
t.rowsRaw = nil
t.separators = nil
}
@@ -305,12 +326,12 @@ func (t *Table) SetRowPainter(painter interface{}) {
t.rowPainterWithAttributes = nil
// if called as SetRowPainter(RowPainter(func...))
switch painter.(type) {
switch p := painter.(type) {
case RowPainter:
t.rowPainter = painter.(RowPainter)
t.rowPainter = p
return
case RowPainterWithAttributes:
t.rowPainterWithAttributes = painter.(RowPainterWithAttributes)
t.rowPainterWithAttributes = p
return
}
@@ -367,6 +388,31 @@ func (t *Table) SuppressTrailingSpaces() {
t.suppressTrailingSpaces = true
}
// calculateNumColumnsFromRaw calculates the number of columns from raw rows and headers
func (t *Table) calculateNumColumnsFromRaw() {
t.numColumns = 0
// Check headers first
if len(t.rowsHeaderRaw) > 0 {
for _, headerRow := range t.rowsHeaderRaw {
if len(headerRow) > t.numColumns {
t.numColumns = len(headerRow)
}
}
}
// Check data rows
for _, row := range t.rowsRawFiltered {
if len(row) > t.numColumns {
t.numColumns = len(row)
}
}
// Check footer rows
for _, footerRow := range t.rowsFooterRaw {
if len(footerRow) > t.numColumns {
t.numColumns = len(footerRow)
}
}
}
func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
align := text.AlignDefault
if cfg, ok := t.columnConfigMap[colIdx]; ok {
@@ -505,13 +551,13 @@ func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) stri
if hint.isSeparatorRow {
if hint.isBorderTop {
if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) {
separator = t.style.Box.MiddleHorizontal
separator = t.style.Box.middleHorizontal(hint.separatorType)
} else {
separator = t.style.Box.TopSeparator
}
} else if hint.isBorderBottom {
if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) {
separator = t.style.Box.MiddleHorizontal
separator = t.style.Box.middleHorizontal(hint.separatorType)
} else {
separator = t.style.Box.BottomSeparator
}
@@ -531,7 +577,7 @@ func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelo
}
mergeCurrCol := t.shouldMergeCellsVerticallyAbove(colIdx-1, hint)
return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol)
return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol, hint)
}
func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string {
@@ -546,11 +592,11 @@ func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint ren
return t.style.Box.MiddleSeparator
}
func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string {
func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool, hint renderHint) string {
if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol {
return t.style.Box.EmptySeparator
} else if mergeCellsAbove && mergeCellsBelow {
return t.style.Box.MiddleHorizontal
return t.style.Box.middleHorizontal(hint.separatorType)
} else if mergeCellsAbove {
return t.style.Box.TopSeparator
} else if mergeCellsBelow {
+44
View File
@@ -1,8 +1,10 @@
package table
import (
"fmt"
"reflect"
"sort"
"strconv"
)
// AutoIndexColumnID returns a unique Column ID/Name for the given Column Number.
@@ -26,6 +28,48 @@ func widthEnforcerNone(col string, _ int) string {
return col
}
// convertValueToString converts a value to string using fast type assertions
// for common numeric types before falling back to fmt.Sprint.
//
//gocyclo:ignore
func convertValueToString(v interface{}) string {
switch val := v.(type) {
case int:
return strconv.FormatInt(int64(val), 10)
case int8:
return strconv.FormatInt(int64(val), 10)
case int16:
return strconv.FormatInt(int64(val), 10)
case int32:
return strconv.FormatInt(int64(val), 10)
case int64:
return strconv.FormatInt(val, 10)
case uint:
return strconv.FormatUint(uint64(val), 10)
case uint8:
return strconv.FormatUint(uint64(val), 10)
case uint16:
return strconv.FormatUint(uint64(val), 10)
case uint32:
return strconv.FormatUint(uint64(val), 10)
case uint64:
return strconv.FormatUint(val, 10)
case float32:
return strconv.FormatFloat(float64(val), 'g', -1, 32)
case float64:
return strconv.FormatFloat(val, 'g', -1, 64)
case bool:
if val {
return "true"
}
return "false"
case string:
return val
default:
return fmt.Sprint(v)
}
}
// isNumber returns true if the argument is a numeric type; false otherwise.
func isNumber(x interface{}) bool {
if x == nil {
+151
View File
@@ -0,0 +1,151 @@
package table
import (
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
// convertEscSequencesToSpans converts ANSI escape sequences to HTML <span> tags with CSS classes.
func convertEscSequencesToSpans(str string) string {
converter := newEscSeqToSpanConverter()
return converter.Convert(str)
}
// escSeqToSpanConverter converts ANSI escape sequences to HTML <span> tags with CSS classes.
type escSeqToSpanConverter struct {
result strings.Builder
esp text.EscSeqParser
currentColors map[int]bool
}
// newEscSeqToSpanConverter creates a new escape sequence to span converter.
func newEscSeqToSpanConverter() *escSeqToSpanConverter {
return &escSeqToSpanConverter{
currentColors: make(map[int]bool),
}
}
// Convert converts ANSI escape sequences in the string to HTML <span> tags with CSS classes.
func (c *escSeqToSpanConverter) Convert(str string) string {
c.reset()
// Process the string character by character
for _, char := range str {
wasInSequence := c.esp.InSequence()
c.esp.Consume(char)
if c.esp.InSequence() {
// We're inside an escape sequence, skip it (don't write to result)
continue
}
if wasInSequence {
// We just finished an escape sequence, update colors
newColors := make(map[int]bool)
for _, code := range c.esp.Codes() {
newColors[code] = true
}
c.updateSpan(newColors)
} else {
// Regular character, escape it for HTML safety and write it
// (will be inside current span if colors are active)
c.writeEscapedRune(char)
}
}
// Close any open span
if len(c.currentColors) > 0 {
c.result.WriteString("</span>")
}
return c.result.String()
}
// clearColors clears the current color tracking.
func (c *escSeqToSpanConverter) clearColors() {
c.currentColors = make(map[int]bool)
}
// closeSpan closes the current span if one is open.
func (c *escSeqToSpanConverter) closeSpan() {
if len(c.currentColors) > 0 {
c.result.WriteString("</span>")
}
}
// colorsChanged checks if the color set has changed.
func (c *escSeqToSpanConverter) colorsChanged(newColors map[int]bool) bool {
// we never set the map values to false, so a simple size compare is enough
return len(c.currentColors) != len(newColors)
}
// cssClasses converts color codes to CSS class names.
func (c *escSeqToSpanConverter) cssClasses(codes map[int]bool) string {
var colors text.Colors
for code := range codes {
colors = append(colors, text.Color(code))
}
return colors.CSSClasses()
}
// openSpan opens a new span with the given CSS class and tracks the colors.
func (c *escSeqToSpanConverter) openSpan(class string, newColors map[int]bool) {
c.result.WriteString("<span class=\"")
c.result.WriteString(class)
c.result.WriteString("\">")
// Track colors since we opened a span
c.currentColors = make(map[int]bool)
for code := range newColors {
c.currentColors[code] = true
}
}
// reset initializes the converter state for a new conversion.
func (c *escSeqToSpanConverter) reset() {
c.result.Reset()
c.esp = text.EscSeqParser{}
c.currentColors = make(map[int]bool)
}
// updateSpan updates span tags when colors change.
func (c *escSeqToSpanConverter) updateSpan(newColors map[int]bool) {
if !c.colorsChanged(newColors) {
return
}
c.closeSpan()
// Open new span if there are colors with valid CSS classes
if len(newColors) > 0 {
class := c.cssClasses(newColors)
if class != "" {
c.openSpan(class, newColors)
} else {
// No CSS classes, so don't track these colors
c.clearColors()
}
} else {
// No colors, clear tracking
c.clearColors()
}
}
// writeEscapedRune writes a rune to the result, escaping it if necessary for HTML safety.
func (c *escSeqToSpanConverter) writeEscapedRune(char rune) {
switch char {
case '<':
c.result.WriteString("&lt;")
case '>':
c.result.WriteString("&gt;")
case '&':
c.result.WriteString("&amp;")
case '"':
c.result.WriteString("&#34;")
case '\'':
c.result.WriteString("&#39;")
default:
// Most characters don't need escaping, write directly
c.result.WriteRune(char)
}
}
+1
View File
@@ -11,6 +11,7 @@ type Writer interface {
AppendRow(row Row, configs ...RowConfig)
AppendRows(rows []Row, configs ...RowConfig)
AppendSeparator()
FilterBy(filterBy []FilterBy)
ImportGrid(grid interface{}) bool
Length() int
Pager(opts ...PagerOption) Pager
+138 -5
View File
@@ -1,8 +1,141 @@
# text
# Text
[![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6/text.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/text)
[![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/text)
Package with utility functions to manipulate strings/text.
Package with utility functions to manipulate strings/text with full support for
ANSI escape sequences (colors, formatting, etc.).
Used heavily in the other packages in this repo ([list](../list),
[progress](../progress), and [table](../table)).
[progress](../progress), and [table](../table)).
## Features
### Colors & Formatting
- **ANSI Color Support** - Full support for terminal colors and formatting
- Foreground colors (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White)
- Background colors (matching foreground set)
- Hi-intensity variants for both foreground and background
- **256-color palette support** - Extended color support for terminals
- Standard 16 colors (0-15)
- RGB cube colors (16-231) - 216 colors organized in a 6x6x6 cube
- Grayscale colors (232-255) - 24 shades of gray
- Helper functions: `Fg256Color(index)`, `Bg256Color(index)`, `Fg256RGB(r, g, b)`, `Bg256RGB(r, g, b)`
- Text attributes (Bold, Faint, Italic, Underline, Blink, Reverse, Concealed, CrossedOut)
- Automatic color detection based on environment variables (`NO_COLOR`, `FORCE_COLOR`, `TERM`)
- Global enable/disable functions for colors
- Cached escape sequences for performance
- **Text Formatting** - Transform text while preserving escape sequences
- `FormatDefault` - No transformation
- `FormatLower` - Convert to lowercase
- `FormatTitle` - Convert to title case
- `FormatUpper` - Convert to uppercase
- **HTML Support** - Generate HTML class attributes for colors
- **Color Combinations** - Combine multiple colors and attributes
### Alignment
- **Horizontal Alignment**
- `AlignDefault` / `AlignLeft` - Left-align text
- `AlignCenter` - Center-align text
- `AlignRight` - Right-align text
- `AlignJustify` - Justify text (distribute spaces between words)
- `AlignAuto` - Auto-detect: right-align numbers, left-align text
- HTML and Markdown property generation for alignment
- **Vertical Alignment**
- `VAlignTop` - Align to top
- `VAlignMiddle` - Align to middle
- `VAlignBottom` - Align to bottom
- Works with both string arrays and multi-line strings
- HTML property generation for vertical alignment
### Text Wrapping
- **WrapHard** - Hard wrap at specified length, breaks words if needed
- Handles ANSI escape sequences without breaking formatting
- Preserves paragraph breaks
- **WrapSoft** - Soft wrap at specified length, tries to keep words intact
- Handles ANSI escape sequences without breaking formatting
- Preserves paragraph breaks
- **WrapText** - Similar to WrapHard but also respects line breaks
- Handles ANSI escape sequences without breaking formatting
### String Utilities
- **Width Calculation**
- `StringWidth` - Calculate display width of string (including escape sequences)
- `StringWidthWithoutEscSequences` - Calculate display width ignoring escape sequences
- `RuneWidth` - Calculate display width of a single rune (handles East Asian characters)
- `LongestLineLen` - Find the longest line in a multi-line string
- **String Manipulation**
- `Trim` - Trim string to specified length while preserving escape sequences
- `Pad` - Pad string to specified length with a character
- `Snip` - Snip string to specified length with an indicator (e.g., "~")
- `RepeatAndTrim` - Repeat string until it reaches specified length
- `InsertEveryN` - Insert a character every N characters
- `ProcessCRLF` - Process carriage returns and line feeds correctly
- `Widen` - Convert half-width characters to full-width
- **Escape Sequence Handling**
- All functions properly handle ANSI escape sequences
- Escape sequences are preserved during transformations
- Width calculations ignore escape sequences
- `EscSeqParser` - Parser for advanced escape sequence parsing and tracking
- Supports both CSI (Control Sequence Introducer) and OSI (Operating System Command) formats
- Tracks active formatting codes and can generate consolidated escape sequences
- Full support for 256-color escape sequences (`\x1b[38;5;n`m` and `\x1b[48;5;n`m`)
### Cursor Control
- Move cursor in all directions
- `CursorUp` - Move cursor up N lines
- `CursorDown` - Move cursor down N lines
- `CursorLeft` - Move cursor left N characters
- `CursorRight` - Move cursor right N characters
- `EraseLine` - Erase all characters to the right of cursor
- Generate ANSI escape sequences for terminal cursor manipulation
### Hyperlinks
- **Terminal Hyperlinks** - Create clickable hyperlinks in supported terminals
- Uses OSC 8 escape sequences
- Format: `Hyperlink(url, text)`
- Falls back to plain text in unsupported terminals
### Transformers
- **Number Transformer** - Format numbers with colors
- Positive numbers colored green
- Negative numbers colored red
- Custom format string support (e.g., `%.2f`)
- Supports all numeric types (int, uint, float)
- **JSON Transformer** - Pretty-print JSON strings or objects
- Customizable indentation (prefix and indent string)
- Validates JSON before formatting
- **Time Transformer** - Format time.Time objects
- Custom layout support (e.g., `time.RFC3339`)
- Timezone localization support
- Auto-detects common time formats from strings
- **Unix Time Transformer** - Format Unix timestamps
- Handles seconds, milliseconds, microseconds, and nanoseconds
- Auto-detects timestamp unit based on value
- Timezone localization support
- **URL Transformer** - Format URLs with styling
- Underlined and colored blue by default
- Custom color support
### Text Direction
- **Bidirectional Text Support**
- `LeftToRight` - Force left-to-right text direction
- `RightToLeft` - Force right-to-left text direction
- Uses Unicode directional markers
### Filtering
- **String Filtering** - Filter string slices with custom functions
- `Filter(slice, predicate)` - Returns filtered slice
### East Asian Character Support
- Proper width calculation for East Asian characters (full-width, half-width)
- Configurable East Asian width handling via `OverrideRuneWidthEastAsianWidth()`
- Handles mixed character sets correctly
+12 -8
View File
@@ -15,18 +15,22 @@ var ANSICodesSupported = areANSICodesSupported()
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
func Escape(str string, escapeSeq string) string {
out := ""
var out strings.Builder
// Estimate capacity: original string + escape sequences
out.Grow(len(str) + len(escapeSeq)*3 + len(EscapeReset)*2)
if !strings.HasPrefix(str, EscapeStart) {
out += escapeSeq
out.WriteString(escapeSeq)
}
out += strings.Replace(str, EscapeReset, EscapeReset+escapeSeq, -1)
if !strings.HasSuffix(out, EscapeReset) {
out += EscapeReset
out.WriteString(strings.ReplaceAll(str, EscapeReset, EscapeReset+escapeSeq))
if !strings.HasSuffix(out.String(), EscapeReset) {
out.WriteString(EscapeReset)
}
if strings.Contains(out, escapeSeq+EscapeReset) {
out = strings.Replace(out, escapeSeq+EscapeReset, "", -1)
result := out.String()
if strings.Contains(result, escapeSeq+EscapeReset) {
result = strings.ReplaceAll(result, escapeSeq+EscapeReset, "")
}
return out
return result
}
// StripEscape strips all ANSI Escape Sequence from the string.
+7 -1
View File
@@ -3,6 +3,12 @@
package text
import "os"
func areANSICodesSupported() bool {
return true
// On Unix systems, ANSI codes are generally supported unless TERM is "dumb"
// This is a basic check; 256-color sequences are ANSI sequences and will
// be handled by terminals that support them (or ignored by those that don't)
term := os.Getenv("TERM")
return term != "dumb"
}
+183 -23
View File
@@ -9,6 +9,7 @@ import (
"sync"
)
// colorsEnabled is true if colors are enabled and supported by the terminal.
var colorsEnabled = areColorsOnInTheEnv() && areANSICodesSupported()
// DisableColors (forcefully) disables color coding globally.
@@ -21,13 +22,24 @@ func EnableColors() {
colorsEnabled = true
}
// areColorsOnInTheEnv returns true is colors are not disable using
// areColorsOnInTheEnv returns true if colors are not disabled using
// well known environment variables.
func areColorsOnInTheEnv() bool {
if os.Getenv("FORCE_COLOR") == "1" {
// FORCE_COLOR takes precedence - if set to a truthy value, enable colors
forceColor := os.Getenv("FORCE_COLOR")
if forceColor != "" && forceColor != "0" && forceColor != "false" {
return true
}
return os.Getenv("NO_COLOR") == "" || os.Getenv("NO_COLOR") == "0"
// NO_COLOR: if set to any non-empty value (except "0"), disable colors
// Note: "0" is treated as "not set" to allow explicit enabling via NO_COLOR=0
noColor := os.Getenv("NO_COLOR")
if noColor != "" && noColor != "0" {
return false
}
// Default: check TERM - if not "dumb", assume colors are supported
return os.Getenv("TERM") != "dumb"
}
// The logic here is inspired from github.com/fatih/color; the following is
@@ -103,18 +115,62 @@ const (
BgHiWhite
)
// 256-color support
// Internal encoding for 256-color codes (used by escape_seq_parser.go):
// Foreground 256-color: fg256Start + colorIndex (1000-1255)
// Background 256-color: bg256Start + colorIndex (2000-2255)
const (
// fg256Start is the base value for 256-color foreground colors.
// Use Fg256Color(index) to create a 256-color foreground color.
fg256Start Color = 1000
// bg256Start is the base value for 256-color background colors.
// Use Bg256Color(index) to create a 256-color background color.
bg256Start Color = 2000
)
// CSSClasses returns the CSS class names for the color.
func (c Color) CSSClasses() string {
// Check for 256-color and convert to RGB-based class
if c >= fg256Start && c < fg256Start+256 {
colorIndex := int(c - fg256Start)
r, g, b := color256ToRGB(colorIndex)
return fmt.Sprintf("fg-256-%d-%d-%d", r, g, b)
}
if c >= bg256Start && c < bg256Start+256 {
colorIndex := int(c - bg256Start)
r, g, b := color256ToRGB(colorIndex)
return fmt.Sprintf("bg-256-%d-%d-%d", r, g, b)
}
// Existing behavior for standard colors
if class, ok := colorCSSClassMap[c]; ok {
return class
}
return ""
}
// EscapeSeq returns the ANSI escape sequence for the color.
func (c Color) EscapeSeq() string {
// Check if it's a 256-color foreground (1000-1255)
if c >= fg256Start && c < fg256Start+256 {
colorIndex := int(c - fg256Start)
return fmt.Sprintf("%s38;5;%d%s", EscapeStart, colorIndex, EscapeStop)
}
// Check if it's a 256-color background (2000-2255)
if c >= bg256Start && c < bg256Start+256 {
colorIndex := int(c - bg256Start)
return fmt.Sprintf("%s48;5;%d%s", EscapeStart, colorIndex, EscapeStop)
}
// Regular color (existing behavior)
return EscapeStart + strconv.Itoa(int(c)) + EscapeStop
}
// HTMLProperty returns the "class" attribute for the color.
func (c Color) HTMLProperty() string {
out := ""
if class, ok := colorCSSClassMap[c]; ok {
out = fmt.Sprintf("class=\"%s\"", class)
classes := c.CSSClasses()
if classes == "" {
return ""
}
return out
return fmt.Sprintf("class=\"%s\"", classes)
}
// Sprint colorizes and prints the given string(s).
@@ -134,6 +190,25 @@ type Colors []Color
// colorsSeqMap caches the escape sequence for a set of colors
var colorsSeqMap = sync.Map{}
// CSSClasses returns the CSS class names for the colors.
func (c Colors) CSSClasses() string {
if len(c) == 0 {
return ""
}
var classes []string
for _, color := range c {
class := color.CSSClasses()
if class != "" {
classes = append(classes, class)
}
}
if len(classes) > 1 {
sort.Strings(classes)
}
return strings.Join(classes, " ")
}
// EscapeSeq returns the ANSI escape sequence for the colors set.
func (c Colors) EscapeSeq() string {
if len(c) == 0 {
@@ -143,32 +218,39 @@ func (c Colors) EscapeSeq() string {
colorsKey := fmt.Sprintf("%#v", c)
escapeSeq, ok := colorsSeqMap.Load(colorsKey)
if !ok || escapeSeq == "" {
colorNums := make([]string, len(c))
for idx, color := range c {
colorNums[idx] = strconv.Itoa(int(color))
codes := make([]string, 0, len(c))
for _, color := range c {
codes = append(codes, c.colorToCode(color))
}
escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop
escapeSeq = EscapeStart + strings.Join(codes, ";") + EscapeStop
colorsSeqMap.Store(colorsKey, escapeSeq)
}
return escapeSeq.(string)
}
// colorToCode converts a Color to its escape sequence code string.
func (c Colors) colorToCode(color Color) string {
// Check if it's a 256-color foreground (1000-1255)
if color >= fg256Start && color < fg256Start+256 {
colorIndex := int(color - fg256Start)
return fmt.Sprintf("38;5;%d", colorIndex)
}
// Check if it's a 256-color background (2000-2255)
if color >= bg256Start && color < bg256Start+256 {
colorIndex := int(color - bg256Start)
return fmt.Sprintf("48;5;%d", colorIndex)
}
// Regular color
return strconv.Itoa(int(color))
}
// HTMLProperty returns the "class" attribute for the colors.
func (c Colors) HTMLProperty() string {
if len(c) == 0 {
classes := c.CSSClasses()
if classes == "" {
return ""
}
var classes []string
for _, color := range c {
if class, ok := colorCSSClassMap[color]; ok {
classes = append(classes, class)
}
}
if len(classes) > 1 {
sort.Strings(classes)
}
return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " "))
return fmt.Sprintf("class=\"%s\"", classes)
}
// Sprint colorizes and prints the given string(s).
@@ -187,3 +269,81 @@ func colorize(s string, escapeSeq string) string {
}
return Escape(s, escapeSeq)
}
// Fg256Color returns a foreground 256-color Color value.
// The index must be in the range 0-255.
func Fg256Color(index int) Color {
if index < 0 || index > 255 {
return Reset
}
return fg256Start + Color(index)
}
// Bg256Color returns a background 256-color Color value.
// The index must be in the range 0-255.
func Bg256Color(index int) Color {
if index < 0 || index > 255 {
return Reset
}
return bg256Start + Color(index)
}
// Fg256RGB returns a foreground 256-color from RGB values in the 6x6x6 color cube.
// Each RGB component must be in the range 0-5.
// The resulting color index will be in the range 16-231.
func Fg256RGB(r, g, b int) Color {
if r < 0 || r > 5 || g < 0 || g > 5 || b < 0 || b > 5 {
return Reset
}
index := 16 + (r*36 + g*6 + b)
return Fg256Color(index)
}
// Bg256RGB returns a background 256-color from RGB values in the 6x6x6 color cube.
// Each RGB component must be in the range 0-5.
// The resulting color index will be in the range 16-231.
func Bg256RGB(r, g, b int) Color {
if r < 0 || r > 5 || g < 0 || g > 5 || b < 0 || b > 5 {
return Reset
}
index := 16 + (r*36 + g*6 + b)
return Bg256Color(index)
}
// color256ToRGB converts a 256-color index to RGB values.
// Returns (r, g, b) values in the range 0-255.
func color256ToRGB(index int) (r, g, b int) {
if index < 16 {
// Standard 16 colors - map to predefined RGB values
standardColors := [16][3]int{
{0, 0, 0}, // 0: black
{128, 0, 0}, // 1: red
{0, 128, 0}, // 2: green
{128, 128, 0}, // 3: yellow
{0, 0, 128}, // 4: blue
{128, 0, 128}, // 5: magenta
{0, 128, 128}, // 6: cyan
{192, 192, 192}, // 7: light gray
{128, 128, 128}, // 8: dark gray
{255, 0, 0}, // 9: bright red
{0, 255, 0}, // 10: bright green
{255, 255, 0}, // 11: bright yellow
{0, 0, 255}, // 12: bright blue
{255, 0, 255}, // 13: bright magenta
{0, 255, 255}, // 14: bright cyan
{255, 255, 255}, // 15: white
}
return standardColors[index][0], standardColors[index][1], standardColors[index][2]
} else if index < 232 {
// 216-color RGB cube (16-231)
index -= 16
r = (index / 36) * 51
g = ((index / 6) % 6) * 51
b = (index % 6) * 51
} else {
// 24 grayscale colors (232-255)
gray := 8 + (index-232)*10
r, g, b = gray, gray, gray
}
return
}
+323 -110
View File
@@ -42,73 +42,7 @@ const (
escSeqKindOSI
)
type escSeqParser struct {
codes map[int]bool
// consume specific
inEscSeq bool
escSeqKind escSeqKind
escapeSeq string
}
func (s *escSeqParser) Codes() []int {
codes := make([]int, 0)
for code, val := range s.codes {
if val {
codes = append(codes, code)
}
}
sort.Ints(codes)
return codes
}
func (s *escSeqParser) Consume(char rune) {
if !s.inEscSeq && char == EscapeStartRune {
s.inEscSeq = true
s.escSeqKind = escSeqKindUnknown
s.escapeSeq = ""
} else if s.inEscSeq && s.escSeqKind == escSeqKindUnknown {
if char == EscapeStartRuneCSI {
s.escSeqKind = escSeqKindCSI
} else if char == EscapeStartRuneOSI {
s.escSeqKind = escSeqKindOSI
}
}
if s.inEscSeq {
s.escapeSeq += string(char)
// --- FIX for OSC 8 hyperlinks (e.g. \x1b]8;;url\x07label\x1b]8;;\x07)
if s.escSeqKind == escSeqKindOSI &&
strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) &&
char == '\a' { // BEL
s.ParseSeq(s.escapeSeq, s.escSeqKind)
s.Reset()
return
}
if s.isEscapeStopRune(char) {
s.ParseSeq(s.escapeSeq, s.escSeqKind)
s.Reset()
}
}
}
func (s *escSeqParser) InSequence() bool {
return s.inEscSeq
}
func (s *escSeqParser) IsOpen() bool {
return len(s.codes) > 0
}
func (s *escSeqParser) Reset() {
s.inEscSeq = false
s.escSeqKind = escSeqKindUnknown
s.escapeSeq = ""
}
// private constants
const (
escCodeResetAll = 0
escCodeResetIntensity = 22
@@ -126,50 +60,132 @@ const (
escCodeReverse = 7
escCodeConceal = 8
escCodeCrossedOut = 9
// conceal OSI sequences
escapeStartConcealOSI = "\x1b]8;"
escapeStopConcealOSI = "\x1b\\"
)
func (s *escSeqParser) ParseSeq(seq string, seqKind escSeqKind) {
if s.codes == nil {
s.codes = make(map[int]bool)
// 256-color codes
const (
escCode256FgStart = 38
escCode256BgStart = 48
escCode256Color = 5
escCodeResetFg = 39
escCodeResetBg = 49
escCode256Max = 255
)
// Internal encoding for 256-color codes uses fg256Start and bg256Start from color.go
// Private constants initialized from private constants to avoid repeated casting in hot paths
// Foreground 256-color: fg256Start + colorIndex (1000-1255)
// Background 256-color: bg256Start + colorIndex (2000-2255)
const (
escCode256FgBase = int(fg256Start) // 1000
escCode256BgBase = int(bg256Start) // 2000
)
// Standard color code ranges
const (
// Standard foreground colors (30-37)
escCodeFgStdStart = 30
escCodeFgStdEnd = 37
// Bright foreground colors (90-97)
escCodeFgBrightStart = 90
escCodeFgBrightEnd = 97
// Standard background colors (40-47)
escCodeBgStdStart = 40
escCodeBgStdEnd = 47
// Bright background colors (100-107)
escCodeBgBrightStart = 100
escCodeBgBrightEnd = 107
)
// Special characters
const (
escRuneBEL = '\a' // BEL character (ASCII 7)
)
// EscSeqParser parses ANSI escape sequences from text and tracks active formatting codes.
// It supports both CSI (Control Sequence Introducer) and OSI (Operating System Command)
// escape sequence formats.
type EscSeqParser struct {
// codes tracks active escape sequence codes (e.g., 1 for bold, 3 for italic).
codes map[int]bool
// inEscSeq indicates whether the parser is currently inside an escape sequence.
inEscSeq bool
// escSeqKind identifies the type of escape sequence being parsed (CSI or OSI).
escSeqKind escSeqKind
// escapeSeq accumulates the current escape sequence being parsed.
escapeSeq string
}
func (s *EscSeqParser) Codes() []int {
codes := make([]int, 0)
for code, val := range s.codes {
if val {
codes = append(codes, code)
}
}
sort.Ints(codes)
return codes
}
func (s *EscSeqParser) Consume(char rune) {
if !s.inEscSeq && char == EscapeStartRune {
s.inEscSeq = true
s.escSeqKind = escSeqKindUnknown
s.escapeSeq = ""
} else if s.inEscSeq && s.escSeqKind == escSeqKindUnknown {
switch char {
case EscapeStartRuneCSI:
s.escSeqKind = escSeqKindCSI
case EscapeStartRuneOSI:
s.escSeqKind = escSeqKindOSI
}
}
if seqKind == escSeqKindOSI {
seq = strings.Replace(seq, EscapeStartOSI, "", 1)
seq = strings.Replace(seq, EscapeStopOSI, "", 1)
} else { // escSeqKindCSI
seq = strings.Replace(seq, EscapeStartCSI, "", 1)
seq = strings.Replace(seq, EscapeStopCSI, "", 1)
}
if s.inEscSeq {
s.escapeSeq += string(char)
codes := strings.Split(seq, ";")
for _, code := range codes {
code = strings.TrimSpace(code)
if codeNum, err := strconv.Atoi(code); err == nil {
switch codeNum {
case escCodeResetAll:
s.codes = make(map[int]bool) // clear everything
case escCodeResetIntensity:
delete(s.codes, escCodeBold)
delete(s.codes, escCodeDim)
case escCodeResetItalic:
delete(s.codes, escCodeItalic)
case escCodeResetUnderline:
delete(s.codes, escCodeUnderline)
case escCodeResetBlink:
delete(s.codes, escCodeBlinkSlow)
delete(s.codes, escCodeBlinkRapid)
case escCodeResetReverse:
delete(s.codes, escCodeReverse)
case escCodeResetCrossedOut:
delete(s.codes, escCodeCrossedOut)
default:
s.codes[codeNum] = true
}
// --- FIX for OSC 8 hyperlinks (e.g. \x1b]8;;url\x07label\x1b]8;;\x07)
if s.escSeqKind == escSeqKindOSI &&
strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) &&
char == escRuneBEL { // BEL
s.ParseSeq(s.escapeSeq, s.escSeqKind)
s.Reset()
return
}
if s.isEscapeStopRune(char) {
s.ParseSeq(s.escapeSeq, s.escSeqKind)
s.Reset()
}
}
}
func (s *escSeqParser) ParseString(str string) string {
func (s *EscSeqParser) InSequence() bool {
return s.inEscSeq
}
func (s *EscSeqParser) IsOpen() bool {
return len(s.codes) > 0
}
func (s *EscSeqParser) ParseSeq(seq string, seqKind escSeqKind) {
if s.codes == nil {
s.codes = make(map[int]bool)
}
seq = s.stripEscapeSequence(seq, seqKind)
codes := s.splitAndTrimCodes(seq)
processed256ColorIndices := s.process256ColorSequences(codes)
s.processRegularCodes(codes, processed256ColorIndices)
}
func (s *EscSeqParser) ParseString(str string) string {
s.escapeSeq, s.inEscSeq, s.escSeqKind = "", false, escSeqKindUnknown
for _, char := range str {
s.Consume(char)
@@ -177,15 +193,33 @@ func (s *escSeqParser) ParseString(str string) string {
return s.Sequence()
}
func (s *escSeqParser) Sequence() string {
func (s *EscSeqParser) Reset() {
s.inEscSeq = false
s.escSeqKind = escSeqKindUnknown
s.escapeSeq = ""
}
func (s *EscSeqParser) Sequence() string {
out := strings.Builder{}
if s.IsOpen() {
out.WriteString(EscapeStart)
for idx, code := range s.Codes() {
codes := s.Codes()
for idx, code := range codes {
if idx > 0 {
out.WriteRune(';')
}
out.WriteString(fmt.Sprint(code))
// Check if this is a 256-color foreground code (1000-1255)
if code >= escCode256FgBase && code <= escCode256FgBase+escCode256Max {
colorIndex := code - escCode256FgBase
out.WriteString(fmt.Sprintf("%d;%d;%d", escCode256FgStart, escCode256Color, colorIndex))
} else if code >= escCode256BgBase && code <= escCode256BgBase+escCode256Max {
// 256-color background code (2000-2255)
colorIndex := code - escCode256BgBase
out.WriteString(fmt.Sprintf("%d;%d;%d", escCode256BgStart, escCode256Color, colorIndex))
} else {
// Regular code
out.WriteString(fmt.Sprint(code))
}
}
out.WriteString(EscapeStop)
}
@@ -193,12 +227,54 @@ func (s *escSeqParser) Sequence() string {
return out.String()
}
const (
escapeStartConcealOSI = "\x1b]8;"
escapeStopConcealOSI = "\x1b\\"
)
// clearAllBackgroundColors clears all background color codes.
func (s *EscSeqParser) clearAllBackgroundColors() {
for code := escCodeBgStdStart; code <= escCodeBgStdEnd; code++ {
delete(s.codes, code)
}
for code := escCodeBgBrightStart; code <= escCodeBgBrightEnd; code++ {
delete(s.codes, code)
}
for code := escCode256BgBase; code <= escCode256BgBase+escCode256Max; code++ {
delete(s.codes, code)
}
}
func (s *escSeqParser) isEscapeStopRune(char rune) bool {
// clearAllForegroundColors clears all foreground color codes.
func (s *EscSeqParser) clearAllForegroundColors() {
for code := escCodeFgStdStart; code <= escCodeFgStdEnd; code++ {
delete(s.codes, code)
}
for code := escCodeFgBrightStart; code <= escCodeFgBrightEnd; code++ {
delete(s.codes, code)
}
for code := escCode256FgBase; code <= escCode256FgBase+escCode256Max; code++ {
delete(s.codes, code)
}
}
// clearColorRange clears standard foreground or background colors.
func (s *EscSeqParser) clearColorRange(isForeground bool) {
if isForeground {
// Clear standard foreground colors (30-37, 90-97)
for code := escCodeFgStdStart; code <= escCodeFgStdEnd; code++ {
delete(s.codes, code)
}
for code := escCodeFgBrightStart; code <= escCodeFgBrightEnd; code++ {
delete(s.codes, code)
}
} else {
// Clear standard background colors (40-47, 100-107)
for code := escCodeBgStdStart; code <= escCodeBgStdEnd; code++ {
delete(s.codes, code)
}
for code := escCodeBgBrightStart; code <= escCodeBgBrightEnd; code++ {
delete(s.codes, code)
}
}
}
func (s *EscSeqParser) isEscapeStopRune(char rune) bool {
if strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) {
if strings.HasSuffix(s.escapeSeq, escapeStopConcealOSI) {
return true
@@ -209,3 +285,140 @@ func (s *escSeqParser) isEscapeStopRune(char rune) bool {
}
return false
}
// isRegularCode checks if a code is a regular code (not a 256-color encoded value).
func (s *EscSeqParser) isRegularCode(codeNum int) bool {
return codeNum < escCode256FgBase || codeNum > escCode256BgBase+escCode256Max
}
// parse256ColorSequence attempts to parse a 256-color sequence starting at index i.
// Returns (colorIndex, base, true) if valid, or (0, 0, false) if not.
func (s *EscSeqParser) parse256ColorSequence(codes []string, i int) (colorIndex int, base int, ok bool) {
if i+2 >= len(codes) {
return 0, 0, false
}
codeNum, err := strconv.Atoi(codes[i])
if err != nil {
return 0, 0, false
}
var expectedBase int
switch codeNum {
case escCode256FgStart:
expectedBase = escCode256FgBase
case escCode256BgStart:
expectedBase = escCode256BgBase
default:
return 0, 0, false
}
nextCode, err := strconv.Atoi(codes[i+1])
if err != nil || nextCode != escCode256Color {
return 0, 0, false
}
colorIndex, err = strconv.Atoi(codes[i+2])
if err != nil || colorIndex < 0 || colorIndex > escCode256Max {
return 0, 0, false
}
return colorIndex, expectedBase, true
}
// process256ColorSequences processes 256-color sequences (38;5;n or 48;5;n) and returns
// a map of indices that were part of valid 256-color sequences.
func (s *EscSeqParser) process256ColorSequences(codes []string) map[int]bool {
processedIndices := make(map[int]bool)
for i := 0; i < len(codes); i++ {
if colorIndex, base, ok := s.parse256ColorSequence(codes, i); ok {
s.set256Color(base, colorIndex)
s.clearColorRange(base == escCode256FgBase)
processedIndices[i] = true
processedIndices[i+1] = true
processedIndices[i+2] = true
i += 2 // Skip i+1 and i+2 (loop will increment to i+3)
}
}
return processedIndices
}
// processCode handles a single escape code.
func (s *EscSeqParser) processCode(codeNum int) {
switch codeNum {
case escCodeResetAll:
s.codes = make(map[int]bool)
case escCodeResetIntensity:
delete(s.codes, escCodeBold)
delete(s.codes, escCodeDim)
case escCodeResetItalic:
delete(s.codes, escCodeItalic)
case escCodeResetUnderline:
delete(s.codes, escCodeUnderline)
case escCodeResetBlink:
delete(s.codes, escCodeBlinkSlow)
delete(s.codes, escCodeBlinkRapid)
case escCodeResetReverse:
delete(s.codes, escCodeReverse)
case escCodeResetCrossedOut:
delete(s.codes, escCodeCrossedOut)
case escCodeResetFg:
s.clearAllForegroundColors()
case escCodeResetBg:
s.clearAllBackgroundColors()
default:
if s.isRegularCode(codeNum) {
s.codes[codeNum] = true
}
}
}
// processRegularCodes processes regular escape codes and reset codes.
func (s *EscSeqParser) processRegularCodes(codes []string, processedIndices map[int]bool) {
for i, code := range codes {
if processedIndices[i] {
continue
}
codeNum, err := strconv.Atoi(code)
if err != nil {
continue
}
s.processCode(codeNum)
}
}
// set256Color sets a 256-color code and clears conflicting colors.
func (s *EscSeqParser) set256Color(base int, colorIndex int) {
encodedValue := base + colorIndex
s.codes[encodedValue] = true
// Clear other colors in the same range
for code := base; code <= base+escCode256Max; code++ {
if code != encodedValue {
delete(s.codes, code)
}
}
}
// splitAndTrimCodes splits the sequence by semicolons and trims whitespace.
func (s *EscSeqParser) splitAndTrimCodes(seq string) []string {
codes := strings.Split(seq, ";")
for i := range codes {
codes[i] = strings.TrimSpace(codes[i])
}
return codes
}
// stripEscapeSequence removes escape sequence markers from the input string.
func (s *EscSeqParser) stripEscapeSequence(seq string, seqKind escSeqKind) string {
if seqKind == escSeqKindOSI {
seq = strings.Replace(seq, EscapeStartOSI, "", 1)
seq = strings.Replace(seq, EscapeStopOSI, "", 1)
} else {
seq = strings.Replace(seq, EscapeStartCSI, "", 1)
seq = strings.Replace(seq, EscapeStopCSI, "", 1)
}
return seq
}
+32 -17
View File
@@ -28,7 +28,7 @@ func InsertEveryN(str string, runeToInsert rune, n int) string {
sLen := StringWidthWithoutEscSequences(str)
var out strings.Builder
out.Grow(sLen + (sLen / n))
outLen, esp := 0, escSeqParser{}
outLen, esp := 0, EscSeqParser{}
for idx, c := range str {
if esp.InSequence() {
esp.Consume(c)
@@ -52,7 +52,7 @@ func InsertEveryN(str string, runeToInsert rune, n int) string {
//
// LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
func LongestLineLen(str string) int {
maxLength, currLength, esp := 0, 0, escSeqParser{}
maxLength, currLength, esp := 0, 0, EscSeqParser{}
//fmt.Println(str)
for _, c := range str {
//fmt.Printf("%03d | %03d | %c | %5v | %v | %#v\n", idx, c, c, esp.inEscSeq, esp.Codes(), esp.escapeSeq)
@@ -76,17 +76,20 @@ func LongestLineLen(str string) int {
return maxLength
}
// OverrideRuneWidthEastAsianWidth can *probably* help with alignment, and
// length calculation issues when dealing with Unicode character-set and a
// non-English language set in the LANG variable.
// OverrideRuneWidthEastAsianWidth overrides the East Asian width detection in
// the runewidth library. This is primarily for advanced use cases.
//
// Set this to 'false' to force the "runewidth" library to pretend to deal with
// English character-set. Be warned that if the text/content you are dealing
// with contains East Asian character-set, this may result in unexpected
// behavior.
// Box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters
// are automatically handled and always reported as width 1, regardless of
// this setting, fixing alignment issues that previously required setting this
// to false.
//
// References:
// * https://github.com/mattn/go-runewidth/issues/64#issuecomment-1221642154
// Setting this to false forces runewidth to treat all characters as if in an
// English locale. Warning: this may cause East Asian characters (Chinese,
// Japanese, Korean) to be incorrectly reported as width 1 instead of 2.
//
// See:
// * https://github.com/mattn/go-runewidth/issues/64
// * https://github.com/jedib0t/go-pretty/issues/220
// * https://github.com/jedib0t/go-pretty/issues/204
func OverrideRuneWidthEastAsianWidth(val bool) {
@@ -184,16 +187,28 @@ func RuneCount(str string) int {
return StringWidthWithoutEscSequences(str)
}
// RuneWidth returns the mostly accurate character-width of the rune. This is
// not 100% accurate as the character width is usually dependent on the
// typeface (font) used in the console/terminal. For ex.:
// RuneWidth returns the display width of a rune. Width accuracy depends on
// the terminal font, as character width is font-dependent. Examples:
//
// RuneWidth('A') == 1
// RuneWidth('ツ') == 2
// RuneWidth('⊙') == 1
// RuneWidth('︿') == 2
// RuneWidth(0x27) == 0
//
// Box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters
// are always treated as width 1, regardless of locale, to ensure proper
// alignment in tables and progress indicators. This fixes incorrect width 2
// reporting in East Asian locales (e.g., LANG=zh_CN.UTF-8).
//
// See:
// * https://github.com/mattn/go-runewidth/issues/64
// * https://github.com/jedib0t/go-pretty/issues/220
// * https://github.com/jedib0t/go-pretty/issues/204
func RuneWidth(r rune) int {
if (r >= 0x2500 && r <= 0x257F) || (r >= 0x2580 && r <= 0x259F) {
return 1
}
return rwCondition.RuneWidth(r)
}
@@ -248,7 +263,7 @@ func StringWidth(str string) int {
// StringWidthWithoutEscSequences("Ghost 生命"): 10
// StringWidthWithoutEscSequences("\x1b[33mGhost 生命\x1b[0m"): 10
func StringWidthWithoutEscSequences(str string) int {
count, esp := 0, escSeqParser{}
count, esp := 0, EscSeqParser{}
for _, c := range str {
if esp.InSequence() {
esp.Consume(c)
@@ -277,7 +292,7 @@ func Trim(str string, maxLen int) string {
var out strings.Builder
out.Grow(maxLen)
outLen, esp := 0, escSeqParser{}
outLen, esp := 0, EscSeqParser{}
for _, sChr := range str {
if esp.InSequence() {
esp.Consume(sChr)
@@ -306,7 +321,7 @@ func Widen(str string) string {
sb := strings.Builder{}
sb.Grow(len(str))
esp := escSeqParser{}
esp := EscSeqParser{}
for _, c := range str {
if esp.InSequence() {
sb.WriteRune(c)
+89 -101
View File
@@ -11,9 +11,15 @@ import (
// Transformer related constants
const (
unixTimeMinMilliseconds = int64(10000000000)
unixTimeMinMicroseconds = unixTimeMinMilliseconds * 1000
unixTimeMinNanoSeconds = unixTimeMinMicroseconds * 1000
// Pre-computed time conversion constants to avoid repeated calculations
nanosPerSecond = int64(time.Second)
microsPerSecond = nanosPerSecond / 1000
millisPerSecond = nanosPerSecond / 1000000
// Thresholds for detecting unix timestamp units (10 seconds worth in each unit)
unixTimeMinMilliseconds = 10 * nanosPerSecond
unixTimeMinMicroseconds = 10 * nanosPerSecond * 1000
unixTimeMinNanoSeconds = 10 * nanosPerSecond * 1000000
)
// Transformer related variables
@@ -40,103 +46,84 @@ type Transformer func(val interface{}) string
// - transforms the number as directed by 'format' (ex.: %.2f)
// - colors negative values Red
// - colors positive values Green
//
//gocyclo:ignore
func NewNumberTransformer(format string) Transformer {
// Pre-compute negative format string to avoid repeated allocations
negFormat := "-" + format
transformInt64 := func(val int64) string {
if val < 0 {
return colorsNumberNegative.Sprintf(negFormat, -val)
}
if val > 0 {
return colorsNumberPositive.Sprintf(format, val)
}
return colorsNumberZero.Sprintf(format, val)
}
transformUint64 := func(val uint64) string {
if val > 0 {
return colorsNumberPositive.Sprintf(format, val)
}
return colorsNumberZero.Sprintf(format, val)
}
transformFloat64 := func(val float64) string {
if val < 0 {
return colorsNumberNegative.Sprintf(negFormat, -val)
}
if val > 0 {
return colorsNumberPositive.Sprintf(format, val)
}
return colorsNumberZero.Sprintf(format, val)
}
// Use type switch for O(1) type checking instead of sequential type assertions
return func(val interface{}) string {
if valStr := transformInt(format, val); valStr != "" {
return valStr
switch v := val.(type) {
case int:
return transformInt64(int64(v))
case int8:
return transformInt64(int64(v))
case int16:
return transformInt64(int64(v))
case int32:
return transformInt64(int64(v))
case int64:
return transformInt64(v)
case uint:
return transformUint64(uint64(v))
case uint8:
return transformUint64(uint64(v))
case uint16:
return transformUint64(uint64(v))
case uint32:
return transformUint64(uint64(v))
case uint64:
return transformUint64(v)
case float32:
return transformFloat64(float64(v))
case float64:
return transformFloat64(v)
default:
return fmt.Sprint(val)
}
if valStr := transformUint(format, val); valStr != "" {
return valStr
}
if valStr := transformFloat(format, val); valStr != "" {
return valStr
}
return fmt.Sprint(val)
}
}
func transformInt(format string, val interface{}) string {
transform := func(val int64) string {
if val < 0 {
return colorsNumberNegative.Sprintf("-"+format, -val)
}
if val > 0 {
return colorsNumberPositive.Sprintf(format, val)
}
return colorsNumberZero.Sprintf(format, val)
}
if number, ok := val.(int); ok {
return transform(int64(number))
}
if number, ok := val.(int8); ok {
return transform(int64(number))
}
if number, ok := val.(int16); ok {
return transform(int64(number))
}
if number, ok := val.(int32); ok {
return transform(int64(number))
}
if number, ok := val.(int64); ok {
return transform(number)
}
return ""
}
func transformUint(format string, val interface{}) string {
transform := func(val uint64) string {
if val > 0 {
return colorsNumberPositive.Sprintf(format, val)
}
return colorsNumberZero.Sprintf(format, val)
}
if number, ok := val.(uint); ok {
return transform(uint64(number))
}
if number, ok := val.(uint8); ok {
return transform(uint64(number))
}
if number, ok := val.(uint16); ok {
return transform(uint64(number))
}
if number, ok := val.(uint32); ok {
return transform(uint64(number))
}
if number, ok := val.(uint64); ok {
return transform(number)
}
return ""
}
func transformFloat(format string, val interface{}) string {
transform := func(val float64) string {
if val < 0 {
return colorsNumberNegative.Sprintf("-"+format, -val)
}
if val > 0 {
return colorsNumberPositive.Sprintf(format, val)
}
return colorsNumberZero.Sprintf(format, val)
}
if number, ok := val.(float32); ok {
return transform(float64(number))
}
if number, ok := val.(float64); ok {
return transform(number)
}
return ""
}
// NewJSONTransformer returns a Transformer that can format a JSON string or an
// object into pretty-indented JSON-strings.
func NewJSONTransformer(prefix string, indent string) Transformer {
return func(val interface{}) string {
if valStr, ok := val.(string); ok {
valStr = strings.TrimSpace(valStr)
// Validate JSON before attempting to indent to avoid unnecessary processing
if !json.Valid([]byte(valStr)) {
return fmt.Sprintf("%#v", valStr)
}
var b bytes.Buffer
if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil {
if err := json.Indent(&b, []byte(valStr), prefix, indent); err == nil {
return b.String()
}
} else if b, err := json.MarshalIndent(val, prefix, indent); err == nil {
@@ -154,17 +141,17 @@ func NewJSONTransformer(prefix string, indent string) Transformer {
// location (use time.Local to get localized timestamps).
func NewTimeTransformer(layout string, location *time.Location) Transformer {
return func(val interface{}) string {
rsp := fmt.Sprint(val)
// Check for time.Time first to avoid unnecessary fmt.Sprint conversion
if valTime, ok := val.(time.Time); ok {
rsp = formatTime(valTime, layout, location)
} else {
// cycle through some supported layouts to see if the string form
// of the object matches any of these layouts
for _, possibleTimeLayout := range possibleTimeLayouts {
if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil {
rsp = formatTime(valTime, layout, location)
break
}
return formatTime(valTime, layout, location)
}
// Only convert to string if not already time.Time
rsp := fmt.Sprint(val)
// Cycle through some supported layouts to see if the string form
// of the object matches any of these layouts
for _, possibleTimeLayout := range possibleTimeLayouts {
if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil {
return formatTime(valTime, layout, location)
}
}
return rsp
@@ -217,12 +204,13 @@ func formatTime(t time.Time, layout string, location *time.Location) string {
}
func formatTimeUnix(unixTime int64, timeTransformer Transformer) string {
// Use pre-computed constants instead of repeated time.Second.Nanoseconds() calls
if unixTime >= unixTimeMinNanoSeconds {
unixTime = unixTime / time.Second.Nanoseconds()
unixTime = unixTime / nanosPerSecond
} else if unixTime >= unixTimeMinMicroseconds {
unixTime = unixTime / (time.Second.Nanoseconds() / 1000)
unixTime = unixTime / microsPerSecond
} else if unixTime >= unixTimeMinMilliseconds {
unixTime = unixTime / (time.Second.Nanoseconds() / 1000000)
unixTime = unixTime / millisPerSecond
}
return timeTransformer(time.Unix(unixTime, 0))
}
+4 -3
View File
@@ -27,10 +27,11 @@ func (va VAlign) Apply(lines []string, maxLines int) []string {
maxLines = len(lines)
}
insertIdx := 0
if va == VAlignMiddle {
var insertIdx int
switch va {
case VAlignMiddle:
insertIdx = int(maxLines-len(lines)) / 2
} else if va == VAlignBottom {
case VAlignBottom:
insertIdx = maxLines - len(lines)
}
+5 -5
View File
@@ -13,7 +13,7 @@ func WrapHard(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
str = strings.Replace(str, "\t", " ", -1)
str = strings.ReplaceAll(str, "\t", " ")
sLen := StringWidthWithoutEscSequences(str)
if sLen <= wrapLen {
return str
@@ -41,7 +41,7 @@ func WrapSoft(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
str = strings.Replace(str, "\t", " ", -1)
str = strings.ReplaceAll(str, "\t", " ")
sLen := StringWidthWithoutEscSequences(str)
if sLen <= wrapLen {
return str
@@ -68,7 +68,7 @@ func WrapText(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
str = strings.Replace(str, "\t", " ", -1)
str = strings.ReplaceAll(str, "\t", " ")
sLen := StringWidthWithoutEscSequences(str)
if sLen <= wrapLen {
return str
@@ -157,7 +157,7 @@ func terminateOutput(lastSeenEscSeq string, out *strings.Builder) {
}
func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
esp := escSeqParser{}
esp := EscSeqParser{}
lineLen, lastSeenEscSeq := 0, ""
words := strings.Fields(paragraph)
for wordIdx, word := range words {
@@ -186,7 +186,7 @@ func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
}
func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) {
esp := escSeqParser{}
esp := EscSeqParser{}
lineLen, lastSeenEscSeq := 0, ""
words := strings.Fields(paragraph)
for wordIdx, word := range words {
+13 -36
View File
@@ -6,11 +6,12 @@
package flate
import (
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"github.com/klauspost/compress/internal/le"
)
const (
@@ -234,12 +235,9 @@ func (d *compressor) fillWindow(b []byte) {
// Calculate 256 hashes at the time (more L1 cache hits)
loops := (n + 256 - minMatchLength) / 256
for j := 0; j < loops; j++ {
for j := range loops {
startindex := j * 256
end := startindex + 256 + minMatchLength - 1
if end > n {
end = n
}
end := min(startindex+256+minMatchLength-1, n)
tocheck := d.window[startindex:end]
dstSize := len(tocheck) - minMatchLength + 1
@@ -269,18 +267,12 @@ func (d *compressor) fillWindow(b []byte) {
// We only look at chainCount possibilities before giving up.
// pos = s.index, prevHead = s.chainHead-s.hashOffset, prevLength=minMatchLength-1, lookahead
func (d *compressor) findMatch(pos int, prevHead int, lookahead int) (length, offset int, ok bool) {
minMatchLook := maxMatchLength
if lookahead < minMatchLook {
minMatchLook = lookahead
}
minMatchLook := min(lookahead, maxMatchLength)
win := d.window[0 : pos+minMatchLook]
// We quit when we get a match that's at least nice long
nice := len(win) - pos
if d.nice < nice {
nice = d.nice
}
nice := min(d.nice, len(win)-pos)
// If we've got a match that's good enough, only look in 1/4 the chain.
tries := d.chain
@@ -288,10 +280,7 @@ func (d *compressor) findMatch(pos int, prevHead int, lookahead int) (length, of
wEnd := win[pos+length]
wPos := win[pos:]
minIndex := pos - windowSize
if minIndex < 0 {
minIndex = 0
}
minIndex := max(pos-windowSize, 0)
offset = 0
if d.chain < 100 {
@@ -374,7 +363,7 @@ func (d *compressor) writeStoredBlock(buf []byte) error {
// of the supplied slice.
// The caller must ensure that len(b) >= 4.
func hash4(b []byte) uint32 {
return hash4u(binary.LittleEndian.Uint32(b), hashBits)
return hash4u(le.Load32(b, 0), hashBits)
}
// hash4 returns the hash of u to fit in a hash table with h bits.
@@ -389,7 +378,7 @@ func bulkHash4(b []byte, dst []uint32) {
if len(b) < 4 {
return
}
hb := binary.LittleEndian.Uint32(b)
hb := le.Load32(b, 0)
dst[0] = hash4u(hb, hashBits)
end := len(b) - 4 + 1
@@ -480,10 +469,7 @@ func (d *compressor) deflateLazy() {
prevOffset := s.offset
s.length = minMatchLength - 1
s.offset = 0
minIndex := s.index - windowSize
if minIndex < 0 {
minIndex = 0
}
minIndex := max(s.index-windowSize, 0)
if s.chainHead-s.hashOffset >= minIndex && lookahead > prevLength && prevLength < d.lazy {
if newLength, newOffset, ok := d.findMatch(s.index, s.chainHead-s.hashOffset, lookahead); ok {
@@ -503,10 +489,7 @@ func (d *compressor) deflateLazy() {
if prevLength < maxMatchLength-checkOff {
prevIndex := s.index - 1
if prevIndex+prevLength < s.maxInsertIndex {
end := lookahead
if lookahead > maxMatchLength+checkOff {
end = maxMatchLength + checkOff
}
end := min(lookahead, maxMatchLength+checkOff)
end += prevIndex
// Hash at match end.
@@ -603,15 +586,9 @@ func (d *compressor) deflateLazy() {
// table.
newIndex := s.index + prevLength - 1
// Calculate missing hashes
end := newIndex
if end > s.maxInsertIndex {
end = s.maxInsertIndex
}
end := min(newIndex, s.maxInsertIndex)
end += minMatchLength - 1
startindex := s.index + 1
if startindex > s.maxInsertIndex {
startindex = s.maxInsertIndex
}
startindex := min(s.index+1, s.maxInsertIndex)
tocheck := d.window[startindex:end]
dstSize := len(tocheck) - minMatchLength + 1
if dstSize > 0 {
+1 -4
View File
@@ -104,10 +104,7 @@ func (dd *dictDecoder) writeCopy(dist, length int) int {
dstBase := dd.wrPos
dstPos := dstBase
srcPos := dstPos - dist
endPos := dstPos + length
if endPos > len(dd.hist) {
endPos = len(dd.hist)
}
endPos := min(dstPos+length, len(dd.hist))
// Copy non-overlapping section after destination position.
//
+3 -46
View File
@@ -7,7 +7,6 @@ package flate
import (
"fmt"
"math/bits"
"github.com/klauspost/compress/internal/le"
)
@@ -151,29 +150,9 @@ func (e *fastGen) matchlen(s, t int, src []byte) int32 {
panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")"))
}
}
s1 := min(s+maxMatchLength-4, len(src))
left := s1 - s
n := int32(0)
for left >= 8 {
diff := le.Load64(src, s) ^ le.Load64(src, t)
if diff != 0 {
return n + int32(bits.TrailingZeros64(diff)>>3)
}
s += 8
t += 8
n += 8
left -= 8
}
a := src[s:s1]
a := src[s:min(s+maxMatchLength-4, len(src))]
b := src[t:]
for i := range a {
if a[i] != b[i] {
break
}
n++
}
return n
return int32(matchLen(a, b))
}
// matchlenLong will return the match length between offsets and t in src.
@@ -193,29 +172,7 @@ func (e *fastGen) matchlenLong(s, t int, src []byte) int32 {
panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")"))
}
}
// Extend the match to be as long as possible.
left := len(src) - s
n := int32(0)
for left >= 8 {
diff := le.Load64(src, s) ^ le.Load64(src, t)
if diff != 0 {
return n + int32(bits.TrailingZeros64(diff)>>3)
}
s += 8
t += 8
n += 8
left -= 8
}
a := src[s:]
b := src[t:]
for i := range a {
if a[i] != b[i] {
break
}
n++
}
return n
return int32(matchLen(src[s:], src[t:]))
}
// Reset the encoding table.
+16 -26
View File
@@ -211,7 +211,9 @@ func (w *huffmanBitWriter) flush() {
n++
}
w.bits = 0
w.write(w.bytes[:n])
if n > 0 {
w.write(w.bytes[:n])
}
w.nbytes = 0
}
@@ -303,10 +305,7 @@ func (w *huffmanBitWriter) generateCodegen(numLiterals int, numOffsets int, litE
w.codegenFreq[size]++
count--
for count >= 3 {
n := 6
if n > count {
n = count
}
n := min(6, count)
codegen[outIndex] = 16
outIndex++
codegen[outIndex] = uint8(n - 3)
@@ -316,10 +315,7 @@ func (w *huffmanBitWriter) generateCodegen(numLiterals int, numOffsets int, litE
}
} else {
for count >= 11 {
n := 138
if n > count {
n = count
}
n := min(138, count)
codegen[outIndex] = 18
outIndex++
codegen[outIndex] = uint8(n - 11)
@@ -438,8 +434,8 @@ func (w *huffmanBitWriter) writeOutBits() {
w.nbits -= 48
n := w.nbytes
// We over-write, but faster...
le.Store64(w.bytes[n:], bits)
// We overwrite, but faster...
le.Store64(w.bytes[:], n, bits)
n += 6
if n >= bufferFlushSize {
@@ -472,7 +468,7 @@ func (w *huffmanBitWriter) writeDynamicHeader(numLiterals int, numOffsets int, n
w.writeBits(int32(numOffsets-1), 5)
w.writeBits(int32(numCodegens-4), 4)
for i := 0; i < numCodegens; i++ {
for i := range numCodegens {
value := uint(w.codegenEncoding.codes[codegenOrder[i]].len())
w.writeBits(int32(value), 3)
}
@@ -650,7 +646,7 @@ func (w *huffmanBitWriter) writeBlockDynamic(tokens *tokens, eof bool, input []b
w.lastHeader = 0
}
numLiterals, numOffsets := w.indexTokens(tokens, !sync)
numLiterals, numOffsets := w.indexTokens(tokens, fillReuse && !sync)
extraBits := 0
ssize, storable := w.storedSize(input)
@@ -855,8 +851,7 @@ func (w *huffmanBitWriter) writeTokens(tokens []token, leCodes, oeCodes []hcode)
bits |= c.code64() << (nbits & 63)
nbits += c.len()
if nbits >= 48 {
le.Store64(w.bytes[nbytes:], bits)
//*(*uint64)(unsafe.Pointer(&w.bytes[nbytes])) = bits
le.Store64(w.bytes[:], nbytes, bits)
bits >>= 48
nbits -= 48
nbytes += 6
@@ -883,8 +878,7 @@ func (w *huffmanBitWriter) writeTokens(tokens []token, leCodes, oeCodes []hcode)
bits |= c.code64() << (nbits & 63)
nbits += c.len()
if nbits >= 48 {
le.Store64(w.bytes[nbytes:], bits)
//*(*uint64)(unsafe.Pointer(&w.bytes[nbytes])) = bits
le.Store64(w.bytes[:], nbytes, bits)
bits >>= 48
nbits -= 48
nbytes += 6
@@ -906,8 +900,7 @@ func (w *huffmanBitWriter) writeTokens(tokens []token, leCodes, oeCodes []hcode)
bits |= uint64(extraLength) << (nbits & 63)
nbits += extraLengthBits
if nbits >= 48 {
le.Store64(w.bytes[nbytes:], bits)
//*(*uint64)(unsafe.Pointer(&w.bytes[nbytes])) = bits
le.Store64(w.bytes[:], nbytes, bits)
bits >>= 48
nbits -= 48
nbytes += 6
@@ -932,8 +925,7 @@ func (w *huffmanBitWriter) writeTokens(tokens []token, leCodes, oeCodes []hcode)
bits |= c.code64() << (nbits & 63)
nbits += c.len()
if nbits >= 48 {
le.Store64(w.bytes[nbytes:], bits)
//*(*uint64)(unsafe.Pointer(&w.bytes[nbytes])) = bits
le.Store64(w.bytes[:], nbytes, bits)
bits >>= 48
nbits -= 48
nbytes += 6
@@ -954,8 +946,7 @@ func (w *huffmanBitWriter) writeTokens(tokens []token, leCodes, oeCodes []hcode)
bits |= uint64((offset-(offsetComb>>8))&matchOffsetOnlyMask) << (nbits & 63)
nbits += uint8(offsetComb)
if nbits >= 48 {
le.Store64(w.bytes[nbytes:], bits)
//*(*uint64)(unsafe.Pointer(&w.bytes[nbytes])) = bits
le.Store64(w.bytes[:], nbytes, bits)
bits >>= 48
nbits -= 48
nbytes += 6
@@ -1108,7 +1099,7 @@ func (w *huffmanBitWriter) writeBlockHuff(eof bool, input []byte, sync bool) {
// We must have at least 48 bits free.
if nbits >= 8 {
n := nbits >> 3
le.Store64(w.bytes[nbytes:], bits)
le.Store64(w.bytes[:], nbytes, bits)
bits >>= (n * 8) & 63
nbits -= n * 8
nbytes += n
@@ -1137,8 +1128,7 @@ func (w *huffmanBitWriter) writeBlockHuff(eof bool, input []byte, sync bool) {
// Remaining...
for _, t := range input {
if nbits >= 48 {
le.Store64(w.bytes[nbytes:], bits)
//*(*uint64)(unsafe.Pointer(&w.bytes[nbytes])) = bits
le.Store64(w.bytes[:], nbytes, bits)
bits >>= 48
nbits -= 48
nbytes += 6
+1 -1
View File
@@ -91,7 +91,7 @@ func generateFixedLiteralEncoding() *huffmanEncoder {
h := newHuffmanEncoder(literalCount)
codes := h.codes
var ch uint16
for ch = 0; ch < literalCount; ch++ {
for ch = range uint16(literalCount) {
var bits uint16
var size uint8
switch {
+2 -2
View File
@@ -485,7 +485,7 @@ func (f *decompressor) readHuffman() error {
f.nb -= 5 + 5 + 4
// (HCLEN+4)*3 bits: code lengths in the magic codeOrder order.
for i := 0; i < nclen; i++ {
for i := range nclen {
for f.nb < 3 {
if err := f.moreBits(); err != nil {
return err
@@ -776,7 +776,7 @@ func fixedHuffmanDecoderInit() {
fixedOnce.Do(func() {
// These come from the RFC section 3.2.6.
var bits [288]int
for i := 0; i < 144; i++ {
for i := range 144 {
bits[i] = 8
}
for i := 144; i < 256; i++ {
+1 -4
View File
@@ -677,10 +677,7 @@ func (e *fastEncL5Window) matchlen(s, t int32, src []byte) int32 {
panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")"))
}
}
s1 := int(s) + maxMatchLength - 4
if s1 > len(src) {
s1 = len(src)
}
s1 := min(int(s)+maxMatchLength-4, len(src))
// Extend the match to be as long as possible.
return int32(matchLen(src[s:s1], src[t:]))
+2 -2
View File
@@ -56,7 +56,7 @@ func NewStatelessWriter(dst io.Writer) io.WriteCloser {
// bitWriterPool contains bit writers that can be reused.
var bitWriterPool = sync.Pool{
New: func() interface{} {
New: func() any {
return newHuffmanBitWriter(nil)
},
}
@@ -184,7 +184,7 @@ func statelessEnc(dst *tokens, src []byte, startAt int16) {
// Index until startAt
if startAt > 0 {
cv := load3232(src, 0)
for i := int16(0); i < startAt; i++ {
for i := range startAt {
table[hashSL(cv)] = tableEntry{offset: i}
cv = (cv >> 8) | (uint32(src[i+4]) << 24)
}
+1 -1
View File
@@ -143,7 +143,7 @@ func (b *bitWriter) flush32() {
// flushAlign will flush remaining full bytes and align to next byte boundary.
func (b *bitWriter) flushAlign() {
nbBytes := (b.nBits + 7) >> 3
for i := uint8(0); i < nbBytes; i++ {
for i := range nbBytes {
b.out = append(b.out, byte(b.bitContainer>>(i*8)))
}
b.nBits = 0
+1 -1
View File
@@ -396,7 +396,7 @@ func (s *Scratch) buildCTable() error {
if v > largeLimit {
s.zeroBits = true
}
for nbOccurrences := int16(0); nbOccurrences < v; nbOccurrences++ {
for range v {
tableSymbol[position] = symbol
position = (position + step) & tableMask
for position > highThreshold {
+1 -1
View File
@@ -85,7 +85,7 @@ func (b *bitWriter) flush32() {
// flushAlign will flush remaining full bytes and align to next byte boundary.
func (b *bitWriter) flushAlign() {
nbBytes := (b.nBits + 7) >> 3
for i := uint8(0); i < nbBytes; i++ {
for i := range nbBytes {
b.out = append(b.out, byte(b.bitContainer>>(i*8)))
}
b.nBits = 0
+3 -3
View File
@@ -276,7 +276,7 @@ func (s *Scratch) compress4X(src []byte) ([]byte, error) {
offsetIdx := len(s.Out)
s.Out = append(s.Out, sixZeros[:]...)
for i := 0; i < 4; i++ {
for i := range 4 {
toDo := src
if len(toDo) > segmentSize {
toDo = toDo[:segmentSize]
@@ -312,7 +312,7 @@ func (s *Scratch) compress4Xp(src []byte) ([]byte, error) {
segmentSize := (len(src) + 3) / 4
var wg sync.WaitGroup
wg.Add(4)
for i := 0; i < 4; i++ {
for i := range 4 {
toDo := src
if len(toDo) > segmentSize {
toDo = toDo[:segmentSize]
@@ -326,7 +326,7 @@ func (s *Scratch) compress4Xp(src []byte) ([]byte, error) {
}(i)
}
wg.Wait()
for i := 0; i < 4; i++ {
for i := range 4 {
o := s.tmpOut[i]
if len(o) > math.MaxUint16 {
// We cannot store the size in the jump table
+4 -10
View File
@@ -626,7 +626,7 @@ func (d *Decoder) decompress4X8bit(dst, src []byte) ([]byte, error) {
var br [4]bitReaderBytes
start := 6
for i := 0; i < 3; i++ {
for i := range 3 {
length := int(src[i*2]) | (int(src[i*2+1]) << 8)
if start+length >= len(src) {
return nil, errors.New("truncated input (or invalid offset)")
@@ -798,10 +798,7 @@ func (d *Decoder) decompress4X8bit(dst, src []byte) ([]byte, error) {
remainBytes := dstEvery - (decoded / 4)
for i := range br {
offset := dstEvery * i
endsAt := offset + remainBytes
if endsAt > len(out) {
endsAt = len(out)
}
endsAt := min(offset+remainBytes, len(out))
br := &br[i]
bitsLeft := br.remaining()
for bitsLeft > 0 {
@@ -864,7 +861,7 @@ func (d *Decoder) decompress4X8bit(dst, src []byte) ([]byte, error) {
func (d *Decoder) decompress4X8bitExactly(dst, src []byte) ([]byte, error) {
var br [4]bitReaderBytes
start := 6
for i := 0; i < 3; i++ {
for i := range 3 {
length := int(src[i*2]) | (int(src[i*2+1]) << 8)
if start+length >= len(src) {
return nil, errors.New("truncated input (or invalid offset)")
@@ -1035,10 +1032,7 @@ func (d *Decoder) decompress4X8bitExactly(dst, src []byte) ([]byte, error) {
remainBytes := dstEvery - (decoded / 4)
for i := range br {
offset := dstEvery * i
endsAt := offset + remainBytes
if endsAt > len(out) {
endsAt = len(out)
}
endsAt := min(offset+remainBytes, len(out))
br := &br[i]
bitsLeft := br.remaining()
for bitsLeft > 0 {
+2 -5
View File
@@ -58,7 +58,7 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
var br [4]bitReaderShifted
// Decode "jump table"
start := 6
for i := 0; i < 3; i++ {
for i := range 3 {
length := int(src[i*2]) | (int(src[i*2+1]) << 8)
if start+length >= len(src) {
return nil, errors.New("truncated input (or invalid offset)")
@@ -109,10 +109,7 @@ func (d *Decoder) Decompress4X(dst, src []byte) ([]byte, error) {
remainBytes := dstEvery - (decoded / 4)
for i := range br {
offset := dstEvery * i
endsAt := offset + remainBytes
if endsAt > len(out) {
endsAt = len(out)
}
endsAt := min(offset+remainBytes, len(out))
br := &br[i]
bitsLeft := br.remaining()
for bitsLeft > 0 {
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more