From 54014599d8f9b7187aa25412a39228fd081fb623 Mon Sep 17 00:00:00 2001 From: Antoine Grondin Date: Fri, 25 Oct 2024 13:17:08 +0900 Subject: [PATCH] feat(query): before fixing bugs, first you need tooling lol --- cmd/humanlog/query.go | 212 ++++++++++++-- cmd/humanlog/query_test.go | 69 +++++ go.mod | 1 + go.sum | 2 + internal/pkg/state/state.go | 25 +- pkg/sink/stdiosink/stdio.go | 11 +- vendor/github.com/crazy3lf/colorconv/LICENSE | 21 ++ .../github.com/crazy3lf/colorconv/README.md | 8 + .../crazy3lf/colorconv/colorconv.go | 272 ++++++++++++++++++ vendor/modules.txt | 3 + 10 files changed, 588 insertions(+), 36 deletions(-) create mode 100644 cmd/humanlog/query_test.go create mode 100644 vendor/github.com/crazy3lf/colorconv/LICENSE create mode 100644 vendor/github.com/crazy3lf/colorconv/README.md create mode 100644 vendor/github.com/crazy3lf/colorconv/colorconv.go diff --git a/cmd/humanlog/query.go b/cmd/humanlog/query.go index a32f4c10..c3c77526 100644 --- a/cmd/humanlog/query.go +++ b/cmd/humanlog/query.go @@ -3,25 +3,30 @@ package main import ( "context" "fmt" - "log" "log/slog" "net/http" "os" + "strconv" + "strings" "time" "connectrpc.com/connect" "github.com/NimbleMarkets/ntcharts/canvas/runes" "github.com/NimbleMarkets/ntcharts/linechart" "github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/term" + "github.com/crazy3lf/colorconv" "github.com/humanlogio/api/go/svc/account/v1/accountv1connect" "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" queryv1 "github.com/humanlogio/api/go/svc/query/v1" "github.com/humanlogio/api/go/svc/query/v1/queryv1connect" "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog/internal/pkg/config" "github.com/humanlogio/humanlog/internal/pkg/state" "github.com/humanlogio/humanlog/pkg/auth" + "github.com/humanlogio/humanlog/pkg/sink/stdiosink" "github.com/humanlogio/humanlog/pkg/tui" "github.com/urfave/cli" "google.golang.org/protobuf/types/known/timestamppb" @@ -117,19 +122,36 @@ func queryApiSummarizeCmd( ) cli.Command { fromFlag := cli.DurationFlag{Name: "since", Value: 365 * 24 * time.Hour} toFlag := cli.DurationFlag{Name: "to", Value: 0} + localhost := cli.BoolFlag{Name: "localhost"} return cli.Command{ Name: "summarize", - Flags: []cli.Flag{fromFlag, toFlag}, + Flags: []cli.Flag{localhost, fromFlag, toFlag}, Action: func(cctx *cli.Context) error { ctx := getCtx(cctx) state := getState(cctx) - ll := getLogger(cctx) - tokenSource := getTokenSource(cctx) - apiURL := getAPIUrl(cctx) - httpClient := getHTTPClient(cctx) - _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) - if err != nil { - return err + + var queryClient queryv1connect.QueryServiceClient + if !cctx.Bool(localhost.Name) { + ll := getLogger(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + queryClient = queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) + } else { + httpClient := getHTTPClient(cctx) + cfg := getCfg(cctx) + if cfg.ExperimentalFeatures == nil || cfg.ExperimentalFeatures.ServeLocalhostOnPort == nil { + return fmt.Errorf("localhost feature is not enabled or not configured, can't dial localhost") + } + addr := fmt.Sprintf("http://localhost:%d", *cfg.ExperimentalFeatures.ServeLocalhostOnPort) + queryClient = queryv1connect.NewQueryServiceClient(httpClient, addr) } termWidth, termHeight, err := term.GetSize(os.Stdout.Fd()) @@ -140,13 +162,8 @@ func queryApiSummarizeCmd( from := now.Add(-cctx.Duration(fromFlag.Name)) to := now.Add(-cctx.Duration(toFlag.Name)) - clOpts := connect.WithInterceptors( - auth.Interceptors(ll, tokenSource)..., - ) - queryClient := queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) - res, err := queryClient.SummarizeEvents(ctx, connect.NewRequest(&queryv1.SummarizeEventsRequest{ - AccountId: *state.AccountID, + AccountId: *state.CurrentAccountID, BucketCount: 20, From: timestamppb.New(from), To: timestamppb.New(to), @@ -215,11 +232,11 @@ func queryApiSummarizeCmd( } else { ts = t.Format(stepTimeFormat) } - log.Printf("label: ts=%v", ts) + loginfo("label: ts=%v", ts) return ts }) for _, bucket := range buckets { - log.Printf("ts=%v ev=%d", bucket.Ts.AsTime().Format(time.RFC3339Nano), bucket.GetEventCount()) + loginfo("ts=%v ev=%d", bucket.Ts.AsTime().Format(time.RFC3339Nano), bucket.GetEventCount()) tslc.Push(timeserieslinechart.TimePoint{ Time: bucket.Ts.AsTime(), Value: float64(bucket.GetEventCount()), @@ -244,25 +261,162 @@ func queryApiWatchCmd( getAPIUrl func(cctx *cli.Context) string, getHTTPClient func(*cli.Context) *http.Client, ) cli.Command { + fromFlag := cli.DurationFlag{Name: "since", Value: 365 * 24 * time.Hour} + toFlag := cli.DurationFlag{Name: "to", Value: 0} + localhost := cli.BoolFlag{Name: "localhost"} return cli.Command{ - Name: "watch", + Name: "watch", + Flags: []cli.Flag{localhost, fromFlag, toFlag}, Action: func(cctx *cli.Context) error { ctx := getCtx(cctx) + cfg := getCfg(cctx) state := getState(cctx) - tokenSource := getTokenSource(cctx) - apiURL := getAPIUrl(cctx) - httpClient := getHTTPClient(cctx) - _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + var queryClient queryv1connect.QueryServiceClient + if !cctx.Bool(localhost.Name) { + ll := getLogger(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + queryClient = queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) + } else { + httpClient := getHTTPClient(cctx) + + if cfg.ExperimentalFeatures == nil || cfg.ExperimentalFeatures.ServeLocalhostOnPort == nil { + return fmt.Errorf("localhost feature is not enabled or not configured, can't dial localhost") + } + addr := fmt.Sprintf("http://localhost:%d", *cfg.ExperimentalFeatures.ServeLocalhostOnPort) + queryClient = queryv1connect.NewQueryServiceClient(httpClient, addr) + } + now := time.Now() + from := now.Add(-cctx.Duration(fromFlag.Name)) + to := now.Add(-cctx.Duration(toFlag.Name)) + sinkOpts, errs := stdiosink.StdioOptsFrom(*cfg) + if len(errs) > 0 { + for _, err := range errs { + logerror("config error: %v", err) + } + } + + loginfo("from=%s", from) + loginfo("to=%s", to) + loginfo("query=%s", strings.Join(cctx.Args(), " ")) + + req := &queryv1.WatchQueryRequest{ + AccountId: *state.CurrentAccountID, + Query: &typesv1.LogQuery{ + From: timestamppb.New(from), + To: timestamppb.New(to), + }, + } + res, err := queryClient.WatchQuery(ctx, connect.NewRequest(req)) if err != nil { - return err + return fmt.Errorf("calling WatchQuery: %v", err) + } + defer res.Close() + + sink := stdiosink.NewStdio(os.Stdout, sinkOpts) + + for res.Receive() { + events := res.Msg().Events + for _, leg := range events { + prefix := getPrefix(leg.MachineId, leg.SessionId) + postProcess := func(pattern string) string { + return prefix + pattern + } + for _, ev := range leg.Logs { + if err := sink.ReceiveWithPostProcess(ctx, ev, postProcess); err != nil { + return fmt.Errorf("printing log: %v", err) + } + } + } + } + if err := res.Err(); err != nil { + return fmt.Errorf("querying: %v", err) } - ll := getLogger(cctx) - clOpts := connect.WithInterceptors( - auth.Interceptors(ll, tokenSource)..., - ) - queryClient := queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) - _ = queryClient return nil }, } } + +type tuple struct{ m, s int64 } + +var colorPrefixes = map[tuple]string{} + +func getPrefix(machine, session int64) string { + prefix, ok := colorPrefixes[tuple{m: machine, s: session}] + if ok { + return prefix + } + s := lipgloss.NewStyle(). + BorderStyle(lipgloss.DoubleBorder()).BorderRight(true) + + mPrefix := s.Background(lipgloss.AdaptiveColor{ + Light: int64toLightRGB(machine), + Dark: int64toDarkRGB(machine), + }).Render(strconv.FormatInt(machine, 10)) + sPrefix := s.Background(lipgloss.AdaptiveColor{ + Light: int64toLightRGB(session), + Dark: int64toDarkRGB(session), + }).Render(strconv.FormatInt(session, 10)) + + prefix = lipgloss.JoinHorizontal(lipgloss.Left, mPrefix, sPrefix) + colorPrefixes[tuple{m: machine, s: session}] = prefix + return prefix +} + +func int64toDarkRGB(n int64) string { + // modified from https://stackoverflow.com/a/52746259 + n = (374761397 + n*3266489917) & 0xffffffff + n = ((n ^ n>>15) * 2246822519) & 0xffffffff + n = ((n ^ n>>13) * 3266489917) & 0xffffffff + n = (n ^ n>>16) >> 8 + + hex := fmt.Sprintf("#%06x", n) + + // clamp the brightness + r, g, b, err := colorconv.HexToRGB(hex) + if err != nil { + panic(err) + } + h, s, v := colorconv.RGBToHSV(r, g, b) + if v > 0.5 { + v -= 0.5 + } + r, g, b, err = colorconv.HSVToRGB(h, s, v) + if err != nil { + panic(err) + } + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +func int64toLightRGB(n int64) string { + // modified from https://stackoverflow.com/a/52746259 + n = (374761397 + n*3266489917) & 0xffffffff + n = ((n ^ n>>15) * 2246822519) & 0xffffffff + n = ((n ^ n>>13) * 3266489917) & 0xffffffff + n = (n ^ n>>16) >> 8 + + hex := fmt.Sprintf("#%06x", n) + + // clamp the brightness + r, g, b, err := colorconv.HexToRGB(hex) + if err != nil { + panic(err) + } + h, s, v := colorconv.RGBToHSV(r, g, b) + if v < 0.5 { + v += 0.5 + } + r, g, b, err = colorconv.HSVToRGB(h, s, v) + if err != nil { + panic(err) + } + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} diff --git a/cmd/humanlog/query_test.go b/cmd/humanlog/query_test.go new file mode 100644 index 00000000..846c898d --- /dev/null +++ b/cmd/humanlog/query_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_int64toLightRGB(t *testing.T) { + tests := []struct { + name string + in int64 + want string + }{ + { + in: 62, + want: "#3aef45", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := int64toLightRGB(tt.in) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_int64toDarkRGB(t *testing.T) { + tests := []struct { + name string + in int64 + want string + }{ + { + in: 62, + want: "#1b6f20", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := int64toDarkRGB(tt.in) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_getPrefix(t *testing.T) { + type args struct { + machine int64 + session int64 + } + tests := []struct { + name string + args args + want string + }{ + { + args: args{machine: 1, session: 2}, + want: "1║2║", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getPrefix(tt.args.machine, tt.args.session); got != tt.want { + t.Errorf("getPrefix() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index bf8b2a8c..30a70d49 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/charmbracelet/lipgloss v0.13.0 github.com/charmbracelet/x/term v0.2.0 github.com/cli/safeexec v1.0.1 + github.com/crazy3lf/colorconv v1.2.0 github.com/fatih/color v1.16.0 github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 github.com/go-logfmt/logfmt v0.5.1 diff --git a/go.sum b/go.sum index d8adc02d..9f503394 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crazy3lf/colorconv v1.2.0 h1:UM7kSZWnwFMGiC+PpYrjxQSOd6sEyWb+dRKKTd3KslA= +github.com/crazy3lf/colorconv v1.2.0/go.mod h1:2jTJ7QCWCj2sSLOhF4Gzi0J5/hoX8/VY8VzNvXAlD1I= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/pkg/state/state.go b/internal/pkg/state/state.go index 466ad82c..4df38b08 100644 --- a/internal/pkg/state/state.go +++ b/internal/pkg/state/state.go @@ -105,20 +105,24 @@ func WriteStateFile(path string, state *State) error { } type State struct { - Version int `json:"version"` - AccountID *int64 `json:"account_id"` - MachineID *int64 `json:"machine_id"` - LatestKnownVersion *semver.Version `json:"latest_known_version,omitempty"` - LastestKnownVersionUpdatedAt *time.Time `json:"latest_known_version_updated_at"` + // to convert across old vs. new version of `type State struct` + Version int `json:"version"` + // set for ingestion purpose + AccountID *int64 `json:"account_id"` + MachineID *int64 `json:"machine_id"` IngestionToken *typesv1.AccountToken `json:"ingestion_token,omitempty"` + // update mechanism + LatestKnownVersion *semver.Version `json:"latest_known_version,omitempty"` + LastestKnownVersionUpdatedAt *time.Time `json:"latest_known_version_updated_at"` + // preferences set in the CLI/TUI when querying CurrentOrgID *int64 `json:"current_org_id,omitempty"` CurrentAccountID *int64 `json:"current_account_id,omitempty"` CurrentMachineID *int64 `json:"current_machine_id,omitempty"` - // unexported + // unexported, the filepath where the `State` get's serialized and saved to path string } @@ -141,5 +145,14 @@ func (cfg State) populateEmpty(other *State) *State { if out.LastestKnownVersionUpdatedAt == nil && other.LastestKnownVersionUpdatedAt != nil { out.LastestKnownVersionUpdatedAt = other.LastestKnownVersionUpdatedAt } + if out.CurrentOrgID == nil && other.CurrentOrgID != nil { + out.CurrentOrgID = other.CurrentOrgID + } + if out.CurrentAccountID == nil && other.CurrentAccountID != nil { + out.CurrentAccountID = other.CurrentAccountID + } + if out.CurrentMachineID == nil && other.CurrentMachineID != nil { + out.CurrentMachineID = other.CurrentMachineID + } return &out } diff --git a/pkg/sink/stdiosink/stdio.go b/pkg/sink/stdiosink/stdio.go index 0c655f38..ed7b9171 100644 --- a/pkg/sink/stdiosink/stdio.go +++ b/pkg/sink/stdiosink/stdio.go @@ -132,6 +132,10 @@ func (std *Stdio) Flush(ctx context.Context) error { } func (std *Stdio) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + return std.ReceiveWithPostProcess(ctx, ev, nil) +} + +func (std *Stdio) ReceiveWithPostProcess(ctx context.Context, ev *typesv1.LogEvent, postProcess func(string) string) error { if ev.Structured == nil { std.lastRaw = true std.lastLevel = "" @@ -200,7 +204,12 @@ func (std *Stdio) Receive(ctx context.Context, ev *typesv1.LogEvent) error { } timestr = timeColor.Sprint(ts.Format(std.opts.TimeFormat)) } - _, _ = fmt.Fprintf(out, "%s |%s| %s\t %s", + + pattern := "%s |%s| %s\t %s" + if postProcess != nil { + pattern = postProcess(pattern) + } + _, _ = fmt.Fprintf(out, pattern, timestr, level, msg, diff --git a/vendor/github.com/crazy3lf/colorconv/LICENSE b/vendor/github.com/crazy3lf/colorconv/LICENSE new file mode 100644 index 00000000..98ba0bfc --- /dev/null +++ b/vendor/github.com/crazy3lf/colorconv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Shu Fu Xiang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/crazy3lf/colorconv/README.md b/vendor/github.com/crazy3lf/colorconv/README.md new file mode 100644 index 00000000..fa184841 --- /dev/null +++ b/vendor/github.com/crazy3lf/colorconv/README.md @@ -0,0 +1,8 @@ +# colorconv +[![Go Reference](https://pkg.go.dev/badge/github.com/crazy3lf/colorconv.svg)](https://pkg.go.dev/github.com/crazy3lf/colorconv) +[![Go Report Card](https://goreportcard.com/badge/github.com/Crazy3lf/colorconv)](https://goreportcard.com/report/github.com/Crazy3lf/colorconv) + +Library to support RGB conversion to HSL, HSV and Hex value for Golang. +This library make use of Go standard library with no external dependency. +### INSTALLATION / UPDATING + go get -u github.com/crazy3lf/colorconv diff --git a/vendor/github.com/crazy3lf/colorconv/colorconv.go b/vendor/github.com/crazy3lf/colorconv/colorconv.go new file mode 100644 index 00000000..48c60639 --- /dev/null +++ b/vendor/github.com/crazy3lf/colorconv/colorconv.go @@ -0,0 +1,272 @@ +// Package colorconv provide conversion of color to HSL, HSV and hex value. +// All the conversion methods is based on the website: https://www.rapidtables.com/convert/color/index.html +package colorconv + +import ( + "errors" + "fmt" + "image/color" + "math" + "strconv" + "strings" +) + +var ErrInvalidHexValue = errors.New("colorconv: invalid input") +var ErrOutOfRange = errors.New("colorconv: inputs out of range") + +//ColorToHSL convert color.Color into HSL triple, ignoring the alpha channel. +func ColorToHSL(c color.Color) (h, s, l float64) { + r, g, b, _ := c.RGBA() + return RGBToHSL(uint8(r>>8), uint8(g>>8), uint8(b>>8)) +} + +//ColorToHSV convert color.Color into HSV triple, ignoring the alpha channel. +func ColorToHSV(c color.Color) (h, s, v float64) { + r, g, b, _ := c.RGBA() + return RGBToHSV(uint8(r>>8), uint8(g>>8), uint8(b>>8)) +} + +//ColorToHex convert color.Color into Hex string, ignoring the alpha channel. +func ColorToHex(c color.Color) string { + r, g, b, _ := c.RGBA() + return RGBToHex(uint8(r>>8), uint8(g>>8), uint8(b>>8)) +} + +//HSLToColor convert HSL triple into color.Color. +func HSLToColor(h, s, l float64) (color.Color, error) { + r, g, b, err := HSLToRGB(h, s, l) + if err != nil { + return nil, err + } + return color.RGBA{R: r, G: g, B: b, A: 0}, nil +} + +//HSVToColor convert HSV triple into color.Color. +func HSVToColor(h, s, v float64) (color.Color, error) { + r, g, b, err := HSVToRGB(h, s, v) + if err != nil { + return nil, err + } + return color.RGBA{R: r, G: g, B: b, A: 0}, nil +} + +//HexToColor convert Hex string into color.Color. +func HexToColor(hex string) (color.Color, error) { + r, g, b, err := HexToRGB(hex) + if err != nil { + return nil, err + } + return color.RGBA{R: r, G: g, B: b, A: 0}, nil +} + +//RGBToHSL converts an RGB triple to an HSL triple. +func RGBToHSL(r, g, b uint8) (h, s, l float64) { + // convert uint32 pre-multiplied value to uint8 + // The r,g,b values are divided by 255 to change the range from 0..255 to 0..1: + Rnot := float64(r) / 255 + Gnot := float64(g) / 255 + Bnot := float64(b) / 255 + Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot) + Δ := Cmax - Cmin + // Lightness calculation: + l = (Cmax + Cmin) / 2 + // Hue and Saturation Calculation: + if Δ == 0 { + h = 0 + s = 0 + } else { + switch Cmax { + case Rnot: + h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6)) + case Gnot: + h = 60 * (((Bnot - Rnot) / Δ) + 2) + case Bnot: + h = 60 * (((Rnot - Gnot) / Δ) + 4) + } + if h < 0 { + h += 360 + } + + s = Δ / (1 - math.Abs((2*l)-1)) + } + + return h, round(s), round(l) +} + +//HSLToRGB converts an HSL triple to an RGB triple. +func HSLToRGB(h, s, l float64) (r, g, b uint8, err error) { + if h < 0 || h >= 360 || + s < 0 || s > 1 || + l < 0 || l > 1 { + return 0, 0, 0, ErrOutOfRange + } + // When 0 ≤ h < 360, 0 ≤ s ≤ 1 and 0 ≤ l ≤ 1: + C := (1 - math.Abs((2*l)-1)) * s + X := C * (1 - math.Abs(math.Mod(h/60, 2)-1)) + m := l - (C / 2) + var Rnot, Gnot, Bnot float64 + + switch { + case 0 <= h && h < 60: + Rnot, Gnot, Bnot = C, X, 0 + case 60 <= h && h < 120: + Rnot, Gnot, Bnot = X, C, 0 + case 120 <= h && h < 180: + Rnot, Gnot, Bnot = 0, C, X + case 180 <= h && h < 240: + Rnot, Gnot, Bnot = 0, X, C + case 240 <= h && h < 300: + Rnot, Gnot, Bnot = X, 0, C + case 300 <= h && h < 360: + Rnot, Gnot, Bnot = C, 0, X + } + r = uint8(math.Round((Rnot + m) * 255)) + g = uint8(math.Round((Gnot + m) * 255)) + b = uint8(math.Round((Bnot + m) * 255)) + return r, g, b, nil +} + +//RGBToHSV converts an RGB triple to an HSV triple. +func RGBToHSV(r, g, b uint8) (h, s, v float64) { + // convert uint32 pre-multiplied value to uint8 + // The r,g,b values are divided by 255 to change the range from 0..255 to 0..1: + Rnot := float64(r) / 255 + Gnot := float64(g) / 255 + Bnot := float64(b) / 255 + Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot) + Δ := Cmax - Cmin + + // Hue calculation: + if Δ == 0 { + h = 0 + } else { + switch Cmax { + case Rnot: + h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6)) + case Gnot: + h = 60 * (((Bnot - Rnot) / Δ) + 2) + case Bnot: + h = 60 * (((Rnot - Gnot) / Δ) + 4) + } + if h < 0 { + h += 360 + } + + } + // Saturation calculation: + if Cmax == 0 { + s = 0 + } else { + s = Δ / Cmax + } + // Value calculation: + v = Cmax + + return h, round(s), round(v) +} + +//HSVToRGB converts an HSV triple to an RGB triple. +func HSVToRGB(h, s, v float64) (r, g, b uint8, err error) { + if h < 0 || h >= 360 || + s < 0 || s > 1 || + v < 0 || v > 1 { + return 0, 0, 0, ErrOutOfRange + } + // When 0 ≤ h < 360, 0 ≤ s ≤ 1 and 0 ≤ v ≤ 1: + C := v * s + X := C * (1 - math.Abs(math.Mod(h/60, 2)-1)) + m := v - C + var Rnot, Gnot, Bnot float64 + switch { + case 0 <= h && h < 60: + Rnot, Gnot, Bnot = C, X, 0 + case 60 <= h && h < 120: + Rnot, Gnot, Bnot = X, C, 0 + case 120 <= h && h < 180: + Rnot, Gnot, Bnot = 0, C, X + case 180 <= h && h < 240: + Rnot, Gnot, Bnot = 0, X, C + case 240 <= h && h < 300: + Rnot, Gnot, Bnot = X, 0, C + case 300 <= h && h < 360: + Rnot, Gnot, Bnot = C, 0, X + } + r = uint8(math.Round((Rnot + m) * 255)) + g = uint8(math.Round((Gnot + m) * 255)) + b = uint8(math.Round((Bnot + m) * 255)) + return r, g, b, nil +} + +//RGBToHex converts an RGB triple to a Hex string in the format of 0xffff. +func RGBToHex(r, g, b uint8) string { + return fmt.Sprintf("0x%02x%02x%02x", r, g, b) +} + +//HexToRGB converts a Hex string to an RGB triple. +func HexToRGB(hex string) (r, g, b uint8, err error) { + // remove prefixes if found in the input string + hex = strings.Replace(hex, "0x", "", -1) + hex = strings.Replace(hex, "#", "", -1) + if len(hex) != 6 { + return 0, 0, 0, ErrInvalidHexValue + } + + r, err = hex2uint8(hex[0:2]) + if err != nil { + return 0, 0, 0, err + } + g, err = hex2uint8(hex[2:4]) + if err != nil { + return 0, 0, 0, err + } + b, err = hex2uint8(hex[4:6]) + if err != nil { + return 0, 0, 0, err + } + return r, g, b, nil +} + +//RGBToGrayAverage calculates the grayscale value of RGB with the average method, ignoring the alpha channel. +func RGBToGrayAverage(r, g, b uint8) color.Gray { + return RGBToGrayWithWeight(r, g, b, 1, 1, 1) +} + +// RGBToGrayWithWeight calculates the grayscale value of RGB wih provided weight, ignoring the alpha channel. +// In the standard library image/color, the conversion used the coefficient given by the JFIF specification. It is +// equivalent to using the weight 299, 587, 114 for rgb. +func RGBToGrayWithWeight(r, g, b uint8, rWeight, gWeight, bWeight uint) color.Gray { + rw := uint(r) * rWeight + gw := uint(g) * gWeight + bw := uint(b) * bWeight + + return color.Gray{Y: uint8(math.Round(float64(rw+gw+bw) / float64(rWeight+gWeight+bWeight)))} +} + +func hex2uint8(hexStr string) (uint8, error) { + // base 16 for hexadecimal + result, err := strconv.ParseUint(hexStr, 16, 8) + if err != nil { + return 0, err + } + return uint8(result), nil +} + +func getMaxMin(a, b, c float64) (max, min float64) { + if a > b { + max = a + min = b + } else { + max = b + min = a + } + if c > max { + max = c + } else if c < min { + min = c + } + return max, min +} + +func round(x float64) float64 { + return math.Round(x*1000) / 1000 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 6a30d80d..abf1b5d0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -75,6 +75,9 @@ github.com/cli/safeexec # github.com/cpuguy83/go-md2man/v2 v2.0.2 ## explicit; go 1.11 github.com/cpuguy83/go-md2man/v2/md2man +# github.com/crazy3lf/colorconv v1.2.0 +## explicit; go 1.17 +github.com/crazy3lf/colorconv # github.com/danieljoos/wincred v1.2.0 ## explicit; go 1.18 github.com/danieljoos/wincred