diff --git a/cli/cliui/select.go b/cli/cliui/select.go index 7d190b4bccf3c..9c918bad94488 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -1,19 +1,54 @@ package cliui import ( - "errors" "flag" - "io" + "fmt" "os" + "os/signal" + "strings" + "syscall" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/terminal" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" "golang.org/x/xerrors" "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" "github.com/coder/serpent" ) +const defaultSelectModelHeight = 7 + +type terminateMsg struct{} + +func installSignalHandler(p *tea.Program) func() { + ch := make(chan struct{}) + + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + + defer func() { + signal.Stop(sig) + close(ch) + }() + + for { + select { + case <-ch: + return + + case <-sig: + p.Send(terminateMsg{}) + } + } + }() + + return func() { + ch <- struct{}{} + } +} + type SelectOptions struct { Options []string // Default will be highlighted first if it's a valid option. @@ -75,31 +110,193 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) { return opts.Options[0], nil } - var defaultOption interface{} - if opts.Default != "" { - defaultOption = opts.Default + initialModel := selectModel{ + search: textinput.New(), + hideSearch: opts.HideSearch, + options: opts.Options, + height: opts.Size, + message: opts.Message, + } + + if initialModel.height == 0 { + initialModel.height = defaultSelectModelHeight + } + + initialModel.search.Prompt = "" + initialModel.search.Focus() + + p := tea.NewProgram( + initialModel, + tea.WithoutSignalHandler(), + tea.WithContext(inv.Context()), + tea.WithInput(inv.Stdin), + tea.WithOutput(inv.Stdout), + ) + + closeSignalHandler := installSignalHandler(p) + defer closeSignalHandler() + + m, err := p.Run() + if err != nil { + return "", err + } + + model, ok := m.(selectModel) + if !ok { + return "", xerrors.New(fmt.Sprintf("unknown model found %T (%+v)", m, m)) + } + + if model.canceled { + return "", Canceled + } + + return model.selected, nil +} + +type selectModel struct { + search textinput.Model + options []string + cursor int + height int + message string + selected string + canceled bool + hideSearch bool +} + +func (selectModel) Init() tea.Cmd { + return nil +} + +//nolint:revive // The linter complains about modifying 'm' but this is typical practice for bubbletea +func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case terminateMsg: + m.canceled = true + return m, tea.Quit + + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC: + m.canceled = true + return m, tea.Quit + + case tea.KeyEnter: + options := m.filteredOptions() + if len(options) != 0 { + m.selected = options[m.cursor] + return m, tea.Quit + } + + case tea.KeyUp: + options := m.filteredOptions() + if m.cursor > 0 { + m.cursor-- + } else { + m.cursor = len(options) - 1 + } + + case tea.KeyDown: + options := m.filteredOptions() + if m.cursor < len(options)-1 { + m.cursor++ + } else { + m.cursor = 0 + } + } + } + + if !m.hideSearch { + oldSearch := m.search.Value() + m.search, cmd = m.search.Update(msg) + + // If the search query has changed then we need to ensure + // the cursor is still pointing at a valid option. + if m.search.Value() != oldSearch { + options := m.filteredOptions() + + if m.cursor > len(options)-1 { + m.cursor = max(0, len(options)-1) + } + } + } + + return m, cmd +} + +func (m selectModel) View() string { + var s strings.Builder + + msg := pretty.Sprintf(pretty.Bold(), "? %s", m.message) + + if m.selected != "" { + selected := pretty.Sprint(DefaultStyles.Keyword, m.selected) + _, _ = s.WriteString(fmt.Sprintf("%s %s\n", msg, selected)) + + return s.String() + } + + if m.hideSearch { + _, _ = s.WriteString(fmt.Sprintf("%s [Use arrows to move]\n", msg)) + } else { + _, _ = s.WriteString(fmt.Sprintf( + "%s %s[Use arrows to move, type to filter]\n", + msg, + m.search.View(), + )) + } + + options, start := m.viewableOptions() + + for i, option := range options { + // Is this the currently selected option? + style := pretty.Wrap(" ", "") + if m.cursor == start+i { + style = pretty.Style{ + pretty.Wrap("> ", ""), + pretty.FgColor(Green), + } + } + + _, _ = s.WriteString(pretty.Sprint(style, option)) + _, _ = s.WriteString("\n") + } + + return s.String() +} + +func (m selectModel) viewableOptions() ([]string, int) { + options := m.filteredOptions() + halfHeight := m.height / 2 + bottom := 0 + top := len(options) + + switch { + case m.cursor <= halfHeight: + top = min(top, m.height) + case m.cursor < top-halfHeight: + bottom = max(0, m.cursor-halfHeight) + top = min(top, m.cursor+halfHeight+1) + default: + bottom = max(0, top-m.height) } - var value string - err := survey.AskOne(&survey.Select{ - Options: opts.Options, - Default: defaultOption, - PageSize: opts.Size, - Message: opts.Message, - }, &value, survey.WithIcons(func(is *survey.IconSet) { - is.Help.Text = "Type to search" - if opts.HideSearch { - is.Help.Text = "" + return options[bottom:top], bottom +} + +func (m selectModel) filteredOptions() []string { + options := []string{} + for _, o := range m.options { + filter := strings.ToLower(m.search.Value()) + option := strings.ToLower(o) + + if strings.Contains(option, filter) { + options = append(options, o) } - }), survey.WithStdio(fileReadWriter{ - Reader: inv.Stdin, - }, fileReadWriter{ - Writer: inv.Stdout, - }, inv.Stdout)) - if errors.Is(err, terminal.InterruptErr) { - return value, Canceled } - return value, err + return options } type MultiSelectOptions struct { @@ -114,35 +311,215 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er return opts.Defaults, nil } - prompt := &survey.MultiSelect{ - Options: opts.Options, - Default: opts.Defaults, - Message: opts.Message, + options := make([]*multiSelectOption, len(opts.Options)) + for i, option := range opts.Options { + chosen := false + for _, d := range opts.Defaults { + if option == d { + chosen = true + break + } + } + + options[i] = &multiSelectOption{ + option: option, + chosen: chosen, + } + } + + initialModel := multiSelectModel{ + search: textinput.New(), + options: options, + message: opts.Message, + } + + initialModel.search.Prompt = "" + initialModel.search.Focus() + + p := tea.NewProgram( + initialModel, + tea.WithoutSignalHandler(), + tea.WithContext(inv.Context()), + tea.WithInput(inv.Stdin), + tea.WithOutput(inv.Stdout), + ) + + closeSignalHandler := installSignalHandler(p) + defer closeSignalHandler() + + m, err := p.Run() + if err != nil { + return nil, err + } + + model, ok := m.(multiSelectModel) + if !ok { + return nil, xerrors.New(fmt.Sprintf("unknown model found %T (%+v)", m, m)) } - var values []string - err := survey.AskOne(prompt, &values, survey.WithStdio(fileReadWriter{ - Reader: inv.Stdin, - }, fileReadWriter{ - Writer: inv.Stdout, - }, inv.Stdout)) - if errors.Is(err, terminal.InterruptErr) { + if model.canceled { return nil, Canceled } - return values, err + + return model.selectedOptions(), nil +} + +type multiSelectOption struct { + option string + chosen bool +} + +type multiSelectModel struct { + search textinput.Model + options []*multiSelectOption + cursor int + message string + canceled bool + selected bool +} + +func (multiSelectModel) Init() tea.Cmd { + return nil +} + +//nolint:revive // For same reason as previous Update definition +func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case terminateMsg: + m.canceled = true + return m, tea.Quit + + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC: + m.canceled = true + return m, tea.Quit + + case tea.KeyEnter: + if len(m.options) != 0 { + m.selected = true + return m, tea.Quit + } + + case tea.KeySpace: + options := m.filteredOptions() + if len(options) != 0 { + options[m.cursor].chosen = !options[m.cursor].chosen + } + // We back out early here otherwise a space will be inserted + // into the search field. + return m, nil + + case tea.KeyUp: + options := m.filteredOptions() + if m.cursor > 0 { + m.cursor-- + } else { + m.cursor = len(options) - 1 + } + + case tea.KeyDown: + options := m.filteredOptions() + if m.cursor < len(options)-1 { + m.cursor++ + } else { + m.cursor = 0 + } + + case tea.KeyRight: + options := m.filteredOptions() + for _, option := range options { + option.chosen = true + } + + case tea.KeyLeft: + options := m.filteredOptions() + for _, option := range options { + option.chosen = false + } + } + } + + oldSearch := m.search.Value() + m.search, cmd = m.search.Update(msg) + + // If the search query has changed then we need to ensure + // the cursor is still pointing at a valid option. + if m.search.Value() != oldSearch { + options := m.filteredOptions() + if m.cursor > len(options)-1 { + m.cursor = max(0, len(options)-1) + } + } + + return m, cmd } -type fileReadWriter struct { - io.Reader - io.Writer +func (m multiSelectModel) View() string { + var s strings.Builder + + msg := pretty.Sprintf(pretty.Bold(), "? %s", m.message) + + if m.selected { + selected := pretty.Sprint(DefaultStyles.Keyword, strings.Join(m.selectedOptions(), ", ")) + _, _ = s.WriteString(fmt.Sprintf("%s %s\n", msg, selected)) + + return s.String() + } + + _, _ = s.WriteString(fmt.Sprintf( + "%s %s[Use arrows to move, space to select, to all, to none, type to filter]\n", + msg, + m.search.View(), + )) + + for i, option := range m.filteredOptions() { + cursor := " " + chosen := "[ ]" + o := option.option + + if m.cursor == i { + cursor = pretty.Sprint(pretty.FgColor(Green), "> ") + chosen = pretty.Sprint(pretty.FgColor(Green), "[ ]") + o = pretty.Sprint(pretty.FgColor(Green), o) + } + + if option.chosen { + chosen = pretty.Sprint(pretty.FgColor(Green), "[x]") + } + + _, _ = s.WriteString(fmt.Sprintf( + "%s%s %s\n", + cursor, + chosen, + o, + )) + } + + return s.String() } -func (f fileReadWriter) Fd() uintptr { - if file, ok := f.Reader.(*os.File); ok { - return file.Fd() +func (m multiSelectModel) filteredOptions() []*multiSelectOption { + options := []*multiSelectOption{} + for _, o := range m.options { + filter := strings.ToLower(m.search.Value()) + option := strings.ToLower(o.option) + + if strings.Contains(option, filter) { + options = append(options, o) + } } - if file, ok := f.Writer.(*os.File); ok { - return file.Fd() + return options +} + +func (m multiSelectModel) selectedOptions() []string { + selected := []string{} + for _, o := range m.options { + if o.chosen { + selected = append(selected, o.option) + } } - return 0 + return selected } diff --git a/flake.nix b/flake.nix index 4a034f77164b7..27c238fe68d8b 100644 --- a/flake.nix +++ b/flake.nix @@ -117,7 +117,7 @@ name = "coder-${osArch}"; # Updated with ./scripts/update-flake.sh`. # This should be updated whenever go.mod changes! - vendorHash = "sha256-cCJOftz6BF9GeS4lHRY//NnEdLLukO5E+V1CuMlvCHo="; + vendorHash = "sha256-GaqNm/eraGPfFgT/E7MTb0jcjkQ7lPS12Nj1OoXNrCQ="; proxyVendor = true; src = ./.; nativeBuildInputs = with pkgs; [ getopt openssl zstd ]; diff --git a/go.mod b/go.mod index 9266baf6158c7..b2b79c8d25880 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,6 @@ replace github.com/lib/pq => github.com/coder/pq v1.10.5-0.20240813183442-0c420c require ( cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 cloud.google.com/go/compute/metadata v0.5.0 - github.com/AlecAivazis/survey/v2 v2.3.5 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.5.0 github.com/ammario/tlru v0.4.0 @@ -202,6 +201,8 @@ require go.uber.org/mock v0.4.0 require ( github.com/cespare/xxhash v1.1.0 + github.com/charmbracelet/bubbles v0.19.0 + github.com/charmbracelet/bubbletea v1.1.0 github.com/coder/serpent v0.8.0 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.21.2 @@ -217,15 +218,21 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/DataDog/go-libddwaf/v3 v3.3.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect - github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // 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/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect @@ -279,7 +286,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect // In later at least v0.7.1, lipgloss changes its terminal detection // which breaks most of our CLI golden files tests. - github.com/charmbracelet/lipgloss v0.12.1 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect @@ -356,7 +363,6 @@ require ( github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect github.com/mdlayher/socket v0.5.0 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/miekg/dns v1.1.57 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index b15ace650b277..be83bd8f77f05 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= -github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= -github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= @@ -47,8 +45,6 @@ github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6Xge github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -93,6 +89,8 @@ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloD github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= @@ -179,14 +177,20 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/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.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= +github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= -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/exp/golden v0.0.0-20240715153702-9ba8adf781c4 h1:6KzMkQeAF56rggw2NZu1L+TH7j9+DM1/2Kmh7KUxg1I= -github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= @@ -245,7 +249,6 @@ github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDh github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= @@ -301,6 +304,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/evanw/esbuild v0.23.0 h1:PLUwTn2pzQfIBRrMKcD3M0g1ALOKIHMDefdFCk7avwM= github.com/evanw/esbuild v0.23.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -597,7 +602,6 @@ github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7H github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v6LAiP7i1bikZJu3gxpgvu3g1Lw+a0= @@ -698,6 +702,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -712,8 +718,6 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= @@ -752,6 +756,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= @@ -1133,9 +1141,9 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1155,7 +1163,6 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=