From a3545d006af1cf6d2c724ad427327fb232306b57 Mon Sep 17 00:00:00 2001 From: Tobias Fried Date: Mon, 22 Jul 2024 20:59:11 -0600 Subject: [PATCH] feat(cmd): remove tablewriter in favor of charm (#374) --- cmd/lk/egress.go | 12 ++++----- cmd/lk/ingress.go | 12 ++++----- cmd/lk/project.go | 27 ++++++++++++++----- cmd/lk/proto.go | 12 +++++---- cmd/lk/replay.go | 11 +++----- cmd/lk/sip.go | 8 +++--- cmd/lk/style.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/lk/utils.go | 67 +++++++++++++++-------------------------------- go.mod | 2 +- go.sum | 3 --- 10 files changed, 133 insertions(+), 87 deletions(-) create mode 100644 cmd/lk/style.go diff --git a/cmd/lk/egress.go b/cmd/lk/egress.go index bd4b5e76..f888c14d 100644 --- a/cmd/lk/egress.go +++ b/cmd/lk/egress.go @@ -26,7 +26,6 @@ import ( "syscall" "time" - "github.com/olekukonko/tablewriter" "github.com/pkg/browser" "github.com/urfave/cli/v3" "google.golang.org/protobuf/encoding/protojson" @@ -596,8 +595,8 @@ func listEgress(ctx context.Context, cmd *cli.Command) error { items = res.Items } - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"EgressID", "Status", "Type", "Source", "Started At", "Error"}) + table := CreateTable(). + Headers("EgressID", "Status", "Type", "Source", "Started At", "Error") for _, item := range items { var startedAt string if item.StartedAt != 0 { @@ -628,17 +627,16 @@ func listEgress(ctx context.Context, cmd *cli.Command) error { egressType = "track" egressSource = fmt.Sprintf("%s/%s", req.Track.RoomName, req.Track.TrackId) } - - table.Append([]string{ + table.Row( item.EgressId, item.Status.String(), egressType, egressSource, startedAt, item.Error, - }) + ) } - table.Render() + fmt.Println(table) return nil } diff --git a/cmd/lk/ingress.go b/cmd/lk/ingress.go index b7020366..e77a2e0b 100644 --- a/cmd/lk/ingress.go +++ b/cmd/lk/ingress.go @@ -17,9 +17,7 @@ package main import ( "context" "fmt" - "os" - "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v3" "github.com/livekit/protocol/livekit" @@ -225,8 +223,8 @@ func listIngress(ctx context.Context, cmd *cli.Command) error { return err } - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"IngressID", "Name", "Room", "StreamKey", "URL", "Status", "Error"}) + table := CreateTable(). + Headers("IngressID", "Name", "Room", "StreamKey", "URL", "Status", "Error") for _, item := range res.Items { if item == nil { continue @@ -238,7 +236,7 @@ func listIngress(ctx context.Context, cmd *cli.Command) error { errorStr = item.State.Error } - table.Append([]string{ + table.Row( item.IngressId, item.Name, item.RoomName, @@ -246,9 +244,9 @@ func listIngress(ctx context.Context, cmd *cli.Command) error { item.Url, status, errorStr, - }) + ) } - table.Render() + fmt.Println(table) if cmd.Bool("verbose") { PrintJSON(res) diff --git a/cmd/lk/project.go b/cmd/lk/project.go index 450bcec6..fcffff4a 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -23,7 +23,7 @@ import ( "regexp" "github.com/charmbracelet/huh" - "github.com/olekukonko/tablewriter" + "github.com/charmbracelet/lipgloss" "github.com/urfave/cli/v3" "github.com/livekit/livekit-cli/pkg/config" @@ -242,13 +242,28 @@ func listProjects(ctx context.Context, cmd *cli.Command) error { return nil } - table := tablewriter.NewWriter(os.Stdout) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", "URL", "API Key", "Default"}) + re := lipgloss.NewRenderer(os.Stdout) + baseStyle := re.NewStyle().Padding(0, 1) + headerStyle := baseStyle.Bold(true) + selectedStyle := baseStyle.Foreground(cyan) + + table := CreateTable(). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == 0: + return headerStyle + case cliConfig.Projects[row-1].Name == cliConfig.DefaultProject: + return selectedStyle + default: + return baseStyle + } + }). + Headers("Name", "URL", "API Key", "Default") for _, p := range cliConfig.Projects { - table.Append([]string{p.Name, p.URL, p.APIKey, fmt.Sprint(p.Name == cliConfig.DefaultProject)}) + table.Row(p.Name, p.URL, p.APIKey, fmt.Sprint(p.Name == cliConfig.DefaultProject)) } - table.Render() + fmt.Println(table) + return nil } diff --git a/cmd/lk/proto.go b/cmd/lk/proto.go index bafc12c0..51d56465 100644 --- a/cmd/lk/proto.go +++ b/cmd/lk/proto.go @@ -17,10 +17,10 @@ package main import ( "context" "errors" + "fmt" "os" "reflect" - "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v3" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" @@ -183,8 +183,8 @@ func listAndPrint[ return err } - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(header) + table := CreateTable(). + Headers(header...) for _, item := range res.GetItems() { if item == nil { continue @@ -193,11 +193,13 @@ func listAndPrint[ if len(row) == 0 { continue } - table.Append(row) + table.Row(row...) } - table.Render() + fmt.Println(table) + if cmd.Bool("verbose") { PrintJSON(res) } + return nil } diff --git a/cmd/lk/replay.go b/cmd/lk/replay.go index 35842572..eb49fa6b 100644 --- a/cmd/lk/replay.go +++ b/cmd/lk/replay.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "net/http" - "os" - "github.com/olekukonko/tablewriter" "github.com/twitchtv/twirp" "github.com/urfave/cli/v3" @@ -127,14 +125,11 @@ func listReplays(ctx context.Context, _ *cli.Command) error { return err } - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"ReplayID"}) + table := CreateTable().Headers("ReplayID") for _, info := range res.Replays { - table.Append([]string{ - info.ReplayId, - }) + table.Row(info.ReplayId) } - table.Render() + fmt.Println(table) return nil } diff --git a/cmd/lk/sip.go b/cmd/lk/sip.go index 224bd73f..53b82e83 100644 --- a/cmd/lk/sip.go +++ b/cmd/lk/sip.go @@ -261,7 +261,7 @@ func listSipTrunk(ctx context.Context, cmd *cli.Command) error { } //lint:ignore SA1019 we still support it return listAndPrint(ctx, cmd, cli.ListSIPTrunk, &livekit.ListSIPTrunkRequest{}, []string{ - "SipTrunkId", "Name", "Kind", "Number", + "SipTrunkID", "Name", "Kind", "Number", "AllowAddresses", "AllowNumbers", "InboundAuth", "OutboundAddress", "OutboundAuth", "Metadata", @@ -285,7 +285,7 @@ func listSipInboundTrunk(ctx context.Context, cmd *cli.Command) error { return err } return listAndPrint(ctx, cmd, cli.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{}, []string{ - "SipTrunkId", "Name", "Numbers", + "SipTrunkID", "Name", "Numbers", "AllowedAddresses", "AllowedNumbers", "Authentication", "Metadata", @@ -305,7 +305,7 @@ func listSipOutboundTrunk(ctx context.Context, cmd *cli.Command) error { return err } return listAndPrint(ctx, cmd, cli.ListSIPOutboundTrunk, &livekit.ListSIPOutboundTrunkRequest{}, []string{ - "SipTrunkId", "Name", + "SipTrunkID", "Name", "Address", "Transport", "Numbers", "Authentication", @@ -387,7 +387,7 @@ func listSipDispatchRule(ctx context.Context, cmd *cli.Command) error { return err } return listAndPrint(ctx, cmd, cli.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{}, []string{ - "SipDispatchRuleId", "Name", "SipTrunks", "Type", "RoomName", "Pin", "HidePhone", "Metadata", + "SipDispatchRuleID", "Name", "SipTrunks", "Type", "RoomName", "Pin", "HidePhone", "Metadata", }, func(item *livekit.SIPDispatchRuleInfo) []string { var room, typ, pin string switch r := item.GetRule().GetRule().(type) { diff --git a/cmd/lk/style.go b/cmd/lk/style.go new file mode 100644 index 00000000..5511ca2d --- /dev/null +++ b/cmd/lk/style.go @@ -0,0 +1,66 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +var ( + normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} + normalBg = lipgloss.AdaptiveColor{Light: "20", Dark: "0"} + dimFg = lipgloss.AdaptiveColor{Light: "", Dark: "243"} + placeholderFg = lipgloss.AdaptiveColor{Light: "248", Dark: "238"} + cyan = lipgloss.AdaptiveColor{Light: "#06B7DB", Dark: "#1FD5F9"} + red = lipgloss.AdaptiveColor{Light: "#CE4A3B", Dark: "#FF6352"} + yellow = lipgloss.AdaptiveColor{Light: "#DB9406", Dark: "#F9B11F"} + green = lipgloss.AdaptiveColor{Light: "#036D26", Dark: "#06DB4D"} + + theme = func() *huh.Theme { + t := huh.ThemeBase() + + t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) + t.Focused.Title = t.Focused.Title.Foreground(cyan).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(cyan).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(cyan) + t.Focused.Description = t.Focused.Description.Foreground(dimFg) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(yellow) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(yellow) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(yellow) + t.Focused.Option = t.Focused.Option.Foreground(normalFg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(yellow) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(green).SetString("✓ ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(dimFg).SetString("• ") + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(normalBg).Background(cyan) + t.Focused.Next = t.Focused.FocusedButton + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"}) + + // t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(placeholderFg) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(yellow) + + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + return t + }() +) diff --git a/cmd/lk/utils.go b/cmd/lk/utils.go index fa814ce2..b3197fa4 100644 --- a/cmd/lk/utils.go +++ b/cmd/lk/utils.go @@ -22,8 +22,8 @@ import ( "path/filepath" "strings" - "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" "github.com/livekit/protocol/utils/interceptors" "github.com/twitchtv/twirp" "github.com/urfave/cli/v3" @@ -81,51 +81,6 @@ var ( Persistent: true, }, } - theme = func() *huh.Theme { - t := huh.ThemeBase() - - var ( - normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} - normalBg = lipgloss.AdaptiveColor{Light: "20", Dark: "0"} - dimFg = lipgloss.AdaptiveColor{Light: "", Dark: "243"} - placeholderFg = lipgloss.AdaptiveColor{Light: "248", Dark: "238"} - cyan = lipgloss.AdaptiveColor{Light: "#06B7DB", Dark: "#1FD5F9"} - red = lipgloss.AdaptiveColor{Light: "#CE4A3B", Dark: "#FF6352"} - yellow = lipgloss.AdaptiveColor{Light: "#DB9406", Dark: "#F9B11F"} - green = lipgloss.AdaptiveColor{Light: "#036D26", Dark: "#06DB4D"} - ) - - t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) - t.Focused.Title = t.Focused.Title.Foreground(cyan).Bold(true) - t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(cyan).Bold(true).MarginBottom(1) - t.Focused.Directory = t.Focused.Directory.Foreground(cyan) - t.Focused.Description = t.Focused.Description.Foreground(dimFg) - t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) - t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) - t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(yellow) - t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(yellow) - t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(yellow) - t.Focused.Option = t.Focused.Option.Foreground(normalFg) - t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(yellow) - t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green) - t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(green).SetString("✓ ") - t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(dimFg).SetString("• ") - t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) - t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(normalBg).Background(cyan) - t.Focused.Next = t.Focused.FocusedButton - t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"}) - - // t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow) - t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(placeholderFg) - t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(yellow) - - t.Blurred = t.Focused - t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) - t.Blurred.NextIndicator = lipgloss.NewStyle() - t.Blurred.PrevIndicator = lipgloss.NewStyle() - - return t - }() ) func optional[T any, C any, VC cli.ValueCreator[T, C]](flag *cli.FlagBase[T, C, VC]) *cli.FlagBase[T, C, VC] { @@ -199,6 +154,26 @@ func PrintJSON(obj any) { fmt.Println(string(txt)) } +func CreateTable() *table.Table { + re := lipgloss.NewRenderer(os.Stdout) + baseStyle := re.NewStyle().Padding(0, 1) + headerStyle := baseStyle.Bold(true) + + styleFunc := func(row, col int) lipgloss.Style { + if row == 0 { + return headerStyle + } + return baseStyle + } + + t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(re.NewStyle().Foreground(normalFg)). + StyleFunc(styleFunc) + + return t +} + func ExpandUser(p string) string { if strings.HasPrefix(p, "~") { home, _ := os.UserHomeDir() diff --git a/go.mod b/go.mod index cb6f5dd0..4c6e8c62 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ toolchain go1.22.2 require ( github.com/charmbracelet/huh v0.5.1 + github.com/charmbracelet/huh/spinner v0.0.0-20240714135825-43e9eb5aeab6 github.com/charmbracelet/lipgloss v0.12.1 github.com/frostbyte73/core v0.0.10 github.com/go-logr/logr v1.4.2 github.com/livekit/protocol v1.19.2-0.20240710171229-73ece66d30e0 github.com/livekit/server-sdk-go/v2 v2.2.0 - github.com/olekukonko/tablewriter v0.0.5 github.com/pion/rtcp v1.2.14 github.com/pion/rtp v1.8.7 github.com/pion/webrtc/v3 v3.2.49 diff --git a/go.sum b/go.sum index 769c12d7..2bf91a81 100644 --- a/go.sum +++ b/go.sum @@ -117,7 +117,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -134,8 +133,6 @@ github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo= github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=