first commit
This commit is contained in:
5
output/formatter/ansi.go
Normal file
5
output/formatter/ansi.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package formatter
|
||||
|
||||
func Ansi() *Formatter {
|
||||
return New()
|
||||
}
|
||||
79
output/formatter/formatter.go
Normal file
79
output/formatter/formatter.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output/style"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var re = regexp.MustCompile(`<(([a-z][^<>]+)|/([a-z][^<>]+)?)>`)
|
||||
|
||||
func WithStyle(styles func(string) (style.Style, error)) func(*Formatter) {
|
||||
return func(f *Formatter) {
|
||||
f.styles = styles
|
||||
}
|
||||
}
|
||||
|
||||
func New(opts ...func(*Formatter)) *Formatter {
|
||||
f := &Formatter{
|
||||
styles: style.Find,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
type Formatter struct {
|
||||
styles func(string) (style.Style, error)
|
||||
}
|
||||
|
||||
func (a *Formatter) Format(ctx context.Context, msg string) string {
|
||||
var (
|
||||
out bytes.Buffer
|
||||
cur int
|
||||
)
|
||||
|
||||
for _, idx := range re.FindAllStringIndex(msg, -1) {
|
||||
tag := msg[idx[0]+1 : idx[1]-1]
|
||||
|
||||
if cur < idx[0] {
|
||||
out.WriteString(msg[cur:idx[0]])
|
||||
}
|
||||
|
||||
var (
|
||||
st style.Style
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case tag[0:1] == "/":
|
||||
st, err = a.styles(tag[1:])
|
||||
if err == nil {
|
||||
out.WriteString(st.Set(style.ActionUnset))
|
||||
}
|
||||
default:
|
||||
st, err = a.styles(tag)
|
||||
if err == nil {
|
||||
out.WriteString(st.Set(style.ActionSet))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cur = idx[0]
|
||||
} else {
|
||||
cur = idx[1]
|
||||
}
|
||||
}
|
||||
|
||||
if len(msg) > cur {
|
||||
out.WriteString(msg[cur:])
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
26
output/formatter/formatter_test.go
Normal file
26
output/formatter/formatter_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package formatter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output/formatter"
|
||||
)
|
||||
|
||||
func TestFormatter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
formatter := formatter.New()
|
||||
|
||||
cases := map[string]string{
|
||||
"<info>info message</info>": "\x1b[32minfo message\x1b[39m",
|
||||
"<info><command></info>": "\x1b[32m<command>\x1b[39m",
|
||||
"<html>...</html>": "<html>...</html>",
|
||||
}
|
||||
|
||||
for msg, ex := range cases {
|
||||
got := formatter.Format(ctx, msg)
|
||||
if ex != got {
|
||||
t.Errorf("ivalid expected:%#v, got: %#v", ex, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
output/formatter/none.go
Normal file
14
output/formatter/none.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package formatter
|
||||
|
||||
import "gitoa.ru/go-4devs/console/output/style"
|
||||
|
||||
func None() *Formatter {
|
||||
return New(
|
||||
WithStyle(func(name string) (style.Style, error) {
|
||||
if _, err := style.Find(name); err != nil {
|
||||
return style.Empty(), err
|
||||
}
|
||||
|
||||
return style.Empty(), nil
|
||||
}))
|
||||
}
|
||||
27
output/formatter/none_test.go
Normal file
27
output/formatter/none_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package formatter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output/formatter"
|
||||
)
|
||||
|
||||
func TestNone(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
none := formatter.None()
|
||||
|
||||
cases := map[string]string{
|
||||
"<info>message info</info>": "message info",
|
||||
"<error>message error</error>": "message error",
|
||||
"<comment><scheme></comment>": "<scheme>",
|
||||
"<body>body</body>": "<body>body</body>",
|
||||
}
|
||||
|
||||
for msg, ex := range cases {
|
||||
got := none.Format(ctx, msg)
|
||||
if ex != got {
|
||||
t.Errorf("expect:%#v, got:%#v", ex, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
59
output/key.go
Normal file
59
output/key.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package output
|
||||
|
||||
type Key string
|
||||
|
||||
func (k Key) Any(v interface{}) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: AnyValue(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) Bool(v bool) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: BoolValue(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) Int(v int) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: IntValue(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) Int64(v int64) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: Int64Value(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) Uint(v uint) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: UintValue(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) Uint64(v uint64) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: Uint64Value(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) Float64(v float64) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: Float64Value(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (k Key) String(v string) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: StringValue(v),
|
||||
}
|
||||
}
|
||||
63
output/kv.go
Normal file
63
output/kv.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
_ fmt.Stringer = KeyValue{}
|
||||
_ fmt.Stringer = KeyValues{}
|
||||
)
|
||||
|
||||
type KeyValues []KeyValue
|
||||
|
||||
func (kv KeyValues) String() string {
|
||||
s := make([]string, len(kv))
|
||||
for i, v := range kv {
|
||||
s[i] = v.String()
|
||||
}
|
||||
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
|
||||
type KeyValue struct {
|
||||
Key Key
|
||||
Value Value
|
||||
}
|
||||
|
||||
func (k KeyValue) String() string {
|
||||
return string(k.Key) + "=\"" + k.Value.String() + "\""
|
||||
}
|
||||
|
||||
func Any(k string, v interface{}) KeyValue {
|
||||
return Key(k).Any(v)
|
||||
}
|
||||
|
||||
func Bool(k string, v bool) KeyValue {
|
||||
return Key(k).Bool(v)
|
||||
}
|
||||
|
||||
func Int(k string, v int) KeyValue {
|
||||
return Key(k).Int(v)
|
||||
}
|
||||
|
||||
func Int64(k string, v int64) KeyValue {
|
||||
return Key(k).Int64(v)
|
||||
}
|
||||
|
||||
func Uint(k string, v uint) KeyValue {
|
||||
return Key(k).Uint(v)
|
||||
}
|
||||
|
||||
func Uint64(k string, v uint64) KeyValue {
|
||||
return Key(k).Uint64(v)
|
||||
}
|
||||
|
||||
func Float64(k string, v float64) KeyValue {
|
||||
return Key(k).Float64(v)
|
||||
}
|
||||
|
||||
func String(k string, v string) KeyValue {
|
||||
return Key(k).String(v)
|
||||
}
|
||||
77
output/output.go
Normal file
77
output/output.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Verbosity int
|
||||
|
||||
const (
|
||||
VerbosityQuiet Verbosity = iota - 1
|
||||
VerbosityNorm
|
||||
VerbosityInfo
|
||||
VerbosityDebug
|
||||
VerbosityTrace
|
||||
)
|
||||
|
||||
type Output func(ctx context.Context, verb Verbosity, msg string, args ...KeyValue) (int, error)
|
||||
|
||||
func (o Output) Print(ctx context.Context, args ...interface{}) {
|
||||
o(ctx, VerbosityNorm, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (o Output) PrintKV(ctx context.Context, msg string, kv ...KeyValue) {
|
||||
o(ctx, VerbosityNorm, msg, kv...)
|
||||
}
|
||||
|
||||
func (o Output) Printf(ctx context.Context, format string, args ...interface{}) {
|
||||
o(ctx, VerbosityNorm, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (o Output) Println(ctx context.Context, args ...interface{}) {
|
||||
o(ctx, VerbosityNorm, fmt.Sprintln(args...))
|
||||
}
|
||||
|
||||
func (o Output) Info(ctx context.Context, args ...interface{}) {
|
||||
o(ctx, VerbosityInfo, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (o Output) InfoKV(ctx context.Context, msg string, kv ...KeyValue) {
|
||||
o(ctx, VerbosityInfo, msg, kv...)
|
||||
}
|
||||
|
||||
func (o Output) Debug(ctx context.Context, args ...interface{}) {
|
||||
o(ctx, VerbosityDebug, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (o Output) DebugKV(ctx context.Context, msg string, kv ...KeyValue) {
|
||||
o(ctx, VerbosityDebug, msg, kv...)
|
||||
}
|
||||
|
||||
func (o Output) Trace(ctx context.Context, args ...interface{}) {
|
||||
o(ctx, VerbosityTrace, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (o Output) TraceKV(ctx context.Context, msg string, kv ...KeyValue) {
|
||||
o(ctx, VerbosityTrace, msg, kv...)
|
||||
}
|
||||
|
||||
func (o Output) Write(b []byte) (int, error) {
|
||||
return o(context.Background(), VerbosityNorm, string(b))
|
||||
}
|
||||
|
||||
func (o Output) Writer(ctx context.Context, verb Verbosity) io.Writer {
|
||||
return verbosityWriter{ctx, o, verb}
|
||||
}
|
||||
|
||||
type verbosityWriter struct {
|
||||
ctx context.Context
|
||||
out Output
|
||||
verb Verbosity
|
||||
}
|
||||
|
||||
func (w verbosityWriter) Write(b []byte) (int, error) {
|
||||
return w.out(w.ctx, w.verb, string(b))
|
||||
}
|
||||
51
output/style/color.go
Normal file
51
output/style/color.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package style
|
||||
|
||||
const (
|
||||
Black Color = "0"
|
||||
Red Color = "1"
|
||||
Green Color = "2"
|
||||
Yellow Color = "3"
|
||||
Blue Color = "4"
|
||||
Magenta Color = "5"
|
||||
Cyan Color = "6"
|
||||
White Color = "7"
|
||||
Default Color = "9"
|
||||
)
|
||||
|
||||
const (
|
||||
Bold Option = "122"
|
||||
Underscore Option = "424"
|
||||
Blink Option = "525"
|
||||
Reverse Option = "727"
|
||||
Conseal Option = "828"
|
||||
)
|
||||
|
||||
const (
|
||||
ActionSet = 1
|
||||
ActionUnset = 2
|
||||
)
|
||||
|
||||
type Option string
|
||||
|
||||
func (o Option) Apply(action int) string {
|
||||
v := string(o)
|
||||
|
||||
switch action {
|
||||
case ActionSet:
|
||||
return v[0:1]
|
||||
case ActionUnset:
|
||||
return v[1:]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type Color string
|
||||
|
||||
func (c Color) Apply(action int) string {
|
||||
if action == ActionSet {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
return string(Default)
|
||||
}
|
||||
88
output/style/style.go
Normal file
88
output/style/style.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package style
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var (
|
||||
styles = map[string]Style{
|
||||
"error": {Foreground: White, Background: Red},
|
||||
"info": {Foreground: Green},
|
||||
"comment": {Foreground: Yellow},
|
||||
"question": {Foreground: Black, Background: Cyan},
|
||||
}
|
||||
stylesMu sync.Mutex
|
||||
empty = Style{}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("console: style not found")
|
||||
ErrDuplicateStyle = errors.New("console: Register called twice")
|
||||
)
|
||||
|
||||
func Empty() Style {
|
||||
return empty
|
||||
}
|
||||
|
||||
func Find(name string) (Style, error) {
|
||||
if st, has := styles[name]; has {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
return empty, ErrNotFound
|
||||
}
|
||||
|
||||
func Register(name string, style Style) error {
|
||||
stylesMu.Lock()
|
||||
defer stylesMu.Unlock()
|
||||
|
||||
if _, has := styles[name]; has {
|
||||
return fmt.Errorf("%w for style %s", ErrDuplicateStyle, name)
|
||||
}
|
||||
|
||||
styles[name] = style
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MustRegister(name string, style Style) {
|
||||
if err := Register(name, style); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type Style struct {
|
||||
Background Color
|
||||
Foreground Color
|
||||
Options []Option
|
||||
}
|
||||
|
||||
func (s Style) Apply(msg string) string {
|
||||
return s.Set(ActionSet) + msg + s.Set(ActionUnset)
|
||||
}
|
||||
|
||||
func (s Style) Set(action int) string {
|
||||
style := make([]string, 0, len(s.Options))
|
||||
|
||||
if s.Foreground != "" {
|
||||
style = append(style, "3"+s.Foreground.Apply(action))
|
||||
}
|
||||
|
||||
if s.Background != "" {
|
||||
style = append(style, "4"+s.Background.Apply(action))
|
||||
}
|
||||
|
||||
for _, opt := range s.Options {
|
||||
style = append(style, opt.Apply(action))
|
||||
}
|
||||
|
||||
if len(style) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "\033[" + strings.Join(style, ";") + "m"
|
||||
}
|
||||
57
output/value.go
Normal file
57
output/value.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package output
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Type int
|
||||
|
||||
const (
|
||||
TypeAny Type = iota
|
||||
TypeBool
|
||||
TypeInt
|
||||
TypeInt64
|
||||
TypeUint
|
||||
TypeUint64
|
||||
TypeFloat64
|
||||
TypeString
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
vtype Type
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (v Value) String() string {
|
||||
return fmt.Sprint(v.value)
|
||||
}
|
||||
|
||||
func AnyValue(v interface{}) Value {
|
||||
return Value{vtype: TypeAny, value: v}
|
||||
}
|
||||
|
||||
func BoolValue(v bool) Value {
|
||||
return Value{vtype: TypeBool, value: v}
|
||||
}
|
||||
|
||||
func IntValue(v int) Value {
|
||||
return Value{vtype: TypeInt, value: v}
|
||||
}
|
||||
|
||||
func Int64Value(v int64) Value {
|
||||
return Value{vtype: TypeInt64, value: v}
|
||||
}
|
||||
|
||||
func UintValue(v uint) Value {
|
||||
return Value{vtype: TypeUint, value: v}
|
||||
}
|
||||
|
||||
func Uint64Value(v uint64) Value {
|
||||
return Value{vtype: TypeUint64, value: v}
|
||||
}
|
||||
|
||||
func Float64Value(v float64) Value {
|
||||
return Value{vtype: TypeFloat64, value: v}
|
||||
}
|
||||
|
||||
func StringValue(v string) Value {
|
||||
return Value{vtype: TypeString, value: v}
|
||||
}
|
||||
23
output/verbosity/norm.go
Normal file
23
output/verbosity/norm.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package verbosity
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
func Verb(out output.Output, verb output.Verbosity) output.Output {
|
||||
return func(ctx context.Context, v output.Verbosity, msg string, kv ...output.KeyValue) (int, error) {
|
||||
if verb >= v {
|
||||
return out(ctx, v, msg, kv...)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
func Quiet() output.Output {
|
||||
return func(context.Context, output.Verbosity, string, ...output.KeyValue) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
22
output/wrap/formatter.go
Normal file
22
output/wrap/formatter.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
"gitoa.ru/go-4devs/console/output/formatter"
|
||||
)
|
||||
|
||||
func Format(out output.Output, format *formatter.Formatter) output.Output {
|
||||
return func(ctx context.Context, v output.Verbosity, msg string, kv ...output.KeyValue) (int, error) {
|
||||
return out(ctx, v, format.Format(ctx, msg), kv...)
|
||||
}
|
||||
}
|
||||
|
||||
func Ansi(out output.Output) output.Output {
|
||||
return Format(out, formatter.Ansi())
|
||||
}
|
||||
|
||||
func None(out output.Output) output.Output {
|
||||
return Format(out, formatter.None())
|
||||
}
|
||||
44
output/writer/output.go
Normal file
44
output/writer/output.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package writer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
func Stderr() output.Output {
|
||||
return New(os.Stderr, String)
|
||||
}
|
||||
|
||||
func Stdout() output.Output {
|
||||
return New(os.Stdout, String)
|
||||
}
|
||||
|
||||
func Buffer(buf *bytes.Buffer) output.Output {
|
||||
return New(buf, String)
|
||||
}
|
||||
|
||||
func String(_ output.Verbosity, msg string, kv ...output.KeyValue) string {
|
||||
if len(kv) > 0 {
|
||||
newline := ""
|
||||
if msg[len(msg)-1:] == "\n" {
|
||||
newline = "\n"
|
||||
}
|
||||
|
||||
return "msg=\"" + strings.TrimSpace(msg) + "\", " + output.KeyValues(kv).String() + newline
|
||||
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func New(w io.Writer, format func(verb output.Verbosity, msg string, kv ...output.KeyValue) string) output.Output {
|
||||
return func(ctx context.Context, verb output.Verbosity, msg string, kv ...output.KeyValue) (int, error) {
|
||||
return fmt.Fprint(w, format(verb, msg, kv...))
|
||||
}
|
||||
}
|
||||
49
output/writer/output_test.go
Normal file
49
output/writer/output_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package writer_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
"gitoa.ru/go-4devs/console/output/writer"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
buf := bytes.Buffer{}
|
||||
wr := writer.New(&buf, writer.String)
|
||||
|
||||
cases := map[string]struct {
|
||||
ex string
|
||||
kv []output.KeyValue
|
||||
}{
|
||||
"message": {
|
||||
ex: "message",
|
||||
},
|
||||
"msg with kv": {
|
||||
ex: "msg=\"msg with kv\", string key=\"string value\", bool key=\"false\", int key=\"42\"",
|
||||
kv: []output.KeyValue{
|
||||
output.String("string key", "string value"),
|
||||
output.Bool("bool key", false),
|
||||
output.Int("int key", 42),
|
||||
},
|
||||
},
|
||||
"msg with newline \n": {
|
||||
ex: "msg=\"msg with newline\", int=\"42\"\n",
|
||||
kv: []output.KeyValue{
|
||||
output.Int("int", 42),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for msg, data := range cases {
|
||||
wr.InfoKV(ctx, msg, data.kv...)
|
||||
|
||||
if data.ex != buf.String() {
|
||||
t.Errorf("message not equals expext:%s, got:%s", data.ex, buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user