Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ module github.com/GoScouter/sdk
go 1.26.4

require github.com/google/uuid v1.6.0

require (
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
158 changes: 158 additions & 0 deletions style/render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package style

import (
"bytes"
"encoding/json"
"fmt"
"strings"
)

const indentWidth = 4

// Render turns an arbitrary JSON report into an indented, styled block of
// terminal text. It is the fallback view for modules that have no opinion on
// how their results should look: keys become labels, nested objects become
// headings, and array elements become bullets.
func Render(raw json.RawMessage) string {
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()

var b strings.Builder
if err := value(&b, dec, "", 0); err != nil {
return Failuref("scan: unreadable report: %v", err) + "\r\n"
}

return b.String()
}

func value(b *strings.Builder, dec *json.Decoder, label string, depth int) error {
tok, err := dec.Token()
if err != nil {
return err
}

if d, ok := tok.(json.Delim); ok {
switch d {
case '{':
return object(b, dec, label, depth)
case '[':
return array(b, dec, label, depth)
default:
return fmt.Errorf("unexpected %q", d)
}
}

field(b, depth, label, scalar(tok))
return nil
}

func object(b *strings.Builder, dec *json.Decoder, label string, depth int) error {
inner := depth
if label != "" {
heading(b, depth, label)
inner++
}

for dec.More() {
key, err := dec.Token()
if err != nil {
return err
}

name, ok := key.(string)
if !ok {
return fmt.Errorf("object key is %T, not a string", key)
}

if err := value(b, dec, name, inner); err != nil {
return err
}
}

_, err := dec.Token() // closing brace
return err
}

func array(b *strings.Builder, dec *json.Decoder, label string, depth int) error {
titled := false
i := 0

for dec.More() {
tok, err := dec.Token()
if err != nil {
return err
}

d, nested := tok.(json.Delim)
if !nested {
if !titled {
heading(b, depth, label)
titled = true
}

bullet(b, depth+1, scalar(tok))
i++
continue
}

element := fmt.Sprintf("%s[%d]", label, i)
b.WriteString("\r\n")

switch d {
case '{':
err = object(b, dec, element, depth)
case '[':
err = array(b, dec, element, depth)
default:
err = fmt.Errorf("unexpected %q", d)
}
if err != nil {
return err
}

i++
}

if i == 0 {
heading(b, depth, label)
}

_, err := dec.Token() // closing bracket
return err
}

func indent(depth int) string {
return strings.Repeat(" ", depth*indentWidth)
}

func heading(b *strings.Builder, depth int, label string) {
b.WriteString(indent(depth) + Found(Bold(Cyan(label))) + "\r\n")
}

func field(b *strings.Builder, depth int, label, val string) {
line := Gray(label + ":")
if val != "" {
line += " " + White(val)
}

b.WriteString(indent(depth) + Found(line) + "\r\n")
}

func bullet(b *strings.Builder, depth int, val string) {
b.WriteString(indent(depth) + Gray("- ") + White(val) + "\r\n")
}

func scalar(tok json.Token) string {
switch v := tok.(type) {
case nil:
return "null"
case string:
return v
case json.Number:
return v.String()
case bool:
return fmt.Sprintf("%t", v)
default:
return fmt.Sprintf("%v", v)
}
}
121 changes: 121 additions & 0 deletions style/style.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package style

import (
"fmt"
"regexp"
"strings"
)

const (
Reset = "\033[0m"

CodeBold = "\033[1m"
CodeDim = "\033[2m"

CodeRed = "\033[38;2;235;77;75m"
CodeGreen = "\033[38;2;111;207;151m"
CodeYellow = "\033[38;2;249;202;54m"
CodeCyan = "\033[38;2;56;193;208m"
CodeGray = "\033[38;2;130;130;150m"
CodePurple = "\033[38;2;87;87;232m"
CodeWhite = "\033[38;2;255;255;255m"
)

func wrap(code, s string) string {
return code + s + Reset
}

func Bold(s string) string { return wrap(CodeBold, s) }

func BoldAll(s string) string {
return CodeBold + strings.ReplaceAll(s, Reset, Reset+CodeBold) + Reset
}

func Dim(s string) string { return wrap(CodeDim, s) }
func Red(s string) string { return wrap(CodeRed, s) }
func Green(s string) string { return wrap(CodeGreen, s) }
func Yellow(s string) string { return wrap(CodeYellow, s) }
func Cyan(s string) string { return wrap(CodeCyan, s) }
func Gray(s string) string { return wrap(CodeGray, s) }
func Purple(s string) string { return wrap(CodePurple, s) }
func White(s string) string { return wrap(CodeWhite, s) }

func Prompt() string {
return Dim("(") + Bold(Purple("gs")) + Dim(")") + " " + Cyan("❯") + " "
}

func rawLines(msg string) string {
msg = strings.ReplaceAll(msg, "\r\n", "\n")
return strings.ReplaceAll(msg, "\n", "\r\n")
}

func Error(msg string) string {
return Red("✗ ") + rawLines(msg)
}

func Errorf(format string, a ...any) string {
return Error(fmt.Sprintf(format, a...))
}

func Success(msg string) string {
return Green("✓ ") + msg
}

func Successf(format string, a ...any) string {
return Success(fmt.Sprintf(format, a...))
}

func Info(msg string) string {
return Cyan("» ") + msg
}

func Infof(format string, a ...any) string {
return Info(fmt.Sprintf(format, a...))
}

var ansiRE = regexp.MustCompile("\x1b\\[[0-9;]*m")

func Width(s string) int {
return len([]rune(ansiRE.ReplaceAllString(s, "")))
}

func Found(msg string) string {
return Green("[+] ") + msg
}

func Foundf(format string, a ...any) string {
return Found(fmt.Sprintf(format, a...))
}

func Failure(msg string) string {
return Red("[-] ") + msg
}

func Failuref(format string, a ...any) string {
return Failure(fmt.Sprintf(format, a...))
}

func Alert(msg string) string {
return Yellow("[!] ") + rawLines(msg)
}

func Alertf(format string, a ...any) string {
return Alert(fmt.Sprintf(format, a...))
}

func Section(title string, body ...string) string {
var b strings.Builder

b.WriteString("\r\n")
b.WriteString(Bold(Cyan("["+title+"]")) + "\r\n")
for _, line := range body {
b.WriteString(line + "\r\n")
}
b.WriteString("\r\n")

return b.String()
}

func Field(label string, width int, value string) string {
return " " + Gray(fmt.Sprintf("%-*s", width, label)) + " " + value
}
Loading
Loading