diff --git a/README.md b/README.md index 0b04b68..fefcff0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,38 @@ # Hobocode -Simply a very small wrapper for [go-pretty](https://github.com/jedib0t/go-pretty) for different opinionated colored print calls like Info() and Error() +Hobocode is a pty-aware colorized printing library intended for use in CLI +applications for human consumption and UX rather than structured outputs. + +Uses [go-pretty v6](https://github.com/jedib0t/go-pretty) for colors and Sprinting. + +```go + hobocode.Header("Hobocode Example") + hobocode.Success("Welcome to hobocode") +``` + +![hobocode_example](static/hobocode1.png) + + +The [underlying functions](coloring.go) can be used if you are working with custom *os.File's or you can use the lazy opinionated helpers like + +### Plain +`hobocode.Warn("Something might be going wrong")` + +### Formatted +`hobocode.Debugf("Output: %v", ret.Body())` + +### Indented +`hobocode.Ierror(2, "Something went terribly wrong")` + +### Indented and Formatted +`hobocode.Iinfof(1, "Date: %s", time.Now().Format("2006/01/02"))` + -This is meant to be a very simple utility for CLI applications relying on UX rather than structured log outputs. # Example +See [examples](examples/) + ``` import ( "github.com/asciifaceman/hobocode" diff --git a/coloring.go b/coloring.go new file mode 100644 index 0000000..78e6e7f --- /dev/null +++ b/coloring.go @@ -0,0 +1,34 @@ +package hobocode + +import ( + "fmt" + "os" + + "github.com/jedib0t/go-pretty/v6/text" +) + +// Icolor prints the given string at the given indent +// to the given os.File with the given text.Color, if colorable +func Icolor(indent int, fd *os.File, color text.Color, message string) { + id := Tindent(indent) + if Colorable(fd) { + fmt.Fprint(fd, id) + Stdlin(fd, color.Sprint(message)) + } else { + fmt.Fprint(fd, id) + Stdlin(fd, message) + } +} + +// Icolorf prints the given format at the given indent +// to the given os.File with the given text.Color, if colorable +func Icolorf(indent int, fd *os.File, color text.Color, format string, a ...interface{}) { + id := Tindent(indent) + if Colorable(fd) { + fmt.Fprint(fd, id) + Stdlin(fd, color.Sprintf(format, a...)) + } else { + fmt.Fprint(fd, id) + Stdlin(fd, fmt.Sprintf(format, a...)) + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..05cac36 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +# Hobocode Examples + +``` +go run prints.go +``` + diff --git a/examples/prints.go b/examples/prints.go new file mode 100644 index 0000000..32df428 --- /dev/null +++ b/examples/prints.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "time" + + "github.com/asciifaceman/hobocode" +) + +func main() { + hobocode.Header("Info") + hobocode.Info("This is an info message") + hobocode.Infof("This is a formatted info message: %v", time.Now().Format("2006/01/02")) + hobocode.Iinfo(1, "This is an info message indented once") + hobocode.Iinfo(2, "This is an info message indented twice") + hobocode.Iinfof(1, "This is a formatted info message indented once: %v", time.Now().Format("2006/01/02")) + hobocode.Iinfof(2, "This is a formatted info message indented twice: %v", time.Now().Format("2006/01/02")) + + hobocode.Header("Warn") + hobocode.Warn("This is a warn message") + hobocode.Warnf("This is a formatted warn message: %v", time.Now().Format("2006/01/02")) + + hobocode.Header("Error") + hobocode.Error("This is an error message") + hobocode.Errorf("This is a formatted error message: %v", fmt.Errorf("error occured at %v", time.Now().Format("2006/01/02"))) + + hobocode.Header("Success") + hobocode.Success("This is a success message!") + hobocode.Successf("This is a formatted successs message! %v", time.Now().Format("2006/01/02")) + + hobocode.Header("Debug") + hobocode.Debug("This is a debug message!") + hobocode.Debugf("This is a formatted debug message! %v", time.Now().Format("2006/01/02")) + + hobocode.HeaderLeft("A left justified header!?") + hobocode.HeaderLeft("Whoa!") + hobocode.HeaderLeft("That's cool") + +} diff --git a/go.mod b/go.mod index b5c634e..70e4068 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,13 @@ module github.com/asciifaceman/hobocode go 1.20 -require github.com/jedib0t/go-pretty/v6 v6.4.6 +require ( + github.com/jedib0t/go-pretty/v6 v6.4.6 + golang.org/x/term v0.11.0 +) require ( github.com/mattn/go-runewidth v0.0.13 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sys v0.1.0 // indirect + golang.org/x/sys v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 64fd3f7..bea8fe4 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,11 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..0a145c3 --- /dev/null +++ b/helpers.go @@ -0,0 +1,160 @@ +package hobocode + +import ( + "os" + + "github.com/jedib0t/go-pretty/v6/text" +) + +var ( + out = os.Stdout + err = os.Stderr +) + +/* + Stdout helpers +*/ + +// Info prints the given message with bright cyan if Colorable +// +// Stdout +func Info(message string) { + Icolor(0, out, text.FgHiCyan, message) +} + +// Infof prints the given formatted message with bright cyan if colorable +// +// Stdout +func Infof(format string, a ...interface{}) { + Icolorf(0, out, text.FgHiCyan, format, a...) +} + +// Iinfo prints the given message with bright cyan if Colorable and an indent +// +// Stdout +func Iinfo(indent int, message string) { + Icolor(indent, out, text.FgHiCyan, message) +} + +// Iinfof prints the given formatted message with bright cyan if Colorable and an indent +// +// Stdout +func Iinfof(indent int, format string, a ...interface{}) { + Icolorf(indent, out, text.FgHiCyan, format, a...) +} + +// Success prints the given message with bright green if Colorable +// +// Stdout +func Success(message string) { + Icolor(0, out, text.FgHiGreen, message) +} + +// Successf prints the given formatted message with bright green if Colorable +// +// Stdout +func Successf(format string, a ...interface{}) { + Icolorf(0, out, text.FgHiGreen, format, a...) +} + +// Isuccess prints the given message with bright green if Colorable with an indent +// +// Stdout +func Isuccess(indent int, message string) { + Icolor(indent, out, text.FgHiGreen, message) +} + +// Isuccessf prints the given formatted message with bright green if Colorable with an indent +// +// Stdout +func Isuccessf(indent int, format string, a ...interface{}) { + Icolorf(indent, out, text.FgHiGreen, format, a...) +} + +// Debug prints the given message with bright green if Colorable +// +// Stdout +func Debug(message string) { + Icolor(0, out, text.FgHiBlue, message) +} + +// Debugf prints the given formatted message with bright green if Colorable +// +// Stdout +func Debugf(format string, a ...interface{}) { + Icolorf(0, out, text.FgHiBlue, format, a...) +} + +// Idebug prints the given message with bright green if Colorable with an indent +// +// Stdout +func Idebug(indent int, message string) { + Icolor(indent, out, text.FgHiBlue, message) +} + +// Idebugf prints the given formatted message with bright green if Colorable with an indent +// +// Stdout +func Idebugf(indent int, format string, a ...interface{}) { + Icolorf(indent, out, text.FgHiBlue, format, a...) +} + +/* + Stderr helpers +*/ + +// Warn prints the given message with bright yellow if Colorable +// +// Stderr +func Warn(message string) { + Icolor(0, err, text.FgHiYellow, message) +} + +// Warnf prints the given formatted message with bright yellow if Colorable +// +// Stderr +func Warnf(format string, a ...interface{}) { + Icolorf(0, err, text.FgHiYellow, format, a...) +} + +// Iwarn prints the given message with bright yellow if Colorable and an indent +// +// Stderr +func Iwarn(indent int, message string) { + Icolor(indent, err, text.FgHiYellow, message) +} + +// Iwarnf prints the given formatted message with bright yellow if Colorable and an indent +// +// Stderr +func Iwarnf(indent int, format string, a ...interface{}) { + Icolorf(indent, err, text.FgHiYellow, format, a...) +} + +// Error prints the given message with bright red if Colorable +// +// Stderr +func Error(message string) { + Icolor(0, err, text.FgHiRed, message) +} + +// Errorf prints the given formatted message with bright red if Colorable +// +// Stderr +func Errorf(format string, a ...interface{}) { + Icolorf(0, err, text.FgHiRed, format, a...) +} + +// Ierror prints the given message with bright red if Colorable and an indent +// +// Stderr +func Ierror(indent int, message string) { + Icolor(indent, err, text.FgHiRed, message) +} + +// Ierrorf prints the given formatted message with bright red if Colorable and an indent +// +// Stderr +func Ierrorf(indent int, format string, a ...interface{}) { + Icolorf(indent, err, text.FgHiRed, format, a...) +} diff --git a/hobocode.go b/hobocode.go index 8b70b65..d8139f5 100644 --- a/hobocode.go +++ b/hobocode.go @@ -1,100 +1,32 @@ +/* +package hobocode provides some lazy convenience functions +for wrapping text in colors, detecting tty, terminal +size, and colored/formatted interactive inputs + +The coloring choices are opinionated and the "levels" are based +on common logging patterns but with a focus on Console UX vs. logging style + +Note: Most of the printers in this library automatically +new-line fields with Println variants +*/ package hobocode import ( "fmt" - "strings" - - "github.com/jedib0t/go-pretty/v6/text" + "os" ) -// Note prints the given message in HiCyan -func Note(message string) { - fmt.Println(text.FgHiCyan.Sprint(message)) -} - -// Notef prints the given formatted message in HiCyan -func Notef(format string, a ...interface{}) { - fmt.Println(text.FgHiCyan.Sprintf(format, a...)) -} - -// Warn prints the given message in HiMagenta -func Warn(message string) { - fmt.Println(text.FgHiMagenta.Sprint(message)) -} - -// Warnf prints the given formatted message in HiMagenta -func Warnf(format string, a ...interface{}) { - fmt.Println(text.FgHiMagenta.Sprintf(format, a...)) -} - -// Input acquires user CLI input -// defaultOption is presented as the default and used on nil entry -func Input(defaultOption string, message string) string { - fmt.Print(text.FgHiYellow.Sprintf("%s [%s]: ", message, defaultOption)) - fmt.Scanln(&defaultOption) - return defaultOption -} - -// Inputf acquires user CLI input using a formatted message -// defaultOption is presented as the default and used on nil entry -func Inputf(defaultOption string, format string, a ...interface{}) string { - fmt.Print(text.FgHiYellow.Sprintf(format, a...)) - fmt.Scanln(&defaultOption) - return defaultOption -} - -// Error prints the given message in HiRed -func Error(message string) { - fmt.Println(text.FgHiRed.Sprint(message)) -} - -// Errorf prints the given formatted message in HiRed -func Errorf(format string, a ...interface{}) { - fmt.Println(text.FgHiRed.Sprintf(format, a...)) -} - -// Success prints the given message in HiGreen -func Success(message string) { - fmt.Println(text.FgHiGreen.Sprint(message)) -} - -// Successf prints the given formatted message in HiGreen -func Successf(format string, a ...interface{}) { - fmt.Println(text.FgHiGreen.Sprintf(format, a...)) +// Stdoutln prints with newline to os.Stdout +func Stdoutln(message string) { + fmt.Fprintln(os.Stdout, message) } -// Debug prints the given message in HiBlue -func Debug(message string) { - fmt.Println(text.FgHiBlue.Sprint(message)) -} - -// Debugf prints the given formatted message in HiBlue -func Debugf(message string, a ...interface{}) { - fmt.Println(text.FgHiBlue.Sprintf(message, a...)) -} - -// Confirm is a naive confirmation prompt in HiRed that won't break until an affirmative answer is given -func Confirm(message string) bool { - var ret string - - for { - fmt.Print(text.FgHiRed.Sprintf("%s [(y)es / (n)o]: ", message)) - fmt.Scanln(&ret) - - if strings.ToLower(ret) == "y" || strings.ToLower(ret) == "yes" { - return true - } else if strings.ToLower(ret) == "n" || strings.ToLower(ret) == "no" { - return false - } else { - continue - } - } - +// Stderrln prints with newline to os.Stderr +func Stderrln(message string) { + fmt.Fprintln(os.Stderr, message) } -// Confirmf is a naive confirmation prompt in HiRed that won't break until an affirmative answer is given -// (with a formatted message) -func Confirmf(format string, a ...interface{}) bool { - msg := fmt.Sprintf(format, a...) - return Confirm(msg) +// Stdlin prints with newline to *os.File the given message +func Stdlin(fd *os.File, message string) { + fmt.Fprintln(fd, message) } diff --git a/static/hobocode1.png b/static/hobocode1.png new file mode 100755 index 0000000..bd64d7d Binary files /dev/null and b/static/hobocode1.png differ diff --git a/structural.go b/structural.go new file mode 100644 index 0000000..94d31bf --- /dev/null +++ b/structural.go @@ -0,0 +1,105 @@ +package hobocode + +import ( + "fmt" + "os" + "strings" + "unicode/utf8" + + "github.com/jedib0t/go-pretty/v6/text" +) + +/* + TODO: Header right justification gets wonky when the terminal width is odd +*/ + +const ( + defaultWidth = 0 +) + +// Header prints a header title wrapped by "=" characters sized to the terminal width +// +// Stdout +func Header(title string) { + + /* + TODO: I've wasted a lot of time here trying to get the header widths to reliably always + be the same across headers based on screen width but I am too stupid to get it to work in every + circumstance + */ + + x, _, err := Size(os.Stdout) + if err != nil { + x = defaultWidth + } + // safety buffer + x = x - 1 + //fmt.Printf("width: %d\n", x) + + titleLength := utf8.RuneCountInString(title) + //fmt.Printf("title len: %d\n", titleLength) + + n := (x - titleLength - 4) / 2 + n2 := x - n*2 + //fmt.Printf("Remainder: %d\n", n2) + //fmt.Printf("Difference: %d\n", titleLength*2-n2) + + //fmt.Printf("half: %d\n", n) + offset := 0 + + if titleLength*2 > n2 { + offset-- + } + + if n%2 == 0 { + if titleLength%2 != 0 { + offset++ + } + } + + //fmt.Printf("Offset: %d\n", offset) + + patternLeft := strings.Repeat("=", n) + patternRight := strings.Repeat("=", n+offset) + + Icolor(0, os.Stdout, text.FgHiWhite, fmt.Sprintf("%s[ %s ]%s", patternLeft, title, patternRight)) + +} + +// HeaderLeft prints a header title offset to the left wrapped by "=" characters sized to the terminal width +// +// Stdout +func HeaderLeft(title string) { + + x, _, err := Size(os.Stdout) + if err != nil { + x = defaultWidth + } + + x = x - 1 + //fmt.Println(x) + + titleLength := utf8.RuneCountInString(title) + //fmt.Printf("title len: %d\n", titleLength) + + n := (x - titleLength) / 10 + n2 := x - titleLength - n - 1 + + offset := 0 + if titleLength%2 != 0 { + offset++ + } + + patternLeft := "" + if n-4 > 0 { + patternLeft = strings.Repeat("=", n-4) + } + + patternRight := "" + if n-2+offset > 0 { + patternRight = strings.Repeat("=", n2-4+offset) + } + + Icolor(0, os.Stdout, text.FgHiWhite, fmt.Sprintf("%s[ %s ]%s", patternLeft, title, patternRight)) + +} diff --git a/userio.go b/userio.go new file mode 100644 index 0000000..0e525b8 --- /dev/null +++ b/userio.go @@ -0,0 +1,54 @@ +package hobocode + +import ( + "fmt" + "strings" + + "github.com/jedib0t/go-pretty/v6/text" +) + +/* + TODO: User IO stuff is rough and needs attention +*/ + +// Input acquires user CLI input +// defaultOption is presented as the default and used on nil entry +func Input(defaultOption string, message string) string { + fmt.Print(text.FgHiYellow.Sprintf("%s [%s]: ", message, defaultOption)) + fmt.Scanln(&defaultOption) + return defaultOption +} + +// Inputf acquires user CLI input using a formatted message +// defaultOption is presented as the default and used on nil entry +func Inputf(defaultOption string, format string, a ...interface{}) string { + fmt.Print(text.FgHiYellow.Sprintf(format, a...)) + fmt.Scanln(&defaultOption) + return defaultOption +} + +// Confirm is a naive confirmation prompt in HiRed that won't break until an affirmative answer is given +func Confirm(message string) bool { + var ret string + + for { + fmt.Print(text.FgHiRed.Sprintf("%s [(y)es / (n)o]: ", message)) + fmt.Scanln(&ret) + + if strings.ToLower(ret) == "y" || strings.ToLower(ret) == "yes" { + return true + } else if strings.ToLower(ret) == "n" || strings.ToLower(ret) == "no" { + return false + } else { + continue + } + } + +} + +// Confirmf is a naive confirmation prompt in HiRed that won't break until an affirmative answer is given +// (with a formatted message) +func Confirmf(format string, a ...interface{}) bool { + msg := fmt.Sprintf(format, a...) + return Confirm(msg) +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..13f16f0 --- /dev/null +++ b/util.go @@ -0,0 +1,29 @@ +package hobocode + +import ( + "os" + "strings" + + "golang.org/x/term" +) + +/* + These aren't really necessary to wrap + but I done did it anyways +*/ + +// Colorable reports if the given *os.File is +// a terminal or not (os.Stdout, os.Stderr) +func Colorable(pipe *os.File) bool { + return term.IsTerminal(int(pipe.Fd())) +} + +// Size returns the terminal size or error +func Size(pipe *os.File) (int, int, error) { + return term.GetSize(int(pipe.Fd())) +} + +// Tindent returns a tabbed indent repeated n times +func Tindent(n int) string { + return strings.Repeat("\t", n) +}