mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
feat(local): sg tail (#64146)
This PR brings back https://github.com/sourcegraph/sgtail back in `sg`, plus a few adjustments to make it easier to use. I'll archive that repo once this PR lands. @camdencheek mentioned you here as you've been the most recent beta tester, it's more an FYI than a request for a review, though it's welcome if you want to spend a bit of time reading this. Closes DINF-155 ## Test plan Locally tested + new unit test + CI ## Changelog - Adds a new `sg tail` command that provides a better UI to tail and filter log messages from `sg start --tail`.
This commit is contained in:
parent
62b4718bd9
commit
bc4acd1fbd
120
deps.bzl
120
deps.bzl
@ -373,6 +373,13 @@ def go_dependencies():
|
||||
sum = "h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=",
|
||||
version = "v0.0.0-20230301143203-a9d515a09cc2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_atotto_clipboard",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/atotto/clipboard",
|
||||
sum = "h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=",
|
||||
version = "v0.1.4",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_aws_aws_sdk_go",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -562,6 +569,13 @@ def go_dependencies():
|
||||
sum = "h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=",
|
||||
version = "v2.0.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_aymanbagabas_go_udiff",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/aymanbagabas/go-udiff",
|
||||
sum = "h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=",
|
||||
version = "v0.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_aymerick_douceur",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -1021,6 +1035,20 @@ def go_dependencies():
|
||||
sum = "h1:riuOFg3Ay1Js10GQtCAsCL2Hp2DJweUlYjKaxXteYV8=",
|
||||
version = "v0.0.0-20240130195846-91a06ffe6715",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_bubbles",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/bubbles",
|
||||
sum = "h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=",
|
||||
version = "v0.18.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_bubbletea",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/bubbletea",
|
||||
sum = "h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=",
|
||||
version = "v0.26.6",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_glamour",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -1028,6 +1056,48 @@ def go_dependencies():
|
||||
sum = "h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=",
|
||||
version = "v0.7.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_harmonica",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/harmonica",
|
||||
sum = "h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=",
|
||||
version = "v0.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_lipgloss",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/lipgloss",
|
||||
sum = "h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=",
|
||||
version = "v0.12.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_x_ansi",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/x/ansi",
|
||||
sum = "h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=",
|
||||
version = "v0.1.4",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_x_input",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/x/input",
|
||||
sum = "h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=",
|
||||
version = "v0.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_x_term",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/x/term",
|
||||
sum = "h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=",
|
||||
version = "v0.1.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_charmbracelet_x_windows",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/charmbracelet/x/windows",
|
||||
sum = "h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=",
|
||||
version = "v0.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_chromedp_cdproto",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -1206,8 +1276,8 @@ def go_dependencies():
|
||||
name = "com_github_containerd_console",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/containerd/console",
|
||||
sum = "h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=",
|
||||
version = "v1.0.3",
|
||||
sum = "h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=",
|
||||
version = "v1.0.4-0.20230313162750-1ae8d489ac81",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_containerd_containerd",
|
||||
@ -1839,6 +1909,13 @@ def go_dependencies():
|
||||
sum = "h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU=",
|
||||
version = "v0.0.0-20200331213906-ae555eb2afa4",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_erikgeiser_coninput",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/erikgeiser/coninput",
|
||||
sum = "h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=",
|
||||
version = "v0.0.0-20211004153227-1c3628e74d0f",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_evanphx_json_patch",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -4280,6 +4357,13 @@ def go_dependencies():
|
||||
sum = "h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=",
|
||||
version = "v0.0.20",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_mattn_go_localereader",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/mattn/go-localereader",
|
||||
sum = "h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=",
|
||||
version = "v0.0.1",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_mattn_go_runewidth",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -4630,6 +4714,20 @@ def go_dependencies():
|
||||
sum = "h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=",
|
||||
version = "v0.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_muesli_ansi",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/muesli/ansi",
|
||||
sum = "h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=",
|
||||
version = "v0.0.0-20230316100256-276c6243b2f6",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_muesli_cancelreader",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/muesli/cancelreader",
|
||||
sum = "h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=",
|
||||
version = "v0.2.2",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_muesli_reflow",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -5371,8 +5469,8 @@ def go_dependencies():
|
||||
name = "com_github_rivo_uniseg",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/rivo/uniseg",
|
||||
sum = "h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=",
|
||||
version = "v0.4.6",
|
||||
sum = "h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=",
|
||||
version = "v0.4.7",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_rjeczalik_notify",
|
||||
@ -5508,6 +5606,13 @@ def go_dependencies():
|
||||
sum = "h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=",
|
||||
version = "v0.1.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_sahilm_fuzzy",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/sahilm/fuzzy",
|
||||
sum = "h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=",
|
||||
version = "v0.1.1-0.20230530133925-c48e322e2a8f",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_satori_go_uuid",
|
||||
build_file_proto_mode = "disable_global",
|
||||
@ -6517,6 +6622,13 @@ def go_dependencies():
|
||||
sum = "h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=",
|
||||
version = "v1.2.0",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_xo_terminfo",
|
||||
build_file_proto_mode = "disable_global",
|
||||
importpath = "github.com/xo/terminfo",
|
||||
sum = "h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=",
|
||||
version = "v0.0.0-20220910002029-abceb7e1c41e",
|
||||
)
|
||||
go_repository(
|
||||
name = "com_github_xordataexchange_crypt",
|
||||
build_file_proto_mode = "disable_global",
|
||||
|
||||
@ -80,6 +80,7 @@ go_library(
|
||||
"//dev/sg/msp",
|
||||
"//dev/sg/root",
|
||||
"//dev/sg/sams",
|
||||
"//dev/sg/tail",
|
||||
"//dev/team",
|
||||
"//internal/accesstoken",
|
||||
"//internal/collections",
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/msp"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/root"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/sams"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/tail"
|
||||
"github.com/sourcegraph/sourcegraph/internal/collections"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
@ -318,6 +319,7 @@ var sg = &cli.App{
|
||||
srcCommand,
|
||||
srcInstanceCommand,
|
||||
imagesCommand,
|
||||
tail.Command,
|
||||
|
||||
// Company
|
||||
teammateCommand,
|
||||
|
||||
@ -86,8 +86,8 @@ sg start --commands frontend gitserver
|
||||
Usage: "Print details about the selected commandset",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "sgtail",
|
||||
Usage: "Connects to running sgtail instance",
|
||||
Name: "tail",
|
||||
Usage: "Connects to a running sg tail instance",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "profile",
|
||||
@ -206,9 +206,9 @@ func startExec(ctx *cli.Context) error {
|
||||
return errors.New("no concurrent sg start with same arguments allowed")
|
||||
}
|
||||
|
||||
if ctx.Bool("sgtail") {
|
||||
if ctx.Bool("tail") {
|
||||
if err := run.OpenUnixSocket(); err != nil {
|
||||
return errors.Wrapf(err, "Did you forget to run sgtail first?")
|
||||
return errors.Wrapf(err, "Did you forget to run sg tail first?")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
dev/sg/tail/BUILD.bazel
Normal file
37
dev/sg/tail/BUILD.bazel
Normal file
@ -0,0 +1,37 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
load("//dev:go_defs.bzl", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "tail",
|
||||
srcs = [
|
||||
"activity.go",
|
||||
"commands.go",
|
||||
"help.go",
|
||||
"model.go",
|
||||
"socket.go",
|
||||
"styles.go",
|
||||
"tail.go",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/dev/sg/tail",
|
||||
tags = [TAG_INFRA_DEVINFRA],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//dev/sg/internal/category",
|
||||
"//lib/errors",
|
||||
"@com_github_charmbracelet_bubbles//help",
|
||||
"@com_github_charmbracelet_bubbles//key",
|
||||
"@com_github_charmbracelet_bubbles//textinput",
|
||||
"@com_github_charmbracelet_bubbles//viewport",
|
||||
"@com_github_charmbracelet_bubbletea//:bubbletea",
|
||||
"@com_github_charmbracelet_lipgloss//:lipgloss",
|
||||
"@com_github_grafana_regexp//:regexp",
|
||||
"@com_github_urfave_cli_v2//:cli",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "tail_test",
|
||||
srcs = ["activity_test.go"],
|
||||
embed = [":tail"],
|
||||
tags = [TAG_INFRA_DEVINFRA],
|
||||
)
|
||||
28
dev/sg/tail/README.md
Normal file
28
dev/sg/tail/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# sg tail
|
||||
|
||||
A small utility that connects to a running `sg start --tail` and provides a better UI to read logs.
|
||||
|
||||
## Usage
|
||||
|
||||
In your usual terminal session:
|
||||
|
||||
```
|
||||
sg tail
|
||||
```
|
||||
|
||||
In another terminal session:
|
||||
|
||||
```
|
||||
cd sourcegraph
|
||||
sg start --tail
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
Flags:
|
||||
|
||||
- `--only-name [name]`: starts `sg tail` with a new tab focused, that only displays logs from service whose name starts with `[name]`.
|
||||
|
||||
### Keybindings
|
||||
|
||||
Press `h` or `?` when `sg tail` is running to see the inline help.
|
||||
87
dev/sg/tail/activity.go
Normal file
87
dev/sg/tail/activity.go
Normal file
@ -0,0 +1,87 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/grafana/regexp"
|
||||
)
|
||||
|
||||
type activityMsg struct {
|
||||
name string
|
||||
ts string
|
||||
level string
|
||||
data string
|
||||
}
|
||||
|
||||
func (a *activityMsg) render(width int, search string) string {
|
||||
name := lipgloss.NewStyle().Width(20).Align(lipgloss.Right).Foreground(nameToColor(a.name)).Render(a.name)
|
||||
level := lipgloss.NewStyle().Width(6).Align(lipgloss.Center).Background(levelToColor(a.level)).Foreground(lipgloss.Color("0")).Render(a.level)
|
||||
wrapped := lipgloss.NewStyle().Width(width - 20 - 6).Render(a.data)
|
||||
if search != "" && strings.Contains(wrapped, search) {
|
||||
wrapped = lipgloss.NewStyle().Background(lipgloss.Color("3")).Render(wrapped)
|
||||
}
|
||||
return name + " " + level + " " + wrapped
|
||||
}
|
||||
|
||||
var activityRe = regexp.MustCompile(`^(?P<name>[\w-]+):\s+(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)?\s*(?P<level>\w{4})\s+(?P<data>.*)`)
|
||||
var tsRe = regexp.MustCompile(`(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)|(?:[\d\/]+ [\d:]+)`)
|
||||
var levelAndContentRe = regexp.MustCompile(`\s*(\w{4} )?\s*(.*)`) // space after \w{4} is here to disambiguate.
|
||||
|
||||
func parseActivity(s string) activityMsg {
|
||||
var name, ts, level, data string
|
||||
parts := strings.SplitAfterN(s, ":", 2)
|
||||
name = strings.TrimSuffix(parts[0], ":")
|
||||
rest := strings.TrimSpace(parts[1])
|
||||
|
||||
for _, c := range rest {
|
||||
if unicode.IsSpace(c) {
|
||||
continue
|
||||
}
|
||||
if unicode.IsDigit(c) {
|
||||
// Ignore the TS for now
|
||||
rest = tsRe.ReplaceAllString(rest, "")
|
||||
}
|
||||
break
|
||||
}
|
||||
matches := levelAndContentRe.FindStringSubmatch(rest)
|
||||
if len(matches) == 2 {
|
||||
// We got the content, but not the level
|
||||
data = matches[1]
|
||||
} else if len(matches) == 3 {
|
||||
level = matches[1]
|
||||
data = matches[2]
|
||||
} else {
|
||||
data = rest
|
||||
}
|
||||
|
||||
return activityMsg{
|
||||
name: name,
|
||||
level: strings.ToUpper(strings.TrimSpace(level)),
|
||||
ts: ts,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
type activityPred func(a *activityMsg) *activityMsg
|
||||
|
||||
type tab struct {
|
||||
title string
|
||||
preds activityPreds
|
||||
}
|
||||
|
||||
type activityPreds []activityPred
|
||||
|
||||
func (p activityPreds) Apply(a *activityMsg) *activityMsg {
|
||||
if p == nil {
|
||||
return a
|
||||
}
|
||||
for _, pred := range p {
|
||||
a = pred(a)
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
49
dev/sg/tail/activity_test.go
Normal file
49
dev/sg/tail/activity_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestActivityParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
raw string
|
||||
wantName string
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
}{
|
||||
{
|
||||
raw: `otel-collector: 2023-11-19T09:50:38.408Z info service/telemetry.go:104 Serving Prometheus metrics {"address": ":8888", "level": "Basic"}`,
|
||||
wantName: "otel-collector",
|
||||
wantLevel: "INFO",
|
||||
wantMessage: `service/telemetry.go:104 Serving Prometheus metrics {"address": ":8888", "level": "Basic"}`,
|
||||
},
|
||||
{
|
||||
raw: `searcher: 2023/11/19 15:36:46 tmpfriend: Removing /tmp/.searcher.tmp/tmpfriend-87880-3133534317`,
|
||||
wantName: `searcher`,
|
||||
wantLevel: ``,
|
||||
wantMessage: `tmpfriend: Removing /tmp/.searcher.tmp/tmpfriend-87880-3133534317`,
|
||||
},
|
||||
{
|
||||
raw: `telemetry-gateway: INFO telemetry-gateway.tracing shared/tracing.go:48 initializing OTLP exporter`,
|
||||
wantName: "telemetry-gateway",
|
||||
wantLevel: "INFO",
|
||||
wantMessage: "telemetry-gateway.tracing shared/tracing.go:48 initializing OTLP exporter",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
t.Run(fmt.Sprintf("raw_%d", i), func(t *testing.T) {
|
||||
a := parseActivity(tt.raw)
|
||||
if a.name != tt.wantName {
|
||||
t.Errorf("got name %q, want %q", a.name, tt.wantName)
|
||||
}
|
||||
if a.level != tt.wantLevel {
|
||||
t.Errorf("got level %q, want %q", a.level, tt.wantLevel)
|
||||
}
|
||||
if a.data != tt.wantMessage {
|
||||
t.Errorf("got data %q, want %q", a.data, tt.wantMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
56
dev/sg/tail/commands.go
Normal file
56
dev/sg/tail/commands.go
Normal file
@ -0,0 +1,56 @@
|
||||
package tail
|
||||
|
||||
import "strings"
|
||||
|
||||
type commandMsg struct {
|
||||
name string
|
||||
args []string
|
||||
}
|
||||
|
||||
func (c *commandMsg) toPred() activityPred {
|
||||
switch c.name {
|
||||
case "drop":
|
||||
return func(a *activityMsg) *activityMsg {
|
||||
switch subject := c.args[0]; subject {
|
||||
case "name":
|
||||
if strings.HasPrefix(a.name, c.args[1]) {
|
||||
return nil
|
||||
}
|
||||
case "level":
|
||||
if strings.EqualFold(a.level, c.args[1]) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
case "only":
|
||||
return func(a *activityMsg) *activityMsg {
|
||||
switch subject := c.args[0]; subject {
|
||||
case "name":
|
||||
if !strings.HasPrefix(a.name, c.args[1]) {
|
||||
return nil
|
||||
}
|
||||
case "level":
|
||||
if strings.EqualFold(a.level, c.args[1]) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
case "grep":
|
||||
return func(a *activityMsg) *activityMsg {
|
||||
var invert bool
|
||||
q := c.args[0]
|
||||
if c.args[0] == "-v" {
|
||||
invert = true
|
||||
q = c.args[1]
|
||||
}
|
||||
|
||||
if strings.Contains(a.data, q) != !invert {
|
||||
return nil
|
||||
}
|
||||
return a
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
46
dev/sg/tail/help.go
Normal file
46
dev/sg/tail/help.go
Normal file
@ -0,0 +1,46 @@
|
||||
package tail
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
type keyMap struct {
|
||||
CtrlC key.Binding
|
||||
Esc key.Binding
|
||||
Prompt key.Binding
|
||||
PromptSend key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
Pause key.Binding
|
||||
ScrollUp key.Binding
|
||||
ScrollDown key.Binding
|
||||
Search key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
CtrlC: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit")),
|
||||
Quit: key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit")),
|
||||
Esc: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "clear search or exit prompt")),
|
||||
Help: key.NewBinding(key.WithKeys("?", "h"), key.WithHelp("?/h", "toggle help")),
|
||||
|
||||
Prompt: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "show command prompt (available commands: drop, only, grep, reset, tabnew tabclose)")),
|
||||
PromptSend: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "if prompt is active, execute command prompt, otherwise resume follow")),
|
||||
Search: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "search prompt")),
|
||||
|
||||
Pause: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "toggle pause/following mode")),
|
||||
ScrollUp: key.NewBinding(key.WithKeys("up"), key.WithHelp("↑", "scroll up")),
|
||||
ScrollDown: key.NewBinding(key.WithKeys("down"), key.WithHelp("↓", "scroll down")),
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view. It's part
|
||||
// of the key.Map interface.
|
||||
func (k keyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view. It's part of the
|
||||
// key.Map interface.
|
||||
func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Pause, k.ScrollUp, k.ScrollDown, k.Esc, k.Search, k.PromptSend, k.Prompt}, // first column
|
||||
{k.Help, k.Quit, k.CtrlC}, // second column
|
||||
}
|
||||
}
|
||||
363
dev/sg/tail/model.go
Normal file
363
dev/sg/tail/model.go
Normal file
@ -0,0 +1,363 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
var backlogSize = 100 * 1024
|
||||
|
||||
type model struct {
|
||||
// l is the unix socket we're listening on for incoming activities.
|
||||
l net.Listener
|
||||
// content is the rendered lines for the pager.
|
||||
content []string
|
||||
// activities are a collection of received activity messages. They get truncated
|
||||
// once they go over backlogSize.
|
||||
activities []*activityMsg
|
||||
// ch is the channel from which we're receiving activity messages.
|
||||
ch chan string
|
||||
// ready is set to true once we've received the window size.
|
||||
ready bool
|
||||
// pause stores the following or paused state of the viewport.
|
||||
pause bool
|
||||
// showHelp is set to true when the help view should be shown.
|
||||
showHelp bool
|
||||
// tabs are a list of predicates to apply to the pager's content, allowing
|
||||
// to filter activities.
|
||||
tabs []*tab
|
||||
// tabIndex stores the current tab index.
|
||||
tabIndex int
|
||||
// visiblePrompt is set to true when the prompt is visible.
|
||||
visiblePrompt bool
|
||||
// search stores the search query used to highlight activities.
|
||||
search string
|
||||
|
||||
// help model, holding the various keybindings for inline help.
|
||||
help help.Model
|
||||
// viewport is the model implementing the pager.
|
||||
viewport viewport.Model
|
||||
// promptInput is the model implementing the prompt (: or /)
|
||||
promptInput textinput.Model
|
||||
// statusMsg holds the error if any, after inputting a command
|
||||
statusMsg string
|
||||
}
|
||||
|
||||
// refreshContent goes through all activities and applies predicates to filter out
|
||||
// unwanted activities, before rendering them into a slice of strings.
|
||||
func (m *model) refreshContent() {
|
||||
t := m.tabs[m.tabIndex]
|
||||
m.content = []string{}
|
||||
for _, a := range m.activities {
|
||||
if t.preds.Apply(a) != nil {
|
||||
m.content = append(m.content, a.render(m.viewport.Width, m.search))
|
||||
}
|
||||
}
|
||||
m.viewport.SetContent(strings.Join(m.content, "\n"))
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.promptInput, cmd = m.promptInput.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case commandMsg:
|
||||
switch msg.name {
|
||||
case "drop", "only", "grep":
|
||||
t := m.tabs[m.tabIndex]
|
||||
t.preds = append(t.preds, msg.toPred())
|
||||
m.refreshContent()
|
||||
case "reset":
|
||||
t := m.tabs[m.tabIndex]
|
||||
t.preds = activityPreds{}
|
||||
m.refreshContent()
|
||||
case "tabnew":
|
||||
m.tabs = append(m.tabs, &tab{title: fmt.Sprintf("%d", len(m.tabs))})
|
||||
m.tabIndex = len(m.tabs) - 1
|
||||
m.refreshContent()
|
||||
case "tabclose":
|
||||
if m.tabIndex == 0 {
|
||||
// TODO print something
|
||||
break
|
||||
}
|
||||
old := m.tabs
|
||||
m.tabs = make([]*tab, 0, len(old))
|
||||
for i, t := range old {
|
||||
if i != m.tabIndex {
|
||||
m.tabs = append(m.tabs, t)
|
||||
}
|
||||
}
|
||||
m.tabIndex = len(m.tabs) - 1
|
||||
m.refreshContent()
|
||||
}
|
||||
case tea.KeyMsg:
|
||||
if m.visiblePrompt {
|
||||
switch k := msg.String(); k {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.visiblePrompt = false
|
||||
m.statusMsg = ""
|
||||
m.promptInput.Blur()
|
||||
case "enter":
|
||||
value := m.promptInput.Value()
|
||||
if m.promptInput.Prompt == ":" {
|
||||
cmd, err := evalPrompt(value)
|
||||
if err != nil {
|
||||
m.statusMsg = err.Error()
|
||||
} else {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
} else {
|
||||
// It's a search
|
||||
m.search = value
|
||||
m.refreshContent()
|
||||
}
|
||||
m.promptInput.SetValue("")
|
||||
m.visiblePrompt = false
|
||||
m.promptInput.Blur()
|
||||
}
|
||||
} else {
|
||||
m.statusMsg = ""
|
||||
switch k := msg.String(); k {
|
||||
case "q":
|
||||
// User might try q to quit help, and if it quitted the entire program
|
||||
// that would be frustrating.
|
||||
if m.showHelp {
|
||||
m.showHelp = false
|
||||
} else {
|
||||
return m, tea.Quit
|
||||
}
|
||||
case "ctrl+c":
|
||||
// But if you ctrl-c, it's assumed that the intent is to really quit,
|
||||
// so here we do that regardless if the inline help is shown or not.
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.search != "" {
|
||||
m.search = ""
|
||||
m.refreshContent()
|
||||
}
|
||||
case "?":
|
||||
m.showHelp = !m.showHelp
|
||||
case "h":
|
||||
m.showHelp = !m.showHelp
|
||||
case "p":
|
||||
m.pause = !m.pause
|
||||
case "up", "down":
|
||||
// When user scrolls, we want to pause
|
||||
m.pause = true
|
||||
case "enter":
|
||||
// When user presses enter, we want to unpause and go to the bottom
|
||||
m.pause = false
|
||||
m.viewport.GotoBottom()
|
||||
case "tab":
|
||||
m.tabIndex = (m.tabIndex + 1) % len(m.tabs)
|
||||
m.refreshContent()
|
||||
case ":":
|
||||
m.visiblePrompt = true
|
||||
m.promptInput.Prompt = ":"
|
||||
m.promptInput.Focus()
|
||||
cmds = append(cmds, textinput.Blink)
|
||||
case "/":
|
||||
m.visiblePrompt = true
|
||||
m.promptInput.Focus()
|
||||
m.promptInput.Prompt = "/"
|
||||
cmds = append(cmds, textinput.Blink)
|
||||
}
|
||||
}
|
||||
case activityMsg:
|
||||
if msg.data != "" {
|
||||
// If we've hit the backlog size limit, remove the oldest activities.
|
||||
if len(m.activities) >= backlogSize {
|
||||
m.activities = m.activities[100:]
|
||||
}
|
||||
|
||||
m.activities = append(m.activities, &msg)
|
||||
m.refreshContent()
|
||||
if !m.pause {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
}
|
||||
cmds = append(cmds, waitForActivity(m.ch))
|
||||
case tea.WindowSizeMsg:
|
||||
m.help.Width = msg.Width
|
||||
m.help.ShowAll = true
|
||||
|
||||
headerHeight := lipgloss.Height(m.headerView())
|
||||
footerHeight := lipgloss.Height(m.footerView())
|
||||
statusHeight := lipgloss.Height(m.promptView())
|
||||
|
||||
verticalMarginHeight := headerHeight + footerHeight + statusHeight
|
||||
|
||||
if !m.ready {
|
||||
// Since this program is using the full size of the viewport we
|
||||
// need to wait until we've received the window dimensions before
|
||||
// we can initialize the viewport. The initial dimensions come in
|
||||
// quickly, though asynchronously, which is why we wait for them
|
||||
// here.
|
||||
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
|
||||
m.viewport.YPosition = headerHeight
|
||||
m.ready = true
|
||||
|
||||
// This is only necessary for high performance rendering, which in
|
||||
// most cases you won't need.
|
||||
//
|
||||
// Render the viewport one line below the header.
|
||||
m.viewport.YPosition = headerHeight + 1
|
||||
} else {
|
||||
m.viewport.Width = msg.Width
|
||||
m.viewport.Height = msg.Height - verticalMarginHeight
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
showUsage(),
|
||||
acceptFromListener(m.l, m.ch),
|
||||
waitForActivity(m.ch),
|
||||
)
|
||||
}
|
||||
|
||||
func showUsage() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return activityMsg{
|
||||
name: "README",
|
||||
ts: "",
|
||||
level: "HELP",
|
||||
data: "👉 You can now run `sg start --tail (...)` to see log messages displayed here. Press h for inline help.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if !m.ready {
|
||||
return "\n Initializing..."
|
||||
}
|
||||
|
||||
helpView := m.help.View(keys)
|
||||
|
||||
var promptView string
|
||||
if m.statusMsg != "" {
|
||||
promptView = lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Render(m.statusMsg)
|
||||
} else {
|
||||
promptView = m.promptView()
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
return helpView
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s\n%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView(), promptView)
|
||||
}
|
||||
|
||||
func (m model) headerView() string {
|
||||
var tabsStr string
|
||||
for i, t := range m.tabs {
|
||||
var s string
|
||||
if i == m.tabIndex {
|
||||
s = activeTabStyle.Render(t.title)
|
||||
} else {
|
||||
s = inactiveTabStyle.Render(t.title)
|
||||
}
|
||||
tabsStr = lipgloss.JoinHorizontal(lipgloss.Left, tabsStr, s)
|
||||
}
|
||||
title := titleStyle.Render("sg")
|
||||
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)-lipgloss.Width(tabsStr)))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Center, title, tabsStr, line)
|
||||
}
|
||||
|
||||
func (m model) footerView() string {
|
||||
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
|
||||
status := titleStyle.Render("FOLLOW")
|
||||
if m.pause {
|
||||
status = titleStyle.Render("PAUSED")
|
||||
}
|
||||
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(status)-lipgloss.Width(info)))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Center, status, line, info)
|
||||
}
|
||||
|
||||
func evalPrompt(value string) (tea.Cmd, error) {
|
||||
parts := strings.Split(value, " ")
|
||||
switch cmd := parts[0]; cmd {
|
||||
case "drop":
|
||||
if len(parts[1:]) < 2 {
|
||||
return nil, errors.Newf("drop requires at least two arguments (ex: ':drop name gitserver')")
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return commandMsg{
|
||||
name: "drop",
|
||||
args: parts[1:],
|
||||
}
|
||||
}, nil
|
||||
case "only":
|
||||
if len(parts[1:]) < 2 {
|
||||
return nil, errors.Newf("only requires at least two arguments (ex: ':only name gitserver')")
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return commandMsg{
|
||||
name: "only",
|
||||
args: parts[1:],
|
||||
}
|
||||
}, nil
|
||||
case "grep":
|
||||
if len(parts[1:]) < 1 {
|
||||
return nil, errors.Newf("grep requires at least one arguments")
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return commandMsg{
|
||||
name: "grep",
|
||||
args: parts[1:],
|
||||
}
|
||||
}, nil
|
||||
case "reset":
|
||||
return func() tea.Msg {
|
||||
return commandMsg{
|
||||
name: "reset",
|
||||
}
|
||||
}, nil
|
||||
case "tabnew":
|
||||
return func() tea.Msg {
|
||||
return commandMsg{
|
||||
name: "tabnew",
|
||||
}
|
||||
}, nil
|
||||
case "tabclose":
|
||||
return func() tea.Msg {
|
||||
return commandMsg{
|
||||
name: "tabclose",
|
||||
}
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.Newf("unknown command: %s, press h or ? to get inline help", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) promptView() string {
|
||||
if m.visiblePrompt {
|
||||
return m.promptInput.View()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
43
dev/sg/tail/socket.go
Normal file
43
dev/sg/tail/socket.go
Normal file
@ -0,0 +1,43 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/grafana/regexp"
|
||||
)
|
||||
|
||||
func acceptFromListener(l net.Listener, ch chan string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
for {
|
||||
fd, err := l.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go reader(fd, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ansiRe = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
|
||||
|
||||
func reader(r io.Reader, ch chan string) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
str := scanner.Text()
|
||||
str = ansiRe.ReplaceAllString(str, "")
|
||||
ch <- str
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForActivity(ch chan string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
msg := <-ch
|
||||
return parseActivity(msg)
|
||||
}
|
||||
}
|
||||
54
dev/sg/tail/styles.go
Normal file
54
dev/sg/tail/styles.go
Normal file
@ -0,0 +1,54 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
titleStyle = func() lipgloss.Style {
|
||||
b := lipgloss.HiddenBorder()
|
||||
b.Right = "├"
|
||||
return lipgloss.NewStyle().BorderStyle(b)
|
||||
}()
|
||||
|
||||
infoStyle = func() lipgloss.Style {
|
||||
b := lipgloss.HiddenBorder()
|
||||
b.Left = "┤"
|
||||
return titleStyle.BorderStyle(b)
|
||||
}()
|
||||
activeTabStyle = lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Padding(0, 1, 0)
|
||||
inactiveTabStyle = lipgloss.NewStyle().Background(lipgloss.Color("0")).Foreground(lipgloss.Color("7")).Padding(0, 1, 0)
|
||||
)
|
||||
|
||||
func nameToColor(s string) lipgloss.Color {
|
||||
h := fnv.New32()
|
||||
h.Write([]byte(s))
|
||||
// We don't use 256 colors because some of those are too dark/bright and hard to read
|
||||
c := int(h.Sum32()) % 220
|
||||
if c == 0 {
|
||||
// 0 is black, so it's going to be the same color as the background.
|
||||
c = 1
|
||||
}
|
||||
return lipgloss.Color(fmt.Sprintf("%d", c))
|
||||
}
|
||||
|
||||
func levelToColor(level string) lipgloss.Color {
|
||||
switch level {
|
||||
case "INFO":
|
||||
return lipgloss.Color("7") // silver
|
||||
case "WARN":
|
||||
return lipgloss.Color("11") // yellow
|
||||
case "DBUG":
|
||||
return lipgloss.Color("8") // gray
|
||||
case "EROR":
|
||||
return lipgloss.Color("9") // red
|
||||
case "HELP":
|
||||
// special case for usage message at the beginning, this isn't a real log level.
|
||||
return lipgloss.Color("6") // green
|
||||
default:
|
||||
return lipgloss.Color("0") // black
|
||||
}
|
||||
}
|
||||
62
dev/sg/tail/tail.go
Normal file
62
dev/sg/tail/tail.go
Normal file
@ -0,0 +1,62 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/internal/category"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var Command = &cli.Command{
|
||||
Name: "tail",
|
||||
Usage: "Listens for 'sg start' log events and streams them with a nice UI",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "only-name",
|
||||
Usage: "--only-name [service_name] Starts with a new tab that display only logs from service named [service_name]",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
Category: category.Dev,
|
||||
Action: func(cctx *cli.Context) error {
|
||||
l, err := net.Listen("unix", "/tmp/sg.sock")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove("/tmp/sg.sock")
|
||||
}()
|
||||
|
||||
m := model{
|
||||
ch: make(chan string, 10),
|
||||
l: l,
|
||||
tabs: []*tab{
|
||||
{title: "all", preds: []activityPred{}},
|
||||
},
|
||||
promptInput: textinput.New(),
|
||||
help: help.New(),
|
||||
}
|
||||
|
||||
if cctx.String("only-name") != "" {
|
||||
onlyCmd := commandMsg{
|
||||
name: "only",
|
||||
args: []string{"name", cctx.String("only-name")},
|
||||
}
|
||||
m.tabs = append(m.tabs, &tab{title: "^" + cctx.String("only-name"), preds: []activityPred{onlyCmd.toPred()}})
|
||||
m.tabIndex = len(m.tabs) - 1
|
||||
}
|
||||
|
||||
p := tea.NewProgram(
|
||||
m,
|
||||
tea.WithAltScreen(),
|
||||
tea.WithMouseCellMotion(),
|
||||
)
|
||||
|
||||
_, err = p.Run()
|
||||
return err
|
||||
},
|
||||
}
|
||||
15
go.mod
15
go.mod
@ -269,6 +269,9 @@ require (
|
||||
github.com/bevzzz/nb v0.3.0
|
||||
github.com/bevzzz/nb-synth v0.0.0-20240128164931-35fdda0583a0
|
||||
github.com/bevzzz/nb/extension/extra/goldmark-jupyter v0.0.0-20240131001330-e69229bd9da4
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.6
|
||||
github.com/charmbracelet/lipgloss v0.12.1
|
||||
github.com/cohere-ai/cohere-go/v2 v2.8.2
|
||||
github.com/derision-test/go-mockgen/v2 v2.0.1
|
||||
github.com/dghubble/gologin/v2 v2.4.0
|
||||
@ -355,6 +358,7 @@ require (
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/apache/arrow/go/v14 v14.0.2 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect
|
||||
@ -365,6 +369,10 @@ require (
|
||||
github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect
|
||||
github.com/bufbuild/protocompile v0.5.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||
github.com/charmbracelet/x/input v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
@ -380,6 +388,7 @@ require (
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.7 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
@ -418,6 +427,7 @@ require (
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mazznoer/csscolorparser v0.1.3 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
@ -425,6 +435,8 @@ require (
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/natefinch/wrap v0.2.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
@ -457,6 +469,7 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.3.0 // indirect
|
||||
github.com/vbatts/tar-split v0.11.5 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
@ -652,7 +665,7 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1
|
||||
github.com/pseudomuto/protoc-gen-doc v1.5.1
|
||||
github.com/pseudomuto/protokit v0.2.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.6 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/rs/cors v1.11.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
|
||||
31
go.sum
31
go.sum
@ -800,6 +800,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4=
|
||||
@ -954,8 +956,22 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=
|
||||
github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk=
|
||||
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
||||
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
|
||||
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
|
||||
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
|
||||
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/chromedp/cdproto v0.0.0-20210526005521-9e51b9051fd0/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||
github.com/chromedp/cdproto v0.0.0-20210706234513-2bc298e8be7f/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc=
|
||||
@ -1135,6 +1151,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+
|
||||
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro=
|
||||
@ -1881,6 +1899,8 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@ -1975,6 +1995,10 @@ github.com/mroth/weightedrand/v2 v2.0.1 h1:zrEVDIaau/E4QLOKu02kpg8T8myweFlMGikIg
|
||||
github.com/mroth/weightedrand/v2 v2.0.1/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
@ -2159,8 +2183,8 @@ github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI
|
||||
github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
|
||||
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
|
||||
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
|
||||
@ -2449,6 +2473,8 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
|
||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
|
||||
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
@ -2950,6 +2976,7 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
Loading…
Reference in New Issue
Block a user