diff --git a/assets/img/MacOSAndLinux.gif b/assets/img/MacOSAndLinux.gif
index a1cbc0a..f115931 100644
Binary files a/assets/img/MacOSAndLinux.gif and b/assets/img/MacOSAndLinux.gif differ
diff --git a/assets/img/Sail.gif b/assets/img/Sail.gif
index c2a0a14..535ded6 100644
Binary files a/assets/img/Sail.gif and b/assets/img/Sail.gif differ
diff --git a/assets/img/configure-oauth.gif b/assets/img/configure-oauth.gif
index be05e69..323f49c 100644
Binary files a/assets/img/configure-oauth.gif and b/assets/img/configure-oauth.gif differ
diff --git a/assets/img/configure-pat.gif b/assets/img/configure-pat.gif
index 7e072a0..060c8d6 100644
Binary files a/assets/img/configure-pat.gif and b/assets/img/configure-pat.gif differ
diff --git a/assets/vhs/configure-oauth.tape b/assets/vhs/configure-oauth.tape
index b494528..b040884 100644
--- a/assets/vhs/configure-oauth.tape
+++ b/assets/vhs/configure-oauth.tape
@@ -4,7 +4,7 @@ Output assets/img/configure-oauth.gif
# Set up a 1200x600 terminal with 46px font.
Set FontSize 16
Set Width 1200
-Set Height 300
+Set Height 500
Hide Sleep 2s Show
@@ -14,7 +14,19 @@ Hide Sleep 2s Show
Sleep 300ms
# Type a command in the terminal.
-Type "sail configure oauth"
+Type "sail configure"
+
+# Pause for dramatic effect...
+Sleep 1s
+
+# Run the command by pressing enter.
+Enter
+
+# Pause for dramatic effect...
+Sleep 1s
+
+# Type a command in the terminal.
+Down
# Pause for dramatic effect...
Sleep 1s
diff --git a/assets/vhs/configure-pat.tape b/assets/vhs/configure-pat.tape
index 013e4b0..0bf9b22 100644
--- a/assets/vhs/configure-pat.tape
+++ b/assets/vhs/configure-pat.tape
@@ -4,7 +4,7 @@ Output assets/img/configure-pat.gif
# Set up a 1200x600 terminal with 46px font.
Set FontSize 16
Set Width 1200
-Set Height 300
+Set Height 500
Hide Sleep 2s Show
@@ -14,7 +14,7 @@ Hide Sleep 2s Show
Sleep 300ms
# Type a command in the terminal.
-Type "sail configure pat"
+Type "sail configure"
# Pause for dramatic effect...
Sleep 1s
diff --git a/cmd/root/configure.go b/cmd/root/configure.go
index f267ac0..82ba767 100644
--- a/cmd/root/configure.go
+++ b/cmd/root/configure.go
@@ -10,8 +10,11 @@ import (
"strconv"
"strings"
+ "github.com/charmbracelet/bubbles/list"
+ tea "github.com/charmbracelet/bubbletea"
"github.com/sailpoint-oss/sailpoint-cli/internal/auth"
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
+ "github.com/sailpoint-oss/sailpoint-cli/internal/tui"
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -27,6 +30,33 @@ const (
configYamlFile = "config.yaml"
)
+func PromptAuth() (string, error) {
+ items := []list.Item{
+ tui.Item("PAT"),
+ tui.Item("OAuth"),
+ }
+
+ const defaultWidth = 20
+
+ l := list.New(items, tui.ItemDelegate{}, defaultWidth, tui.ListHeight)
+ l.Title = "What authentication method do you want to use?"
+ l.SetShowStatusBar(false)
+ l.SetFilteringEnabled(false)
+ l.Styles.Title = tui.TitleStyle
+ l.Styles.PaginationStyle = tui.PaginationStyle
+ l.Styles.HelpStyle = tui.HelpStyle
+
+ m := tui.Model{List: l}
+ _, err := tea.NewProgram(m).Run()
+ if err != nil {
+ return "", err
+ }
+
+ choice := m.Retrieve()
+
+ return choice, nil
+}
+
func newConfigureCmd(client client.Client) *cobra.Command {
var debug bool
cmd := &cobra.Command{
@@ -38,9 +68,15 @@ func newConfigureCmd(client client.Client) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
var AuthType string
+ var err error
if len(args) > 0 {
AuthType = args[0]
+ } else {
+ AuthType, err = PromptAuth()
+ if err != nil {
+ return err
+ }
}
config, err := getConfigureParamsFromStdin(AuthType, debug)
diff --git a/go.mod b/go.mod
index 0f5e5a3..20e3a64 100644
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@ require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/alessio/shellescape v1.4.1 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
@@ -43,7 +44,7 @@ require (
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
- github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
+ github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.13.0 // indirect
@@ -51,6 +52,7 @@ require (
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
+ github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 82726ea..6865df2 100644
--- a/go.sum
+++ b/go.sum
@@ -44,6 +44,7 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpH
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
@@ -167,6 +168,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
@@ -191,8 +193,9 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
+github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
@@ -225,6 +228,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
diff --git a/internal/model/model.go b/internal/model/model.go
deleted file mode 100644
index 96ae869..0000000
--- a/internal/model/model.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package model
-
-import (
- "fmt"
- "os"
-
- "github.com/charmbracelet/bubbles/key"
- "github.com/charmbracelet/bubbles/spinner"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
-)
-
-type errMsg error
-
-type model struct {
- spinner spinner.Model
- quitting bool
- err error
-}
-
-var quitKeys = key.NewBinding(
- key.WithKeys("q", "esc", "ctrl+c"),
- key.WithHelp("", "press q to quit"),
-)
-
-func initialModel() model {
- s := spinner.New()
- s.Spinner = spinner.Dot
- s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
- return model{spinner: s}
-}
-
-func (m model) Init() tea.Cmd {
- return m.spinner.Tick
-}
-
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
-
- case tea.KeyMsg:
- if key.Matches(msg, quitKeys) {
- m.quitting = true
- return m, tea.Quit
-
- }
- return m, nil
- case errMsg:
- m.err = msg
- return m, nil
-
- default:
- var cmd tea.Cmd
- m.spinner, cmd = m.spinner.Update(msg)
- return m, cmd
- }
-}
-
-func (m model) View() string {
- if m.err != nil {
- return m.err.Error()
- }
- str := fmt.Sprintf("\n\n %s Loading forever... %s\n\n", m.spinner.View(), quitKeys.Help().Desc)
- if m.quitting {
- return str + "\n"
- }
- return str
-}
-
-func main() {
- p := tea.NewProgram(initialModel())
- if _, err := p.Run(); err != nil {
- fmt.Println(err)
- os.Exit(1)
- }
-}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
new file mode 100644
index 0000000..5cd22c1
--- /dev/null
+++ b/internal/tui/tui.go
@@ -0,0 +1,99 @@
+package tui
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/charmbracelet/bubbles/list"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+const ListHeight = 14
+
+var (
+ TitleStyle = lipgloss.NewStyle().MarginLeft(2)
+ itemStyle = lipgloss.NewStyle().PaddingLeft(4)
+ selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
+ PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
+ HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
+ quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
+)
+
+type Item string
+
+func (i Item) FilterValue() string { return "" }
+
+type ItemDelegate struct{}
+
+func (d ItemDelegate) Height() int { return 1 }
+func (d ItemDelegate) Spacing() int { return 0 }
+func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
+func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
+ i, ok := listItem.(Item)
+ if !ok {
+ return
+ }
+
+ str := fmt.Sprintf("%d. %s", index+1, i)
+
+ fn := itemStyle.Render
+ if index == m.Index() {
+ fn = func(s string) string {
+ return selectedItemStyle.Render("> " + s)
+ }
+ }
+
+ fmt.Fprint(w, fn(str))
+}
+
+type Model struct {
+ List list.Model
+ Quitting bool
+}
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+var choice string
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.List.SetWidth(msg.Width)
+ return m, nil
+
+ case tea.KeyMsg:
+ switch keypress := msg.String(); keypress {
+ case "ctrl+c":
+ m.Quitting = true
+ return m, tea.Quit
+
+ case "enter":
+ i, ok := m.List.SelectedItem().(Item)
+ if ok {
+ choice = string(i)
+ }
+ return m, tea.Quit
+ }
+ }
+
+ var cmd tea.Cmd
+ m.List, cmd = m.List.Update(msg)
+ return m, cmd
+}
+
+func (m Model) View() string {
+ if choice != "" {
+ return quitTextStyle.Render(fmt.Sprintf("Begining %s Configuration.", choice))
+ }
+ if m.Quitting {
+ return quitTextStyle.Render("Aborting Configuration.")
+ }
+ return "\n" + m.List.View()
+}
+
+func (m Model) Retrieve() string {
+ return choice
+}
diff --git a/vendor/github.com/atotto/clipboard/.travis.yml b/vendor/github.com/atotto/clipboard/.travis.yml
new file mode 100644
index 0000000..23f21d8
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/.travis.yml
@@ -0,0 +1,22 @@
+language: go
+
+os:
+ - linux
+ - osx
+ - windows
+
+go:
+ - go1.13.x
+ - go1.x
+
+services:
+ - xvfb
+
+before_install:
+ - export DISPLAY=:99.0
+
+script:
+ - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xsel; fi
+ - go test -v .
+ - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xclip; fi
+ - go test -v .
diff --git a/vendor/github.com/atotto/clipboard/LICENSE b/vendor/github.com/atotto/clipboard/LICENSE
new file mode 100644
index 0000000..dee3257
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Ato Araki. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of @atotto. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/atotto/clipboard/README.md b/vendor/github.com/atotto/clipboard/README.md
new file mode 100644
index 0000000..41fdd57
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/README.md
@@ -0,0 +1,48 @@
+[](https://travis-ci.org/atotto/clipboard)
+
+[](http://godoc.org/github.com/atotto/clipboard)
+
+# Clipboard for Go
+
+Provide copying and pasting to the Clipboard for Go.
+
+Build:
+
+ $ go get github.com/atotto/clipboard
+
+Platforms:
+
+* OSX
+* Windows 7 (probably work on other Windows)
+* Linux, Unix (requires 'xclip' or 'xsel' command to be installed)
+
+
+Document:
+
+* http://godoc.org/github.com/atotto/clipboard
+
+Notes:
+
+* Text string only
+* UTF-8 text encoding only (no conversion)
+
+TODO:
+
+* Clipboard watcher(?)
+
+## Commands:
+
+paste shell command:
+
+ $ go get github.com/atotto/clipboard/cmd/gopaste
+ $ # example:
+ $ gopaste > document.txt
+
+copy shell command:
+
+ $ go get github.com/atotto/clipboard/cmd/gocopy
+ $ # example:
+ $ cat document.txt | gocopy
+
+
+
diff --git a/vendor/github.com/atotto/clipboard/clipboard.go b/vendor/github.com/atotto/clipboard/clipboard.go
new file mode 100644
index 0000000..d7907d3
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard.go
@@ -0,0 +1,20 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package clipboard read/write on clipboard
+package clipboard
+
+// ReadAll read string from clipboard
+func ReadAll() (string, error) {
+ return readAll()
+}
+
+// WriteAll write string to clipboard
+func WriteAll(text string) error {
+ return writeAll(text)
+}
+
+// Unsupported might be set true during clipboard init, to help callers decide
+// whether or not to offer clipboard options.
+var Unsupported bool
diff --git a/vendor/github.com/atotto/clipboard/clipboard_darwin.go b/vendor/github.com/atotto/clipboard/clipboard_darwin.go
new file mode 100644
index 0000000..6f33078
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_darwin.go
@@ -0,0 +1,52 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package clipboard
+
+import (
+ "os/exec"
+)
+
+var (
+ pasteCmdArgs = "pbpaste"
+ copyCmdArgs = "pbcopy"
+)
+
+func getPasteCommand() *exec.Cmd {
+ return exec.Command(pasteCmdArgs)
+}
+
+func getCopyCommand() *exec.Cmd {
+ return exec.Command(copyCmdArgs)
+}
+
+func readAll() (string, error) {
+ pasteCmd := getPasteCommand()
+ out, err := pasteCmd.Output()
+ if err != nil {
+ return "", err
+ }
+ return string(out), nil
+}
+
+func writeAll(text string) error {
+ copyCmd := getCopyCommand()
+ in, err := copyCmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+
+ if err := copyCmd.Start(); err != nil {
+ return err
+ }
+ if _, err := in.Write([]byte(text)); err != nil {
+ return err
+ }
+ if err := in.Close(); err != nil {
+ return err
+ }
+ return copyCmd.Wait()
+}
diff --git a/vendor/github.com/atotto/clipboard/clipboard_plan9.go b/vendor/github.com/atotto/clipboard/clipboard_plan9.go
new file mode 100644
index 0000000..9d2fef4
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_plan9.go
@@ -0,0 +1,42 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build plan9
+
+package clipboard
+
+import (
+ "os"
+ "io/ioutil"
+)
+
+func readAll() (string, error) {
+ f, err := os.Open("/dev/snarf")
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+
+ str, err := ioutil.ReadAll(f)
+ if err != nil {
+ return "", err
+ }
+
+ return string(str), nil
+}
+
+func writeAll(text string) error {
+ f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ _, err = f.Write([]byte(text))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/atotto/clipboard/clipboard_unix.go b/vendor/github.com/atotto/clipboard/clipboard_unix.go
new file mode 100644
index 0000000..d9f6a56
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_unix.go
@@ -0,0 +1,149 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build freebsd linux netbsd openbsd solaris dragonfly
+
+package clipboard
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+)
+
+const (
+ xsel = "xsel"
+ xclip = "xclip"
+ powershellExe = "powershell.exe"
+ clipExe = "clip.exe"
+ wlcopy = "wl-copy"
+ wlpaste = "wl-paste"
+ termuxClipboardGet = "termux-clipboard-get"
+ termuxClipboardSet = "termux-clipboard-set"
+)
+
+var (
+ Primary bool
+ trimDos bool
+
+ pasteCmdArgs []string
+ copyCmdArgs []string
+
+ xselPasteArgs = []string{xsel, "--output", "--clipboard"}
+ xselCopyArgs = []string{xsel, "--input", "--clipboard"}
+
+ xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
+ xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"}
+
+ powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"}
+ clipExeCopyArgs = []string{clipExe}
+
+ wlpasteArgs = []string{wlpaste, "--no-newline"}
+ wlcopyArgs = []string{wlcopy}
+
+ termuxPasteArgs = []string{termuxClipboardGet}
+ termuxCopyArgs = []string{termuxClipboardSet}
+
+ missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.")
+)
+
+func init() {
+ if os.Getenv("WAYLAND_DISPLAY") != "" {
+ pasteCmdArgs = wlpasteArgs
+ copyCmdArgs = wlcopyArgs
+
+ if _, err := exec.LookPath(wlcopy); err == nil {
+ if _, err := exec.LookPath(wlpaste); err == nil {
+ return
+ }
+ }
+ }
+
+ pasteCmdArgs = xclipPasteArgs
+ copyCmdArgs = xclipCopyArgs
+
+ if _, err := exec.LookPath(xclip); err == nil {
+ return
+ }
+
+ pasteCmdArgs = xselPasteArgs
+ copyCmdArgs = xselCopyArgs
+
+ if _, err := exec.LookPath(xsel); err == nil {
+ return
+ }
+
+ pasteCmdArgs = termuxPasteArgs
+ copyCmdArgs = termuxCopyArgs
+
+ if _, err := exec.LookPath(termuxClipboardSet); err == nil {
+ if _, err := exec.LookPath(termuxClipboardGet); err == nil {
+ return
+ }
+ }
+
+ pasteCmdArgs = powershellExePasteArgs
+ copyCmdArgs = clipExeCopyArgs
+ trimDos = true
+
+ if _, err := exec.LookPath(clipExe); err == nil {
+ if _, err := exec.LookPath(powershellExe); err == nil {
+ return
+ }
+ }
+
+ Unsupported = true
+}
+
+func getPasteCommand() *exec.Cmd {
+ if Primary {
+ pasteCmdArgs = pasteCmdArgs[:1]
+ }
+ return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...)
+}
+
+func getCopyCommand() *exec.Cmd {
+ if Primary {
+ copyCmdArgs = copyCmdArgs[:1]
+ }
+ return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...)
+}
+
+func readAll() (string, error) {
+ if Unsupported {
+ return "", missingCommands
+ }
+ pasteCmd := getPasteCommand()
+ out, err := pasteCmd.Output()
+ if err != nil {
+ return "", err
+ }
+ result := string(out)
+ if trimDos && len(result) > 1 {
+ result = result[:len(result)-2]
+ }
+ return result, nil
+}
+
+func writeAll(text string) error {
+ if Unsupported {
+ return missingCommands
+ }
+ copyCmd := getCopyCommand()
+ in, err := copyCmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+
+ if err := copyCmd.Start(); err != nil {
+ return err
+ }
+ if _, err := in.Write([]byte(text)); err != nil {
+ return err
+ }
+ if err := in.Close(); err != nil {
+ return err
+ }
+ return copyCmd.Wait()
+}
diff --git a/vendor/github.com/atotto/clipboard/clipboard_windows.go b/vendor/github.com/atotto/clipboard/clipboard_windows.go
new file mode 100644
index 0000000..253bb93
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_windows.go
@@ -0,0 +1,157 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package clipboard
+
+import (
+ "runtime"
+ "syscall"
+ "time"
+ "unsafe"
+)
+
+const (
+ cfUnicodetext = 13
+ gmemMoveable = 0x0002
+)
+
+var (
+ user32 = syscall.MustLoadDLL("user32")
+ isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
+ openClipboard = user32.MustFindProc("OpenClipboard")
+ closeClipboard = user32.MustFindProc("CloseClipboard")
+ emptyClipboard = user32.MustFindProc("EmptyClipboard")
+ getClipboardData = user32.MustFindProc("GetClipboardData")
+ setClipboardData = user32.MustFindProc("SetClipboardData")
+
+ kernel32 = syscall.NewLazyDLL("kernel32")
+ globalAlloc = kernel32.NewProc("GlobalAlloc")
+ globalFree = kernel32.NewProc("GlobalFree")
+ globalLock = kernel32.NewProc("GlobalLock")
+ globalUnlock = kernel32.NewProc("GlobalUnlock")
+ lstrcpy = kernel32.NewProc("lstrcpyW")
+)
+
+// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
+func waitOpenClipboard() error {
+ started := time.Now()
+ limit := started.Add(time.Second)
+ var r uintptr
+ var err error
+ for time.Now().Before(limit) {
+ r, _, err = openClipboard.Call(0)
+ if r != 0 {
+ return nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+ return err
+}
+
+func readAll() (string, error) {
+ // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
+ // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+ if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
+ return "", err
+ }
+ err := waitOpenClipboard()
+ if err != nil {
+ return "", err
+ }
+
+ h, _, err := getClipboardData.Call(cfUnicodetext)
+ if h == 0 {
+ _, _, _ = closeClipboard.Call()
+ return "", err
+ }
+
+ l, _, err := globalLock.Call(h)
+ if l == 0 {
+ _, _, _ = closeClipboard.Call()
+ return "", err
+ }
+
+ text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
+
+ r, _, err := globalUnlock.Call(h)
+ if r == 0 {
+ _, _, _ = closeClipboard.Call()
+ return "", err
+ }
+
+ closed, _, err := closeClipboard.Call()
+ if closed == 0 {
+ return "", err
+ }
+ return text, nil
+}
+
+func writeAll(text string) error {
+ // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
+ // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ err := waitOpenClipboard()
+ if err != nil {
+ return err
+ }
+
+ r, _, err := emptyClipboard.Call(0)
+ if r == 0 {
+ _, _, _ = closeClipboard.Call()
+ return err
+ }
+
+ data := syscall.StringToUTF16(text)
+
+ // "If the hMem parameter identifies a memory object, the object must have
+ // been allocated using the function with the GMEM_MOVEABLE flag."
+ h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
+ if h == 0 {
+ _, _, _ = closeClipboard.Call()
+ return err
+ }
+ defer func() {
+ if h != 0 {
+ globalFree.Call(h)
+ }
+ }()
+
+ l, _, err := globalLock.Call(h)
+ if l == 0 {
+ _, _, _ = closeClipboard.Call()
+ return err
+ }
+
+ r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
+ if r == 0 {
+ _, _, _ = closeClipboard.Call()
+ return err
+ }
+
+ r, _, err = globalUnlock.Call(h)
+ if r == 0 {
+ if err.(syscall.Errno) != 0 {
+ _, _, _ = closeClipboard.Call()
+ return err
+ }
+ }
+
+ r, _, err = setClipboardData.Call(cfUnicodetext, h)
+ if r == 0 {
+ _, _, _ = closeClipboard.Call()
+ return err
+ }
+ h = 0 // suppress deferred cleanup
+ closed, _, err := closeClipboard.Call()
+ if closed == 0 {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/help/help.go b/vendor/github.com/charmbracelet/bubbles/help/help.go
new file mode 100644
index 0000000..90971ac
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/help/help.go
@@ -0,0 +1,233 @@
+package help
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// KeyMap is a map of keybindings used to generate help. Since it's an
+// interface it can be any type, though struct or a map[string][]key.Binding
+// are likely candidates.
+//
+// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be
+// rendered in the help view, so in theory generated help should self-manage.
+type KeyMap interface {
+
+ // ShortHelp returns a slice of bindings to be displayed in the short
+ // version of the help. The help bubble will render help in the order in
+ // which the help items are returned here.
+ ShortHelp() []key.Binding
+
+ // MoreHelp returns an extended group of help items, grouped by columns.
+ // The help bubble will render the help in the order in which the help
+ // items are returned here.
+ FullHelp() [][]key.Binding
+}
+
+// Styles is a set of available style definitions for the Help bubble.
+type Styles struct {
+ Ellipsis lipgloss.Style
+
+ // Styling for the short help
+ ShortKey lipgloss.Style
+ ShortDesc lipgloss.Style
+ ShortSeparator lipgloss.Style
+
+ // Styling for the full help
+ FullKey lipgloss.Style
+ FullDesc lipgloss.Style
+ FullSeparator lipgloss.Style
+}
+
+// Model contains the state of the help view.
+type Model struct {
+ Width int
+ ShowAll bool // if true, render the "full" help menu
+
+ ShortSeparator string
+ FullSeparator string
+
+ // The symbol we use in the short help when help items have been truncated
+ // due to width. Periods of ellipsis by default.
+ Ellipsis string
+
+ Styles Styles
+}
+
+// New creates a new help view with some useful defaults.
+func New() Model {
+ keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
+ Light: "#909090",
+ Dark: "#626262",
+ })
+
+ descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
+ Light: "#B2B2B2",
+ Dark: "#4A4A4A",
+ })
+
+ sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
+ Light: "#DDDADA",
+ Dark: "#3C3C3C",
+ })
+
+ return Model{
+ ShortSeparator: " • ",
+ FullSeparator: " ",
+ Ellipsis: "…",
+ Styles: Styles{
+ ShortKey: keyStyle,
+ ShortDesc: descStyle,
+ ShortSeparator: sepStyle,
+ Ellipsis: sepStyle.Copy(),
+ FullKey: keyStyle.Copy(),
+ FullDesc: descStyle.Copy(),
+ FullSeparator: sepStyle.Copy(),
+ },
+ }
+}
+
+// NewModel creates a new help view with some useful defaults.
+//
+// Deprecated. Use New instead.
+var NewModel = New
+
+// Update helps satisfy the Bubble Tea Model interface. It's a no-op.
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ return m, nil
+}
+
+// View renders the help view's current state.
+func (m Model) View(k KeyMap) string {
+ if m.ShowAll {
+ return m.FullHelpView(k.FullHelp())
+ }
+ return m.ShortHelpView(k.ShortHelp())
+}
+
+// ShortHelpView renders a single line help view from a slice of keybindings.
+// If the line is longer than the maximum width it will be gracefully
+// truncated, showing only as many help items as possible.
+func (m Model) ShortHelpView(bindings []key.Binding) string {
+ if len(bindings) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ var totalWidth int
+ var separator = m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator)
+
+ for i, kb := range bindings {
+ if !kb.Enabled() {
+ continue
+ }
+
+ var sep string
+ if totalWidth > 0 && i < len(bindings) {
+ sep = separator
+ }
+
+ str := sep +
+ m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " +
+ m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)
+
+ w := lipgloss.Width(str)
+
+ // If adding this help item would go over the available width, stop
+ // drawing.
+ if m.Width > 0 && totalWidth+w > m.Width {
+ // Although if there's room for an ellipsis, print that.
+ tail := " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)
+ tailWidth := lipgloss.Width(tail)
+
+ if totalWidth+tailWidth < m.Width {
+ b.WriteString(tail)
+ }
+
+ break
+ }
+
+ totalWidth += w
+ b.WriteString(str)
+ }
+
+ return b.String()
+}
+
+// FullHelpView renders help columns from a slice of key binding slices. Each
+// top level slice entry renders into a column.
+func (m Model) FullHelpView(groups [][]key.Binding) string {
+ if len(groups) == 0 {
+ return ""
+ }
+
+ // Linter note: at this time we don't think it's worth the additional
+ // code complexity involved in preallocating this slice.
+ //nolint:prealloc
+ var (
+ out []string
+
+ totalWidth int
+ sep = m.Styles.FullSeparator.Render(m.FullSeparator)
+ sepWidth = lipgloss.Width(sep)
+ )
+
+ // Iterate over groups to build columns
+ for i, group := range groups {
+ if group == nil || !shouldRenderColumn(group) {
+ continue
+ }
+
+ var (
+ keys []string
+ descriptions []string
+ )
+
+ // Separate keys and descriptions into different slices
+ for _, kb := range group {
+ if !kb.Enabled() {
+ continue
+ }
+ keys = append(keys, kb.Help().Key)
+ descriptions = append(descriptions, kb.Help().Desc)
+ }
+
+ col := lipgloss.JoinHorizontal(lipgloss.Top,
+ m.Styles.FullKey.Render(strings.Join(keys, "\n")),
+ m.Styles.FullKey.Render(" "),
+ m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")),
+ )
+
+ // Column
+ totalWidth += lipgloss.Width(col)
+ if totalWidth > m.Width {
+ break
+ }
+
+ out = append(out, col)
+
+ // Separator
+ if i < len(group)-1 {
+ totalWidth += sepWidth
+ if totalWidth > m.Width {
+ break
+ }
+ }
+
+ out = append(out, sep)
+ }
+
+ return lipgloss.JoinHorizontal(lipgloss.Top, out...)
+}
+
+func shouldRenderColumn(b []key.Binding) (ok bool) {
+ for _, v := range b {
+ if v.Enabled() {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/list/defaultitem.go b/vendor/github.com/charmbracelet/bubbles/list/defaultitem.go
new file mode 100644
index 0000000..74e4ee2
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/list/defaultitem.go
@@ -0,0 +1,227 @@
+package list
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/muesli/reflow/truncate"
+)
+
+// DefaultItemStyles defines styling for a default list item.
+// See DefaultItemView for when these come into play.
+type DefaultItemStyles struct {
+ // The Normal state.
+ NormalTitle lipgloss.Style
+ NormalDesc lipgloss.Style
+
+ // The selected item state.
+ SelectedTitle lipgloss.Style
+ SelectedDesc lipgloss.Style
+
+ // The dimmed state, for when the filter input is initially activated.
+ DimmedTitle lipgloss.Style
+ DimmedDesc lipgloss.Style
+
+ // Charcters matching the current filter, if any.
+ FilterMatch lipgloss.Style
+}
+
+// NewDefaultItemStyles returns style definitions for a default item. See
+// DefaultItemView for when these come into play.
+func NewDefaultItemStyles() (s DefaultItemStyles) {
+ s.NormalTitle = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}).
+ Padding(0, 0, 0, 2)
+
+ s.NormalDesc = s.NormalTitle.Copy().
+ Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"})
+
+ s.SelectedTitle = lipgloss.NewStyle().
+ Border(lipgloss.NormalBorder(), false, false, false, true).
+ BorderForeground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"}).
+ Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
+ Padding(0, 0, 0, 1)
+
+ s.SelectedDesc = s.SelectedTitle.Copy().
+ Foreground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"})
+
+ s.DimmedTitle = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
+ Padding(0, 0, 0, 2)
+
+ s.DimmedDesc = s.DimmedTitle.Copy().
+ Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"})
+
+ s.FilterMatch = lipgloss.NewStyle().Underline(true)
+
+ return s
+}
+
+// DefaultItem describes an items designed to work with DefaultDelegate.
+type DefaultItem interface {
+ Item
+ Title() string
+ Description() string
+}
+
+// DefaultDelegate is a standard delegate designed to work in lists. It's
+// styled by DefaultItemStyles, which can be customized as you like.
+//
+// The description line can be hidden by setting Description to false, which
+// renders the list as single-line-items. The spacing between items can be set
+// with the SetSpacing method.
+//
+// Setting UpdateFunc is optional. If it's set it will be called when the
+// ItemDelegate called, which is called when the list's Update function is
+// invoked.
+//
+// Settings ShortHelpFunc and FullHelpFunc is optional. They can can be set to
+// include items in the list's default short and full help menus.
+type DefaultDelegate struct {
+ ShowDescription bool
+ Styles DefaultItemStyles
+ UpdateFunc func(tea.Msg, *Model) tea.Cmd
+ ShortHelpFunc func() []key.Binding
+ FullHelpFunc func() [][]key.Binding
+ height int
+ spacing int
+}
+
+// NewDefaultDelegate creates a new delegate with default styles.
+func NewDefaultDelegate() DefaultDelegate {
+ return DefaultDelegate{
+ ShowDescription: true,
+ Styles: NewDefaultItemStyles(),
+ height: 2,
+ spacing: 1,
+ }
+}
+
+// SetHeight sets delegate's preferred height.
+func (d *DefaultDelegate) SetHeight(i int) {
+ d.height = i
+}
+
+// Height returns the delegate's preferred height.
+// This has effect only if ShowDescription is true,
+// otherwise height is always 1.
+func (d DefaultDelegate) Height() int {
+ if d.ShowDescription {
+ return d.height
+ }
+ return 1
+}
+
+// SetSpacing set the delegate's spacing.
+func (d *DefaultDelegate) SetSpacing(i int) {
+ d.spacing = i
+}
+
+// Spacing returns the delegate's spacing.
+func (d DefaultDelegate) Spacing() int {
+ return d.spacing
+}
+
+// Update checks whether the delegate's UpdateFunc is set and calls it.
+func (d DefaultDelegate) Update(msg tea.Msg, m *Model) tea.Cmd {
+ if d.UpdateFunc == nil {
+ return nil
+ }
+ return d.UpdateFunc(msg, m)
+}
+
+// Render prints an item.
+func (d DefaultDelegate) Render(w io.Writer, m Model, index int, item Item) {
+ var (
+ title, desc string
+ matchedRunes []int
+ s = &d.Styles
+ )
+
+ if i, ok := item.(DefaultItem); ok {
+ title = i.Title()
+ desc = i.Description()
+ } else {
+ return
+ }
+
+ if m.width <= 0 {
+ // short-circuit
+ return
+ }
+
+ // Prevent text from exceeding list width
+ textwidth := uint(m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight())
+ title = truncate.StringWithTail(title, textwidth, ellipsis)
+ if d.ShowDescription {
+ var lines []string
+ for i, line := range strings.Split(desc, "\n") {
+ if i >= d.height-1 {
+ break
+ }
+ lines = append(lines, truncate.StringWithTail(line, textwidth, ellipsis))
+ }
+ desc = strings.Join(lines, "\n")
+ }
+
+ // Conditions
+ var (
+ isSelected = index == m.Index()
+ emptyFilter = m.FilterState() == Filtering && m.FilterValue() == ""
+ isFiltered = m.FilterState() == Filtering || m.FilterState() == FilterApplied
+ )
+
+ if isFiltered && index < len(m.filteredItems) {
+ // Get indices of matched characters
+ matchedRunes = m.MatchesForItem(index)
+ }
+
+ if emptyFilter {
+ title = s.DimmedTitle.Render(title)
+ desc = s.DimmedDesc.Render(desc)
+ } else if isSelected && m.FilterState() != Filtering {
+ if isFiltered {
+ // Highlight matches
+ unmatched := s.SelectedTitle.Inline(true)
+ matched := unmatched.Copy().Inherit(s.FilterMatch)
+ title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
+ }
+ title = s.SelectedTitle.Render(title)
+ desc = s.SelectedDesc.Render(desc)
+ } else {
+ if isFiltered {
+ // Highlight matches
+ unmatched := s.NormalTitle.Inline(true)
+ matched := unmatched.Copy().Inherit(s.FilterMatch)
+ title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
+ }
+ title = s.NormalTitle.Render(title)
+ desc = s.NormalDesc.Render(desc)
+ }
+
+ if d.ShowDescription {
+ fmt.Fprintf(w, "%s\n%s", title, desc)
+ return
+ }
+ fmt.Fprintf(w, "%s", title)
+}
+
+// ShortHelp returns the delegate's short help.
+func (d DefaultDelegate) ShortHelp() []key.Binding {
+ if d.ShortHelpFunc != nil {
+ return d.ShortHelpFunc()
+ }
+ return nil
+}
+
+// FullHelp returns the delegate's full help.
+func (d DefaultDelegate) FullHelp() [][]key.Binding {
+ if d.FullHelpFunc != nil {
+ return d.FullHelpFunc()
+ }
+ return nil
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/list/keys.go b/vendor/github.com/charmbracelet/bubbles/list/keys.go
new file mode 100644
index 0000000..421a247
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/list/keys.go
@@ -0,0 +1,97 @@
+package list
+
+import "github.com/charmbracelet/bubbles/key"
+
+// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
+// is used to render the menu menu.
+type KeyMap struct {
+ // Keybindings used when browsing the list.
+ CursorUp key.Binding
+ CursorDown key.Binding
+ NextPage key.Binding
+ PrevPage key.Binding
+ GoToStart key.Binding
+ GoToEnd key.Binding
+ Filter key.Binding
+ ClearFilter key.Binding
+
+ // Keybindings used when setting a filter.
+ CancelWhileFiltering key.Binding
+ AcceptWhileFiltering key.Binding
+
+ // Help toggle keybindings.
+ ShowFullHelp key.Binding
+ CloseFullHelp key.Binding
+
+ // The quit keybinding. This won't be caught when filtering.
+ Quit key.Binding
+
+ // The quit-no-matter-what keybinding. This will be caught when filtering.
+ ForceQuit key.Binding
+}
+
+// DefaultKeyMap returns a default set of keybindings.
+func DefaultKeyMap() KeyMap {
+ return KeyMap{
+ // Browsing.
+ CursorUp: key.NewBinding(
+ key.WithKeys("up", "k"),
+ key.WithHelp("↑/k", "up"),
+ ),
+ CursorDown: key.NewBinding(
+ key.WithKeys("down", "j"),
+ key.WithHelp("↓/j", "down"),
+ ),
+ PrevPage: key.NewBinding(
+ key.WithKeys("left", "h", "pgup", "b", "u"),
+ key.WithHelp("←/h/pgup", "prev page"),
+ ),
+ NextPage: key.NewBinding(
+ key.WithKeys("right", "l", "pgdown", "f", "d"),
+ key.WithHelp("→/l/pgdn", "next page"),
+ ),
+ GoToStart: key.NewBinding(
+ key.WithKeys("home", "g"),
+ key.WithHelp("g/home", "go to start"),
+ ),
+ GoToEnd: key.NewBinding(
+ key.WithKeys("end", "G"),
+ key.WithHelp("G/end", "go to end"),
+ ),
+ Filter: key.NewBinding(
+ key.WithKeys("/"),
+ key.WithHelp("/", "filter"),
+ ),
+ ClearFilter: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "clear filter"),
+ ),
+
+ // Filtering.
+ CancelWhileFiltering: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
+ ),
+ AcceptWhileFiltering: key.NewBinding(
+ key.WithKeys("enter", "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down"),
+ key.WithHelp("enter", "apply filter"),
+ ),
+
+ // Toggle help.
+ ShowFullHelp: key.NewBinding(
+ key.WithKeys("?"),
+ key.WithHelp("?", "more"),
+ ),
+ CloseFullHelp: key.NewBinding(
+ key.WithKeys("?"),
+ key.WithHelp("?", "close help"),
+ ),
+
+ // Quitting.
+ Quit: key.NewBinding(
+ key.WithKeys("q", "esc"),
+ key.WithHelp("q", "quit"),
+ ),
+ ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")),
+ }
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/list/list.go b/vendor/github.com/charmbracelet/bubbles/list/list.go
new file mode 100644
index 0000000..bec5e6f
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/list/list.go
@@ -0,0 +1,1264 @@
+// Package list provides a feature-rich Bubble Tea component for browsing
+// a general purpose list of items. It features optional filtering, pagination,
+// help, status messages, and a spinner to indicate activity.
+package list
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/paginator"
+ "github.com/charmbracelet/bubbles/spinner"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/muesli/reflow/ansi"
+ "github.com/muesli/reflow/truncate"
+ "github.com/sahilm/fuzzy"
+)
+
+// Item is an item that appears in the list.
+type Item interface {
+ // Filter value is the value we use when filtering against this item when
+ // we're filtering the list.
+ FilterValue() string
+}
+
+// ItemDelegate encapsulates the general functionality for all list items. The
+// benefit to separating this logic from the item itself is that you can change
+// the functionality of items without changing the actual items themselves.
+//
+// Note that if the delegate also implements help.KeyMap delegate-related
+// help items will be added to the help view.
+type ItemDelegate interface {
+ // Render renders the item's view.
+ Render(w io.Writer, m Model, index int, item Item)
+
+ // Height is the height of the list item.
+ Height() int
+
+ // Spacing is the size of the horizontal gap between list items in cells.
+ Spacing() int
+
+ // Update is the update loop for items. All messages in the list's update
+ // loop will pass through here except when the user is setting a filter.
+ // Use this method to perform item-level updates appropriate to this
+ // delegate.
+ Update(msg tea.Msg, m *Model) tea.Cmd
+}
+
+type filteredItem struct {
+ item Item // item matched
+ matches []int // rune indices of matched items
+}
+
+type filteredItems []filteredItem
+
+func (f filteredItems) items() []Item {
+ agg := make([]Item, len(f))
+ for i, v := range f {
+ agg[i] = v.item
+ }
+ return agg
+}
+
+// FilterMatchesMsg contains data about items matched during filtering. The
+// message should be routed to Update for processing.
+type FilterMatchesMsg []filteredItem
+
+// FilterFunc takes a term and a list of strings to search through
+// (defined by Item#FilterValue).
+// It should return a sorted list of ranks.
+type FilterFunc func(string, []string) []Rank
+
+// Rank defines a rank for a given item.
+type Rank struct {
+ // The index of the item in the original input.
+ Index int
+ // Indices of the actual word that were matched against the filter term.
+ MatchedIndexes []int
+}
+
+// DefaultFilter uses the sahilm/fuzzy to filter through the list.
+// This is set by default.
+func DefaultFilter(term string, targets []string) []Rank {
+ var ranks = fuzzy.Find(term, targets)
+ sort.Stable(ranks)
+ result := make([]Rank, len(ranks))
+ for i, r := range ranks {
+ result[i] = Rank{
+ Index: r.Index,
+ MatchedIndexes: r.MatchedIndexes,
+ }
+ }
+ return result
+}
+
+type statusMessageTimeoutMsg struct{}
+
+// FilterState describes the current filtering state on the model.
+type FilterState int
+
+// Possible filter states.
+const (
+ Unfiltered FilterState = iota // no filter set
+ Filtering // user is actively setting a filter
+ FilterApplied // a filter is applied and user is not editing filter
+)
+
+// String returns a human-readable string of the current filter state.
+func (f FilterState) String() string {
+ return [...]string{
+ "unfiltered",
+ "filtering",
+ "filter applied",
+ }[f]
+}
+
+// Model contains the state of this component.
+type Model struct {
+ showTitle bool
+ showFilter bool
+ showStatusBar bool
+ showPagination bool
+ showHelp bool
+ filteringEnabled bool
+
+ itemNameSingular string
+ itemNamePlural string
+
+ Title string
+ Styles Styles
+
+ // Key mappings for navigating the list.
+ KeyMap KeyMap
+
+ // Filter is used to filter the list.
+ Filter FilterFunc
+
+ disableQuitKeybindings bool
+
+ // Additional key mappings for the short and full help views. This allows
+ // you to add additional key mappings to the help menu without
+ // re-implementing the help component. Of course, you can also disable the
+ // list's help component and implement a new one if you need more
+ // flexibility.
+ AdditionalShortHelpKeys func() []key.Binding
+ AdditionalFullHelpKeys func() []key.Binding
+
+ spinner spinner.Model
+ showSpinner bool
+ width int
+ height int
+ Paginator paginator.Model
+ cursor int
+ Help help.Model
+ FilterInput textinput.Model
+ filterState FilterState
+
+ // How long status messages should stay visible. By default this is
+ // 1 second.
+ StatusMessageLifetime time.Duration
+
+ statusMessage string
+ statusMessageTimer *time.Timer
+
+ // The master set of items we're working with.
+ items []Item
+
+ // Filtered items we're currently displaying. Filtering, toggles and so on
+ // will alter this slice so we can show what is relevant. For that reason,
+ // this field should be considered ephemeral.
+ filteredItems filteredItems
+
+ delegate ItemDelegate
+}
+
+// New returns a new model with sensible defaults.
+func New(items []Item, delegate ItemDelegate, width, height int) Model {
+ styles := DefaultStyles()
+
+ sp := spinner.NewModel()
+ sp.Spinner = spinner.Line
+ sp.Style = styles.Spinner
+
+ filterInput := textinput.NewModel()
+ filterInput.Prompt = "Filter: "
+ filterInput.PromptStyle = styles.FilterPrompt
+ filterInput.CursorStyle = styles.FilterCursor
+ filterInput.CharLimit = 64
+ filterInput.Focus()
+
+ p := paginator.NewModel()
+ p.Type = paginator.Dots
+ p.ActiveDot = styles.ActivePaginationDot.String()
+ p.InactiveDot = styles.InactivePaginationDot.String()
+
+ m := Model{
+ showTitle: true,
+ showFilter: true,
+ showStatusBar: true,
+ showPagination: true,
+ showHelp: true,
+ itemNameSingular: "item",
+ itemNamePlural: "items",
+ filteringEnabled: true,
+ KeyMap: DefaultKeyMap(),
+ Filter: DefaultFilter,
+ Styles: styles,
+ Title: "List",
+ FilterInput: filterInput,
+ StatusMessageLifetime: time.Second,
+
+ width: width,
+ height: height,
+ delegate: delegate,
+ items: items,
+ Paginator: p,
+ spinner: sp,
+ Help: help.NewModel(),
+ }
+
+ m.updatePagination()
+ m.updateKeybindings()
+ return m
+}
+
+// NewModel returns a new model with sensible defaults.
+//
+// Deprecated. Use New instead.
+var NewModel = New
+
+// SetFilteringEnabled enables or disables filtering. Note that this is different
+// from ShowFilter, which merely hides or shows the input view.
+func (m *Model) SetFilteringEnabled(v bool) {
+ m.filteringEnabled = v
+ if !v {
+ m.resetFiltering()
+ }
+ m.updateKeybindings()
+}
+
+// FilteringEnabled returns whether or not filtering is enabled.
+func (m Model) FilteringEnabled() bool {
+ return m.filteringEnabled
+}
+
+// SetShowTitle shows or hides the title bar.
+func (m *Model) SetShowTitle(v bool) {
+ m.showTitle = v
+ m.updatePagination()
+}
+
+// ShowTitle returns whether or not the title bar is set to be rendered.
+func (m Model) ShowTitle() bool {
+ return m.showTitle
+}
+
+// SetShowFilter shows or hides the filer bar. Note that this does not disable
+// filtering, it simply hides the built-in filter view. This allows you to
+// use the FilterInput to render the filtering UI differently without having to
+// re-implement filtering from scratch.
+//
+// To disable filtering entirely use EnableFiltering.
+func (m *Model) SetShowFilter(v bool) {
+ m.showFilter = v
+ m.updatePagination()
+}
+
+// ShowFilter returns whether or not the filter is set to be rendered. Note
+// that this is separate from FilteringEnabled, so filtering can be hidden yet
+// still invoked. This allows you to render filtering differently without
+// having to re-implement it from scratch.
+func (m Model) ShowFilter() bool {
+ return m.showFilter
+}
+
+// SetShowStatusBar shows or hides the view that displays metadata about the
+// list, such as item counts.
+func (m *Model) SetShowStatusBar(v bool) {
+ m.showStatusBar = v
+ m.updatePagination()
+}
+
+// ShowStatusBar returns whether or not the status bar is set to be rendered.
+func (m Model) ShowStatusBar() bool {
+ return m.showStatusBar
+}
+
+// SetStatusBarItemName defines a replacement for the items identifier.
+// Defaults to item/items.
+func (m *Model) SetStatusBarItemName(singular, plural string) {
+ m.itemNameSingular = singular
+ m.itemNamePlural = plural
+}
+
+// StatusBarItemName returns singular and plural status bar item names.
+func (m Model) StatusBarItemName() (string, string) {
+ return m.itemNameSingular, m.itemNamePlural
+}
+
+// SetShowPagination hides or shoes the paginator. Note that pagination will
+// still be active, it simply won't be displayed.
+func (m *Model) SetShowPagination(v bool) {
+ m.showPagination = v
+ m.updatePagination()
+}
+
+// ShowPagination returns whether the pagination is visible.
+func (m *Model) ShowPagination() bool {
+ return m.showPagination
+}
+
+// SetShowHelp shows or hides the help view.
+func (m *Model) SetShowHelp(v bool) {
+ m.showHelp = v
+ m.updatePagination()
+}
+
+// ShowHelp returns whether or not the help is set to be rendered.
+func (m Model) ShowHelp() bool {
+ return m.showHelp
+}
+
+// Items returns the items in the list.
+func (m Model) Items() []Item {
+ return m.items
+}
+
+// Set the items available in the list. This returns a command.
+func (m *Model) SetItems(i []Item) tea.Cmd {
+ var cmd tea.Cmd
+ m.items = i
+
+ if m.filterState != Unfiltered {
+ m.filteredItems = nil
+ cmd = filterItems(*m)
+ }
+
+ m.updatePagination()
+ m.updateKeybindings()
+ return cmd
+}
+
+// Select selects the given index of the list and goes to its respective page.
+func (m *Model) Select(index int) {
+ m.Paginator.Page = index / m.Paginator.PerPage
+ m.cursor = index % m.Paginator.PerPage
+}
+
+// ResetSelected resets the selected item to the first item in the first page of the list.
+func (m *Model) ResetSelected() {
+ m.Select(0)
+}
+
+// ResetFilter resets the current filtering state.
+func (m *Model) ResetFilter() {
+ m.resetFiltering()
+}
+
+// Replace an item at the given index. This returns a command.
+func (m *Model) SetItem(index int, item Item) tea.Cmd {
+ var cmd tea.Cmd
+ m.items[index] = item
+
+ if m.filterState != Unfiltered {
+ cmd = filterItems(*m)
+ }
+
+ m.updatePagination()
+ return cmd
+}
+
+// Insert an item at the given index. If index is out of the upper bound, the
+// item will be appended. This returns a command.
+func (m *Model) InsertItem(index int, item Item) tea.Cmd {
+ var cmd tea.Cmd
+ m.items = insertItemIntoSlice(m.items, item, index)
+
+ if m.filterState != Unfiltered {
+ cmd = filterItems(*m)
+ }
+
+ m.updatePagination()
+ m.updateKeybindings()
+ return cmd
+}
+
+// RemoveItem removes an item at the given index. If the index is out of bounds
+// this will be a no-op. O(n) complexity, which probably won't matter in the
+// case of a TUI.
+func (m *Model) RemoveItem(index int) {
+ m.items = removeItemFromSlice(m.items, index)
+ if m.filterState != Unfiltered {
+ m.filteredItems = removeFilterMatchFromSlice(m.filteredItems, index)
+ if len(m.filteredItems) == 0 {
+ m.resetFiltering()
+ }
+ }
+ m.updatePagination()
+}
+
+// Set the item delegate.
+func (m *Model) SetDelegate(d ItemDelegate) {
+ m.delegate = d
+ m.updatePagination()
+}
+
+// VisibleItems returns the total items available to be shown.
+func (m Model) VisibleItems() []Item {
+ if m.filterState != Unfiltered {
+ return m.filteredItems.items()
+ }
+ return m.items
+}
+
+// SelectedItems returns the current selected item in the list.
+func (m Model) SelectedItem() Item {
+ i := m.Index()
+
+ items := m.VisibleItems()
+ if i < 0 || len(items) == 0 || len(items) <= i {
+ return nil
+ }
+
+ return items[i]
+}
+
+// MatchesForItem returns rune positions matched by the current filter, if any.
+// Use this to style runes matched by the active filter.
+//
+// See DefaultItemView for a usage example.
+func (m Model) MatchesForItem(index int) []int {
+ if m.filteredItems == nil || index >= len(m.filteredItems) {
+ return nil
+ }
+ return m.filteredItems[index].matches
+}
+
+// Index returns the index of the currently selected item as it appears in the
+// entire slice of items.
+func (m Model) Index() int {
+ return m.Paginator.Page*m.Paginator.PerPage + m.cursor
+}
+
+// Cursor returns the index of the cursor on the current page.
+func (m Model) Cursor() int {
+ return m.cursor
+}
+
+// CursorUp moves the cursor up. This can also move the state to the previous
+// page.
+func (m *Model) CursorUp() {
+ m.cursor--
+
+ // If we're at the start, stop
+ if m.cursor < 0 && m.Paginator.Page == 0 {
+ m.cursor = 0
+ return
+ }
+
+ // Move the cursor as normal
+ if m.cursor >= 0 {
+ return
+ }
+
+ // Go to the previous page
+ m.Paginator.PrevPage()
+ m.cursor = m.Paginator.ItemsOnPage(len(m.VisibleItems())) - 1
+}
+
+// CursorDown moves the cursor down. This can also advance the state to the
+// next page.
+func (m *Model) CursorDown() {
+ itemsOnPage := m.Paginator.ItemsOnPage(len(m.VisibleItems()))
+
+ m.cursor++
+
+ // If we're at the end, stop
+ if m.cursor < itemsOnPage {
+ return
+ }
+
+ // Go to the next page
+ if !m.Paginator.OnLastPage() {
+ m.Paginator.NextPage()
+ m.cursor = 0
+ return
+ }
+
+ // During filtering the cursor position can exceed the number of
+ // itemsOnPage. It's more intuitive to start the cursor at the
+ // topmost position when moving it down in this scenario.
+ if m.cursor > itemsOnPage {
+ m.cursor = 0
+ return
+ }
+
+ m.cursor = itemsOnPage - 1
+}
+
+// PrevPage moves to the previous page, if available.
+func (m Model) PrevPage() {
+ m.Paginator.PrevPage()
+}
+
+// NextPage moves to the next page, if available.
+func (m Model) NextPage() {
+ m.Paginator.NextPage()
+}
+
+// FilterState returns the current filter state.
+func (m Model) FilterState() FilterState {
+ return m.filterState
+}
+
+// FilterValue returns the current value of the filter.
+func (m Model) FilterValue() string {
+ return m.FilterInput.Value()
+}
+
+// SettingFilter returns whether or not the user is currently editing the
+// filter value. It's purely a convenience method for the following:
+//
+// m.FilterState() == Filtering
+//
+// It's included here because it's a common thing to check for when
+// implementing this component.
+func (m Model) SettingFilter() bool {
+ return m.filterState == Filtering
+}
+
+// IsFiltered returns whether or not the list is currently filtered.
+// It's purely a convenience method for the following:
+//
+// m.FilterState() == FilterApplied
+//
+func (m Model) IsFiltered() bool {
+ return m.filterState == FilterApplied
+}
+
+// Width returns the current width setting.
+func (m Model) Width() int {
+ return m.width
+}
+
+// Height returns the current height setting.
+func (m Model) Height() int {
+ return m.height
+}
+
+// SetSpinner allows to set the spinner style.
+func (m *Model) SetSpinner(spinner spinner.Spinner) {
+ m.spinner.Spinner = spinner
+}
+
+// Toggle the spinner. Note that this also returns a command.
+func (m *Model) ToggleSpinner() tea.Cmd {
+ if !m.showSpinner {
+ return m.StartSpinner()
+ }
+ m.StopSpinner()
+ return nil
+}
+
+// StartSpinner starts the spinner. Note that this returns a command.
+func (m *Model) StartSpinner() tea.Cmd {
+ m.showSpinner = true
+ return spinner.Tick
+}
+
+// StopSpinner stops the spinner.
+func (m *Model) StopSpinner() {
+ m.showSpinner = false
+}
+
+// Helper for disabling the keybindings used for quitting, in case you want to
+// handle this elsewhere in your application.
+func (m *Model) DisableQuitKeybindings() {
+ m.disableQuitKeybindings = true
+ m.KeyMap.Quit.SetEnabled(false)
+ m.KeyMap.ForceQuit.SetEnabled(false)
+}
+
+// NewStatusMessage sets a new status message, which will show for a limited
+// amount of time. Note that this also returns a command.
+func (m *Model) NewStatusMessage(s string) tea.Cmd {
+ m.statusMessage = s
+ if m.statusMessageTimer != nil {
+ m.statusMessageTimer.Stop()
+ }
+
+ m.statusMessageTimer = time.NewTimer(m.StatusMessageLifetime)
+
+ // Wait for timeout
+ return func() tea.Msg {
+ <-m.statusMessageTimer.C
+ return statusMessageTimeoutMsg{}
+ }
+}
+
+// SetSize sets the width and height of this component.
+func (m *Model) SetSize(width, height int) {
+ m.setSize(width, height)
+}
+
+// SetWidth sets the width of this component.
+func (m *Model) SetWidth(v int) {
+ m.setSize(v, m.height)
+}
+
+// SetHeight sets the height of this component.
+func (m *Model) SetHeight(v int) {
+ m.setSize(m.width, v)
+}
+
+func (m *Model) setSize(width, height int) {
+ promptWidth := lipgloss.Width(m.Styles.Title.Render(m.FilterInput.Prompt))
+
+ m.width = width
+ m.height = height
+ m.Help.Width = width
+ m.FilterInput.Width = width - promptWidth - lipgloss.Width(m.spinnerView())
+ m.updatePagination()
+}
+
+func (m *Model) resetFiltering() {
+ if m.filterState == Unfiltered {
+ return
+ }
+
+ m.filterState = Unfiltered
+ m.FilterInput.Reset()
+ m.filteredItems = nil
+ m.updatePagination()
+ m.updateKeybindings()
+}
+
+func (m Model) itemsAsFilterItems() filteredItems {
+ fi := make([]filteredItem, len(m.items))
+ for i, item := range m.items {
+ fi[i] = filteredItem{
+ item: item,
+ }
+ }
+ return filteredItems(fi)
+}
+
+// Set keybindings according to the filter state.
+func (m *Model) updateKeybindings() {
+ switch m.filterState {
+ case Filtering:
+ m.KeyMap.CursorUp.SetEnabled(false)
+ m.KeyMap.CursorDown.SetEnabled(false)
+ m.KeyMap.NextPage.SetEnabled(false)
+ m.KeyMap.PrevPage.SetEnabled(false)
+ m.KeyMap.GoToStart.SetEnabled(false)
+ m.KeyMap.GoToEnd.SetEnabled(false)
+ m.KeyMap.Filter.SetEnabled(false)
+ m.KeyMap.ClearFilter.SetEnabled(false)
+ m.KeyMap.CancelWhileFiltering.SetEnabled(true)
+ m.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != "")
+ m.KeyMap.Quit.SetEnabled(false)
+ m.KeyMap.ShowFullHelp.SetEnabled(false)
+ m.KeyMap.CloseFullHelp.SetEnabled(false)
+
+ default:
+ hasItems := len(m.items) != 0
+ m.KeyMap.CursorUp.SetEnabled(hasItems)
+ m.KeyMap.CursorDown.SetEnabled(hasItems)
+
+ hasPages := m.Paginator.TotalPages > 1
+ m.KeyMap.NextPage.SetEnabled(hasPages)
+ m.KeyMap.PrevPage.SetEnabled(hasPages)
+
+ m.KeyMap.GoToStart.SetEnabled(hasItems)
+ m.KeyMap.GoToEnd.SetEnabled(hasItems)
+
+ m.KeyMap.Filter.SetEnabled(m.filteringEnabled && hasItems)
+ m.KeyMap.ClearFilter.SetEnabled(m.filterState == FilterApplied)
+ m.KeyMap.CancelWhileFiltering.SetEnabled(false)
+ m.KeyMap.AcceptWhileFiltering.SetEnabled(false)
+ m.KeyMap.Quit.SetEnabled(!m.disableQuitKeybindings)
+
+ if m.Help.ShowAll {
+ m.KeyMap.ShowFullHelp.SetEnabled(true)
+ m.KeyMap.CloseFullHelp.SetEnabled(true)
+ } else {
+ minHelp := countEnabledBindings(m.FullHelp()) > 1
+ m.KeyMap.ShowFullHelp.SetEnabled(minHelp)
+ m.KeyMap.CloseFullHelp.SetEnabled(minHelp)
+ }
+ }
+}
+
+// Update pagination according to the amount of items for the current state.
+func (m *Model) updatePagination() {
+ index := m.Index()
+ availHeight := m.height
+
+ if m.showTitle || (m.showFilter && m.filteringEnabled) {
+ availHeight -= lipgloss.Height(m.titleView())
+ }
+ if m.showStatusBar {
+ availHeight -= lipgloss.Height(m.statusView())
+ }
+ if m.showPagination {
+ availHeight -= lipgloss.Height(m.paginationView())
+ }
+ if m.showHelp {
+ availHeight -= lipgloss.Height(m.helpView())
+ }
+
+ m.Paginator.PerPage = max(1, availHeight/(m.delegate.Height()+m.delegate.Spacing()))
+
+ if pages := len(m.VisibleItems()); pages < 1 {
+ m.Paginator.SetTotalPages(1)
+ } else {
+ m.Paginator.SetTotalPages(pages)
+ }
+
+ // Restore index
+ m.Paginator.Page = index / m.Paginator.PerPage
+ m.cursor = index % m.Paginator.PerPage
+
+ // Make sure the page stays in bounds
+ if m.Paginator.Page >= m.Paginator.TotalPages-1 {
+ m.Paginator.Page = max(0, m.Paginator.TotalPages-1)
+ }
+}
+
+func (m *Model) hideStatusMessage() {
+ m.statusMessage = ""
+ if m.statusMessageTimer != nil {
+ m.statusMessageTimer.Stop()
+ }
+}
+
+// Update is the Bubble Tea update loop.
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ var cmds []tea.Cmd
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if key.Matches(msg, m.KeyMap.ForceQuit) {
+ return m, tea.Quit
+ }
+
+ case FilterMatchesMsg:
+ m.filteredItems = filteredItems(msg)
+ return m, nil
+
+ case spinner.TickMsg:
+ newSpinnerModel, cmd := m.spinner.Update(msg)
+ m.spinner = newSpinnerModel
+ if m.showSpinner {
+ cmds = append(cmds, cmd)
+ }
+
+ case statusMessageTimeoutMsg:
+ m.hideStatusMessage()
+ }
+
+ if m.filterState == Filtering {
+ cmds = append(cmds, m.handleFiltering(msg))
+ } else {
+ cmds = append(cmds, m.handleBrowsing(msg))
+ }
+
+ return m, tea.Batch(cmds...)
+}
+
+// Updates for when a user is browsing the list.
+func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
+ var cmds []tea.Cmd
+ numItems := len(m.VisibleItems())
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ // Note: we match clear filter before quit because, by default, they're
+ // both mapped to escape.
+ case key.Matches(msg, m.KeyMap.ClearFilter):
+ m.resetFiltering()
+
+ case key.Matches(msg, m.KeyMap.Quit):
+ return tea.Quit
+
+ case key.Matches(msg, m.KeyMap.CursorUp):
+ m.CursorUp()
+
+ case key.Matches(msg, m.KeyMap.CursorDown):
+ m.CursorDown()
+
+ case key.Matches(msg, m.KeyMap.PrevPage):
+ m.Paginator.PrevPage()
+
+ case key.Matches(msg, m.KeyMap.NextPage):
+ m.Paginator.NextPage()
+
+ case key.Matches(msg, m.KeyMap.GoToStart):
+ m.Paginator.Page = 0
+ m.cursor = 0
+
+ case key.Matches(msg, m.KeyMap.GoToEnd):
+ m.Paginator.Page = m.Paginator.TotalPages - 1
+ m.cursor = m.Paginator.ItemsOnPage(numItems) - 1
+
+ case key.Matches(msg, m.KeyMap.Filter):
+ m.hideStatusMessage()
+ if m.FilterInput.Value() == "" {
+ // Populate filter with all items only if the filter is empty.
+ m.filteredItems = m.itemsAsFilterItems()
+ }
+ m.Paginator.Page = 0
+ m.cursor = 0
+ m.filterState = Filtering
+ m.FilterInput.CursorEnd()
+ m.FilterInput.Focus()
+ m.updateKeybindings()
+ return textinput.Blink
+
+ case key.Matches(msg, m.KeyMap.ShowFullHelp):
+ fallthrough
+ case key.Matches(msg, m.KeyMap.CloseFullHelp):
+ m.Help.ShowAll = !m.Help.ShowAll
+ m.updatePagination()
+ }
+ }
+
+ cmd := m.delegate.Update(msg, m)
+ cmds = append(cmds, cmd)
+
+ // Keep the index in bounds when paginating
+ itemsOnPage := m.Paginator.ItemsOnPage(len(m.VisibleItems()))
+ if m.cursor > itemsOnPage-1 {
+ m.cursor = max(0, itemsOnPage-1)
+ }
+
+ return tea.Batch(cmds...)
+}
+
+// Updates for when a user is in the filter editing interface.
+func (m *Model) handleFiltering(msg tea.Msg) tea.Cmd {
+ var cmds []tea.Cmd
+
+ // Handle keys
+ if msg, ok := msg.(tea.KeyMsg); ok {
+ switch {
+ case key.Matches(msg, m.KeyMap.CancelWhileFiltering):
+ m.resetFiltering()
+ m.KeyMap.Filter.SetEnabled(true)
+ m.KeyMap.ClearFilter.SetEnabled(false)
+
+ case key.Matches(msg, m.KeyMap.AcceptWhileFiltering):
+ m.hideStatusMessage()
+
+ if len(m.items) == 0 {
+ break
+ }
+
+ h := m.VisibleItems()
+
+ // If we've filtered down to nothing, clear the filter
+ if len(h) == 0 {
+ m.resetFiltering()
+ break
+ }
+
+ m.FilterInput.Blur()
+ m.filterState = FilterApplied
+ m.updateKeybindings()
+
+ if m.FilterInput.Value() == "" {
+ m.resetFiltering()
+ }
+ }
+ }
+
+ // Update the filter text input component
+ newFilterInputModel, inputCmd := m.FilterInput.Update(msg)
+ filterChanged := m.FilterInput.Value() != newFilterInputModel.Value()
+ m.FilterInput = newFilterInputModel
+ cmds = append(cmds, inputCmd)
+
+ // If the filtering input has changed, request updated filtering
+ if filterChanged {
+ cmds = append(cmds, filterItems(*m))
+ m.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != "")
+ }
+
+ // Update pagination
+ m.updatePagination()
+
+ return tea.Batch(cmds...)
+}
+
+// ShortHelp returns bindings to show in the abbreviated help view. It's part
+// of the help.KeyMap interface.
+func (m Model) ShortHelp() []key.Binding {
+ kb := []key.Binding{
+ m.KeyMap.CursorUp,
+ m.KeyMap.CursorDown,
+ }
+
+ filtering := m.filterState == Filtering
+
+ // If the delegate implements the help.KeyMap interface add the short help
+ // items to the short help after the cursor movement keys.
+ if !filtering {
+ if b, ok := m.delegate.(help.KeyMap); ok {
+ kb = append(kb, b.ShortHelp()...)
+ }
+ }
+
+ kb = append(kb,
+ m.KeyMap.Filter,
+ m.KeyMap.ClearFilter,
+ m.KeyMap.AcceptWhileFiltering,
+ m.KeyMap.CancelWhileFiltering,
+ )
+
+ if !filtering && m.AdditionalShortHelpKeys != nil {
+ kb = append(kb, m.AdditionalShortHelpKeys()...)
+ }
+
+ return append(kb,
+ m.KeyMap.Quit,
+ m.KeyMap.ShowFullHelp,
+ )
+}
+
+// FullHelp returns bindings to show the full help view. It's part of the
+// help.KeyMap interface.
+func (m Model) FullHelp() [][]key.Binding {
+ kb := [][]key.Binding{{
+ m.KeyMap.CursorUp,
+ m.KeyMap.CursorDown,
+ m.KeyMap.NextPage,
+ m.KeyMap.PrevPage,
+ m.KeyMap.GoToStart,
+ m.KeyMap.GoToEnd,
+ }}
+
+ filtering := m.filterState == Filtering
+
+ // If the delegate implements the help.KeyMap interface add full help
+ // keybindings to a special section of the full help.
+ if !filtering {
+ if b, ok := m.delegate.(help.KeyMap); ok {
+ kb = append(kb, b.FullHelp()...)
+ }
+ }
+
+ listLevelBindings := []key.Binding{
+ m.KeyMap.Filter,
+ m.KeyMap.ClearFilter,
+ m.KeyMap.AcceptWhileFiltering,
+ m.KeyMap.CancelWhileFiltering,
+ }
+
+ if !filtering && m.AdditionalFullHelpKeys != nil {
+ listLevelBindings = append(listLevelBindings, m.AdditionalFullHelpKeys()...)
+ }
+
+ return append(kb,
+ listLevelBindings,
+ []key.Binding{
+ m.KeyMap.Quit,
+ m.KeyMap.CloseFullHelp,
+ })
+}
+
+// View renders the component.
+func (m Model) View() string {
+ var (
+ sections []string
+ availHeight = m.height
+ )
+
+ if m.showTitle || (m.showFilter && m.filteringEnabled) {
+ v := m.titleView()
+ sections = append(sections, v)
+ availHeight -= lipgloss.Height(v)
+ }
+
+ if m.showStatusBar {
+ v := m.statusView()
+ sections = append(sections, v)
+ availHeight -= lipgloss.Height(v)
+ }
+
+ var pagination string
+ if m.showPagination {
+ pagination = m.paginationView()
+ availHeight -= lipgloss.Height(pagination)
+ }
+
+ var help string
+ if m.showHelp {
+ help = m.helpView()
+ availHeight -= lipgloss.Height(help)
+ }
+
+ content := lipgloss.NewStyle().Height(availHeight).Render(m.populatedView())
+ sections = append(sections, content)
+
+ if m.showPagination {
+ sections = append(sections, pagination)
+ }
+
+ if m.showHelp {
+ sections = append(sections, help)
+ }
+
+ return lipgloss.JoinVertical(lipgloss.Left, sections...)
+}
+
+func (m Model) titleView() string {
+ var (
+ view string
+ titleBarStyle = m.Styles.TitleBar.Copy()
+
+ // We need to account for the size of the spinner, even if we don't
+ // render it, to reserve some space for it should we turn it on later.
+ spinnerView = m.spinnerView()
+ spinnerWidth = lipgloss.Width(spinnerView)
+ spinnerLeftGap = " "
+ spinnerOnLeft = titleBarStyle.GetPaddingLeft() >= spinnerWidth+lipgloss.Width(spinnerLeftGap) && m.showSpinner
+ )
+
+ // If the filter's showing, draw that. Otherwise draw the title.
+ if m.showFilter && m.filterState == Filtering {
+ view += m.FilterInput.View()
+ } else if m.showTitle {
+ if m.showSpinner && spinnerOnLeft {
+ view += spinnerView + spinnerLeftGap
+ titleBarGap := titleBarStyle.GetPaddingLeft()
+ titleBarStyle = titleBarStyle.PaddingLeft(titleBarGap - spinnerWidth - lipgloss.Width(spinnerLeftGap))
+ }
+
+ view += m.Styles.Title.Render(m.Title)
+
+ // Status message
+ if m.filterState != Filtering {
+ view += " " + m.statusMessage
+ view = truncate.StringWithTail(view, uint(m.width-spinnerWidth), ellipsis)
+ }
+ }
+
+ // Spinner
+ if m.showSpinner && !spinnerOnLeft {
+ // Place spinner on the right
+ availSpace := m.width - lipgloss.Width(m.Styles.TitleBar.Render(view))
+ if availSpace > spinnerWidth {
+ view += strings.Repeat(" ", availSpace-spinnerWidth)
+ view += spinnerView
+ }
+ }
+
+ if len(view) > 0 {
+ return titleBarStyle.Render(view)
+ }
+ return view
+}
+
+func (m Model) statusView() string {
+ var status string
+
+ totalItems := len(m.items)
+ visibleItems := len(m.VisibleItems())
+
+ var itemName string
+ if visibleItems != 1 {
+ itemName = m.itemNamePlural
+ } else {
+ itemName = m.itemNameSingular
+ }
+
+ itemsDisplay := fmt.Sprintf("%d %s", visibleItems, itemName)
+
+ if m.filterState == Filtering {
+ // Filter results
+ if visibleItems == 0 {
+ status = m.Styles.StatusEmpty.Render("Nothing matched")
+ } else {
+ status = itemsDisplay
+ }
+ } else if len(m.items) == 0 {
+ // Not filtering: no items.
+ status = m.Styles.StatusEmpty.Render("No " + m.itemNamePlural)
+ } else {
+ // Normal
+ filtered := m.FilterState() == FilterApplied
+
+ if filtered {
+ f := strings.TrimSpace(m.FilterInput.Value())
+ f = truncate.StringWithTail(f, 10, "…")
+ status += fmt.Sprintf("“%s” ", f)
+ }
+
+ status += itemsDisplay
+ }
+
+ numFiltered := totalItems - visibleItems
+ if numFiltered > 0 {
+ status += m.Styles.DividerDot.String()
+ status += m.Styles.StatusBarFilterCount.Render(fmt.Sprintf("%d filtered", numFiltered))
+ }
+
+ return m.Styles.StatusBar.Render(status)
+}
+
+func (m Model) paginationView() string {
+ if m.Paginator.TotalPages < 2 { //nolint:gomnd
+ return ""
+ }
+
+ s := m.Paginator.View()
+
+ // If the dot pagination is wider than the width of the window
+ // use the arabic paginator.
+ if ansi.PrintableRuneWidth(s) > m.width {
+ m.Paginator.Type = paginator.Arabic
+ s = m.Styles.ArabicPagination.Render(m.Paginator.View())
+ }
+
+ style := m.Styles.PaginationStyle
+ if m.delegate.Spacing() == 0 && style.GetMarginTop() == 0 {
+ style = style.Copy().MarginTop(1)
+ }
+
+ return style.Render(s)
+}
+
+func (m Model) populatedView() string {
+ items := m.VisibleItems()
+
+ var b strings.Builder
+
+ // Empty states
+ if len(items) == 0 {
+ if m.filterState == Filtering {
+ return ""
+ }
+ return m.Styles.NoItems.Render("No " + m.itemNamePlural + " found.")
+ }
+
+ if len(items) > 0 {
+ start, end := m.Paginator.GetSliceBounds(len(items))
+ docs := items[start:end]
+
+ for i, item := range docs {
+ m.delegate.Render(&b, m, i+start, item)
+ if i != len(docs)-1 {
+ fmt.Fprint(&b, strings.Repeat("\n", m.delegate.Spacing()+1))
+ }
+ }
+ }
+
+ // If there aren't enough items to fill up this page (always the last page)
+ // then we need to add some newlines to fill up the space where items would
+ // have been.
+ itemsOnPage := m.Paginator.ItemsOnPage(len(items))
+ if itemsOnPage < m.Paginator.PerPage {
+ n := (m.Paginator.PerPage - itemsOnPage) * (m.delegate.Height() + m.delegate.Spacing())
+ if len(items) == 0 {
+ n -= m.delegate.Height() - 1
+ }
+ fmt.Fprint(&b, strings.Repeat("\n", n))
+ }
+
+ return b.String()
+}
+
+func (m Model) helpView() string {
+ return m.Styles.HelpStyle.Render(m.Help.View(m))
+}
+
+func (m Model) spinnerView() string {
+ return m.spinner.View()
+}
+
+func filterItems(m Model) tea.Cmd {
+ return func() tea.Msg {
+ if m.FilterInput.Value() == "" || m.filterState == Unfiltered {
+ return FilterMatchesMsg(m.itemsAsFilterItems()) // return nothing
+ }
+
+ targets := []string{}
+ items := m.items
+
+ for _, t := range items {
+ targets = append(targets, t.FilterValue())
+ }
+
+ filterMatches := []filteredItem{}
+ for _, r := range m.Filter(m.FilterInput.Value(), targets) {
+ filterMatches = append(filterMatches, filteredItem{
+ item: items[r.Index],
+ matches: r.MatchedIndexes,
+ })
+ }
+
+ return FilterMatchesMsg(filterMatches)
+ }
+}
+
+func insertItemIntoSlice(items []Item, item Item, index int) []Item {
+ if items == nil {
+ return []Item{item}
+ }
+ if index >= len(items) {
+ return append(items, item)
+ }
+
+ index = max(0, index)
+
+ items = append(items, nil)
+ copy(items[index+1:], items[index:])
+ items[index] = item
+ return items
+}
+
+// Remove an item from a slice of items at the given index. This runs in O(n).
+func removeItemFromSlice(i []Item, index int) []Item {
+ if index >= len(i) {
+ return i // noop
+ }
+ copy(i[index:], i[index+1:])
+ i[len(i)-1] = nil
+ return i[:len(i)-1]
+}
+
+func removeFilterMatchFromSlice(i []filteredItem, index int) []filteredItem {
+ if index >= len(i) {
+ return i // noop
+ }
+ copy(i[index:], i[index+1:])
+ i[len(i)-1] = filteredItem{}
+ return i[:len(i)-1]
+}
+
+func countEnabledBindings(groups [][]key.Binding) (agg int) {
+ for _, group := range groups {
+ for _, kb := range group {
+ if kb.Enabled() {
+ agg++
+ }
+ }
+ }
+ return agg
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/list/style.go b/vendor/github.com/charmbracelet/bubbles/list/style.go
new file mode 100644
index 0000000..e4451f8
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/list/style.go
@@ -0,0 +1,99 @@
+package list
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+const (
+ bullet = "•"
+ ellipsis = "…"
+)
+
+// Styles contains style definitions for this list component. By default, these
+// values are generated by DefaultStyles.
+type Styles struct {
+ TitleBar lipgloss.Style
+ Title lipgloss.Style
+ Spinner lipgloss.Style
+ FilterPrompt lipgloss.Style
+ FilterCursor lipgloss.Style
+
+ // Default styling for matched characters in a filter. This can be
+ // overridden by delegates.
+ DefaultFilterCharacterMatch lipgloss.Style
+
+ StatusBar lipgloss.Style
+ StatusEmpty lipgloss.Style
+ StatusBarActiveFilter lipgloss.Style
+ StatusBarFilterCount lipgloss.Style
+
+ NoItems lipgloss.Style
+
+ PaginationStyle lipgloss.Style
+ HelpStyle lipgloss.Style
+
+ // Styled characters.
+ ActivePaginationDot lipgloss.Style
+ InactivePaginationDot lipgloss.Style
+ ArabicPagination lipgloss.Style
+ DividerDot lipgloss.Style
+}
+
+// DefaultStyles returns a set of default style definitions for this list
+// component.
+func DefaultStyles() (s Styles) {
+ verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
+ subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}
+
+ s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2)
+
+ s.Title = lipgloss.NewStyle().
+ Background(lipgloss.Color("62")).
+ Foreground(lipgloss.Color("230")).
+ Padding(0, 1)
+
+ s.Spinner = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"})
+
+ s.FilterPrompt = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"})
+
+ s.FilterCursor = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"})
+
+ s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true)
+
+ s.StatusBar = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
+ Padding(0, 0, 1, 2)
+
+ s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor)
+
+ s.StatusBarActiveFilter = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
+
+ s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor)
+
+ s.NoItems = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"})
+
+ s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor)
+
+ s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:gomnd
+
+ s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2)
+
+ s.ActivePaginationDot = lipgloss.NewStyle().
+ Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}).
+ SetString(bullet)
+
+ s.InactivePaginationDot = lipgloss.NewStyle().
+ Foreground(verySubduedColor).
+ SetString(bullet)
+
+ s.DividerDot = lipgloss.NewStyle().
+ Foreground(verySubduedColor).
+ SetString(" " + bullet + " ")
+
+ return s
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go
new file mode 100644
index 0000000..a84819a
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go
@@ -0,0 +1,202 @@
+// Package paginator provides a Bubble Tea package for calulating pagination
+// and rendering pagination info. Note that this package does not render actual
+// pages: it's purely for handling keystrokes related to pagination, and
+// rendering pagination status.
+package paginator
+
+import (
+ "fmt"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+// Type specifies the way we render pagination.
+type Type int
+
+// Pagination rendering options.
+const (
+ Arabic Type = iota
+ Dots
+)
+
+// Model is the Bubble Tea model for this user interface.
+type Model struct {
+ Type Type
+ Page int
+ PerPage int
+ TotalPages int
+ ActiveDot string
+ InactiveDot string
+ ArabicFormat string
+ UsePgUpPgDownKeys bool
+ UseLeftRightKeys bool
+ UseUpDownKeys bool
+ UseHLKeys bool
+ UseJKKeys bool
+}
+
+// SetTotalPages is a helper function for calculating the total number of pages
+// from a given number of items. It's use is optional since this pager can be
+// used for other things beyond navigating sets. Note that it both returns the
+// number of total pages and alters the model.
+func (m *Model) SetTotalPages(items int) int {
+ if items < 1 {
+ return m.TotalPages
+ }
+ n := items / m.PerPage
+ if items%m.PerPage > 0 {
+ n++
+ }
+ m.TotalPages = n
+ return n
+}
+
+// ItemsOnPage is a helper function for returning the numer of items on the
+// current page given the total numer of items passed as an argument.
+func (m Model) ItemsOnPage(totalItems int) int {
+ if totalItems < 1 {
+ return 0
+ }
+ start, end := m.GetSliceBounds(totalItems)
+ return end - start
+}
+
+// GetSliceBounds is a helper function for paginating slices. Pass the length
+// of the slice you're rendering and you'll receive the start and end bounds
+// corresponding the to pagination. For example:
+//
+// bunchOfStuff := []stuff{...}
+// start, end := model.GetSliceBounds(len(bunchOfStuff))
+// sliceToRender := bunchOfStuff[start:end]
+//
+func (m *Model) GetSliceBounds(length int) (start int, end int) {
+ start = m.Page * m.PerPage
+ end = min(m.Page*m.PerPage+m.PerPage, length)
+ return start, end
+}
+
+// PrevPage is a number function for navigating one page backward. It will not
+// page beyond the first page (i.e. page 0).
+func (m *Model) PrevPage() {
+ if m.Page > 0 {
+ m.Page--
+ }
+}
+
+// NextPage is a helper function for navigating one page forward. It will not
+// page beyond the last page (i.e. totalPages - 1).
+func (m *Model) NextPage() {
+ if !m.OnLastPage() {
+ m.Page++
+ }
+}
+
+// OnLastPage returns whether or not we're on the last page.
+func (m Model) OnLastPage() bool {
+ return m.Page == m.TotalPages-1
+}
+
+// New creates a new model with defaults.
+func New() Model {
+ return Model{
+ Type: Arabic,
+ Page: 0,
+ PerPage: 1,
+ TotalPages: 1,
+ ActiveDot: "•",
+ InactiveDot: "○",
+ ArabicFormat: "%d/%d",
+ UsePgUpPgDownKeys: true,
+ UseLeftRightKeys: true,
+ UseUpDownKeys: false,
+ UseHLKeys: true,
+ UseJKKeys: false,
+ }
+}
+
+// NewModel creates a new model with defaults.
+//
+// Deprecated. Use New instead.
+var NewModel = New
+
+// Update is the Tea update function which binds keystrokes to pagination.
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if m.UsePgUpPgDownKeys {
+ switch msg.String() {
+ case "pgup":
+ m.PrevPage()
+ case "pgdown":
+ m.NextPage()
+ }
+ }
+ if m.UseLeftRightKeys {
+ switch msg.String() {
+ case "left":
+ m.PrevPage()
+ case "right":
+ m.NextPage()
+ }
+ }
+ if m.UseUpDownKeys {
+ switch msg.String() {
+ case "up":
+ m.PrevPage()
+ case "down":
+ m.NextPage()
+ }
+ }
+ if m.UseHLKeys {
+ switch msg.String() {
+ case "h":
+ m.PrevPage()
+ case "l":
+ m.NextPage()
+ }
+ }
+ if m.UseJKKeys {
+ switch msg.String() {
+ case "j":
+ m.PrevPage()
+ case "k":
+ m.NextPage()
+ }
+ }
+ }
+
+ return m, nil
+}
+
+// View renders the pagination to a string.
+func (m Model) View() string {
+ switch m.Type {
+ case Dots:
+ return m.dotsView()
+ default:
+ return m.arabicView()
+ }
+}
+
+func (m Model) dotsView() string {
+ var s string
+ for i := 0; i < m.TotalPages; i++ {
+ if i == m.Page {
+ s += m.ActiveDot
+ continue
+ }
+ s += m.InactiveDot
+ }
+ return s
+}
+
+func (m Model) arabicView() string {
+ return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
diff --git a/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go
new file mode 100644
index 0000000..9692ed7
--- /dev/null
+++ b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go
@@ -0,0 +1,860 @@
+package textinput
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "time"
+ "unicode"
+
+ "github.com/atotto/clipboard"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ rw "github.com/mattn/go-runewidth"
+)
+
+const defaultBlinkSpeed = time.Millisecond * 530
+
+// Internal ID management for text inputs. Necessary for blink integrity when
+// multiple text inputs are involved.
+var (
+ lastID int
+ idMtx sync.Mutex
+)
+
+// Return the next ID we should use on the Model.
+func nextID() int {
+ idMtx.Lock()
+ defer idMtx.Unlock()
+ lastID++
+ return lastID
+}
+
+// initialBlinkMsg initializes cursor blinking.
+type initialBlinkMsg struct{}
+
+// blinkMsg signals that the cursor should blink. It contains metadata that
+// allows us to tell if the blink message is the one we're expecting.
+type blinkMsg struct {
+ id int
+ tag int
+}
+
+// blinkCanceled is sent when a blink operation is canceled.
+type blinkCanceled struct{}
+
+// Internal messages for clipboard operations.
+type pasteMsg string
+type pasteErrMsg struct{ error }
+
+// EchoMode sets the input behavior of the text input field.
+type EchoMode int
+
+const (
+ // EchoNormal displays text as is. This is the default behavior.
+ EchoNormal EchoMode = iota
+
+ // EchoPassword displays the EchoCharacter mask instead of actual
+ // characters. This is commonly used for password fields.
+ EchoPassword
+
+ // EchoNone displays nothing as characters are entered. This is commonly
+ // seen for password fields on the command line.
+ EchoNone
+
+ // EchoOnEdit.
+)
+
+// blinkCtx manages cursor blinking.
+type blinkCtx struct {
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+// CursorMode describes the behavior of the cursor.
+type CursorMode int
+
+// Available cursor modes.
+const (
+ CursorBlink CursorMode = iota
+ CursorStatic
+ CursorHide
+)
+
+// String returns a the cursor mode in a human-readable format. This method is
+// provisional and for informational purposes only.
+func (c CursorMode) String() string {
+ return [...]string{
+ "blink",
+ "static",
+ "hidden",
+ }[c]
+}
+
+// ValidateFunc is a function that returns an error if the input is invalid.
+type ValidateFunc func(string) error
+
+// Model is the Bubble Tea model for this text input element.
+type Model struct {
+ Err error
+
+ // General settings.
+ Prompt string
+ Placeholder string
+ BlinkSpeed time.Duration
+ EchoMode EchoMode
+ EchoCharacter rune
+
+ // Styles. These will be applied as inline styles.
+ //
+ // For an introduction to styling with Lip Gloss see:
+ // https://github.com/charmbracelet/lipgloss
+ PromptStyle lipgloss.Style
+ TextStyle lipgloss.Style
+ BackgroundStyle lipgloss.Style
+ PlaceholderStyle lipgloss.Style
+ CursorStyle lipgloss.Style
+
+ // CharLimit is the maximum amount of characters this input element will
+ // accept. If 0 or less, there's no limit.
+ CharLimit int
+
+ // Width is the maximum number of characters that can be displayed at once.
+ // It essentially treats the text field like a horizontally scrolling
+ // viewport. If 0 or less this setting is ignored.
+ Width int
+
+ // The ID of this Model as it relates to other textinput Models.
+ id int
+
+ // The ID of the blink message we're expecting to receive.
+ blinkTag int
+
+ // Underlying text value.
+ value []rune
+
+ // focus indicates whether user input focus should be on this input
+ // component. When false, ignore keyboard input and hide the cursor.
+ focus bool
+
+ // Cursor blink state.
+ blink bool
+
+ // Cursor position.
+ pos int
+
+ // Used to emulate a viewport when width is set and the content is
+ // overflowing.
+ offset int
+ offsetRight int
+
+ // Used to manage cursor blink
+ blinkCtx *blinkCtx
+
+ // cursorMode determines the behavior of the cursor
+ cursorMode CursorMode
+
+ // Validate is a function that checks whether or not the text within the
+ // input is valid. If it is not valid, the `Err` field will be set to the
+ // error returned by the function. If the function is not defined, all
+ // input is considered valid.
+ Validate ValidateFunc
+}
+
+// New creates a new model with default settings.
+func New() Model {
+ return Model{
+ Prompt: "> ",
+ BlinkSpeed: defaultBlinkSpeed,
+ EchoCharacter: '*',
+ CharLimit: 0,
+ PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
+
+ id: nextID(),
+ value: nil,
+ focus: false,
+ blink: true,
+ pos: 0,
+ cursorMode: CursorBlink,
+
+ blinkCtx: &blinkCtx{
+ ctx: context.Background(),
+ },
+ }
+}
+
+// NewModel creates a new model with default settings.
+//
+// Deprecated. Use New instead.
+var NewModel = New
+
+// SetValue sets the value of the text input.
+func (m *Model) SetValue(s string) {
+ if m.Validate != nil {
+ if err := m.Validate(s); err != nil {
+ m.Err = err
+ return
+ }
+ }
+
+ m.Err = nil
+
+ runes := []rune(s)
+ if m.CharLimit > 0 && len(runes) > m.CharLimit {
+ m.value = runes[:m.CharLimit]
+ } else {
+ m.value = runes
+ }
+ if m.pos == 0 || m.pos > len(m.value) {
+ m.setCursor(len(m.value))
+ }
+ m.handleOverflow()
+}
+
+// Value returns the value of the text input.
+func (m Model) Value() string {
+ return string(m.value)
+}
+
+// Cursor returns the cursor position.
+func (m Model) Cursor() int {
+ return m.pos
+}
+
+// Blink returns whether or not to draw the cursor.
+func (m Model) Blink() bool {
+ return m.blink
+}
+
+// SetCursor moves the cursor to the given position. If the position is
+// out of bounds the cursor will be moved to the start or end accordingly.
+func (m *Model) SetCursor(pos int) {
+ m.setCursor(pos)
+}
+
+// setCursor moves the cursor to the given position and returns whether or not
+// the cursor blink should be reset. If the position is out of bounds the
+// cursor will be moved to the start or end accordingly.
+func (m *Model) setCursor(pos int) bool {
+ m.pos = clamp(pos, 0, len(m.value))
+ m.handleOverflow()
+
+ // Show the cursor unless it's been explicitly hidden
+ m.blink = m.cursorMode == CursorHide
+
+ // Reset cursor blink if necessary
+ return m.cursorMode == CursorBlink
+}
+
+// CursorStart moves the cursor to the start of the input field.
+func (m *Model) CursorStart() {
+ m.cursorStart()
+}
+
+// cursorStart moves the cursor to the start of the input field and returns
+// whether or not the curosr blink should be reset.
+func (m *Model) cursorStart() bool {
+ return m.setCursor(0)
+}
+
+// CursorEnd moves the cursor to the end of the input field.
+func (m *Model) CursorEnd() {
+ m.cursorEnd()
+}
+
+// CursorMode returns the model's cursor mode. For available cursor modes, see
+// type CursorMode.
+func (m Model) CursorMode() CursorMode {
+ return m.cursorMode
+}
+
+// SetCursorMode sets the model's cursor mode. This method returns a command.
+//
+// For available cursor modes, see type CursorMode.
+func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
+ m.cursorMode = mode
+ m.blink = m.cursorMode == CursorHide || !m.focus
+ if mode == CursorBlink {
+ return Blink
+ }
+ return nil
+}
+
+// cursorEnd moves the cursor to the end of the input field and returns whether
+// the cursor should blink should reset.
+func (m *Model) cursorEnd() bool {
+ return m.setCursor(len(m.value))
+}
+
+// Focused returns the focus state on the model.
+func (m Model) Focused() bool {
+ return m.focus
+}
+
+// Focus sets the focus state on the model. When the model is in focus it can
+// receive keyboard input and the cursor will be hidden.
+func (m *Model) Focus() tea.Cmd {
+ m.focus = true
+ m.blink = m.cursorMode == CursorHide // show the cursor unless we've explicitly hidden it
+
+ if m.cursorMode == CursorBlink && m.focus {
+ return m.blinkCmd()
+ }
+ return nil
+}
+
+// Blur removes the focus state on the model. When the model is blurred it can
+// not receive keyboard input and the cursor will be hidden.
+func (m *Model) Blur() {
+ m.focus = false
+ m.blink = true
+}
+
+// Reset sets the input to its default state with no input. Returns whether
+// or not the cursor blink should reset.
+func (m *Model) Reset() bool {
+ m.value = nil
+ return m.setCursor(0)
+}
+
+// handle a clipboard paste event, if supported. Returns whether or not the
+// cursor blink should reset.
+func (m *Model) handlePaste(v string) bool {
+ paste := []rune(v)
+
+ var availSpace int
+ if m.CharLimit > 0 {
+ availSpace = m.CharLimit - len(m.value)
+ }
+
+ // If the char limit's been reached cancel
+ if m.CharLimit > 0 && availSpace <= 0 {
+ return false
+ }
+
+ // If there's not enough space to paste the whole thing cut the pasted
+ // runes down so they'll fit
+ if m.CharLimit > 0 && availSpace < len(paste) {
+ paste = paste[:len(paste)-availSpace]
+ }
+
+ // Stuff before and after the cursor
+ head := m.value[:m.pos]
+ tailSrc := m.value[m.pos:]
+ tail := make([]rune, len(tailSrc))
+ copy(tail, tailSrc)
+
+ oldPos := m.pos
+
+ // Insert pasted runes
+ for _, r := range paste {
+ head = append(head, r)
+ m.pos++
+ if m.CharLimit > 0 {
+ availSpace--
+ if availSpace <= 0 {
+ break
+ }
+ }
+ }
+
+ // Put it all back together
+ value := append(head, tail...)
+ m.SetValue(string(value))
+
+ if m.Err != nil {
+ m.pos = oldPos
+ }
+
+ // Reset blink state if necessary and run overflow checks
+ return m.setCursor(m.pos)
+}
+
+// If a max width is defined, perform some logic to treat the visible area
+// as a horizontally scrolling viewport.
+func (m *Model) handleOverflow() {
+ if m.Width <= 0 || rw.StringWidth(string(m.value)) <= m.Width {
+ m.offset = 0
+ m.offsetRight = len(m.value)
+ return
+ }
+
+ // Correct right offset if we've deleted characters
+ m.offsetRight = min(m.offsetRight, len(m.value))
+
+ if m.pos < m.offset {
+ m.offset = m.pos
+
+ w := 0
+ i := 0
+ runes := m.value[m.offset:]
+
+ for i < len(runes) && w <= m.Width {
+ w += rw.RuneWidth(runes[i])
+ if w <= m.Width+1 {
+ i++
+ }
+ }
+
+ m.offsetRight = m.offset + i
+ } else if m.pos >= m.offsetRight {
+ m.offsetRight = m.pos
+
+ w := 0
+ runes := m.value[:m.offsetRight]
+ i := len(runes) - 1
+
+ for i > 0 && w < m.Width {
+ w += rw.RuneWidth(runes[i])
+ if w <= m.Width {
+ i--
+ }
+ }
+
+ m.offset = m.offsetRight - (len(runes) - 1 - i)
+ }
+}
+
+// deleteBeforeCursor deletes all text before the cursor. Returns whether or
+// not the cursor blink should be reset.
+func (m *Model) deleteBeforeCursor() bool {
+ m.value = m.value[m.pos:]
+ m.offset = 0
+ return m.setCursor(0)
+}
+
+// deleteAfterCursor deletes all text after the cursor. Returns whether or not
+// the cursor blink should be reset. If input is masked delete everything after
+// the cursor so as not to reveal word breaks in the masked input.
+func (m *Model) deleteAfterCursor() bool {
+ m.value = m.value[:m.pos]
+ return m.setCursor(len(m.value))
+}
+
+// deleteWordLeft deletes the word left to the cursor. Returns whether or not
+// the cursor blink should be reset.
+func (m *Model) deleteWordLeft() bool {
+ if m.pos == 0 || len(m.value) == 0 {
+ return false
+ }
+
+ if m.EchoMode != EchoNormal {
+ return m.deleteBeforeCursor()
+ }
+
+ // Linter note: it's critical that we acquire the initial cursor position
+ // here prior to altering it via SetCursor() below. As such, moving this
+ // call into the corresponding if clause does not apply here.
+ oldPos := m.pos //nolint:ifshort
+
+ blink := m.setCursor(m.pos - 1)
+ for unicode.IsSpace(m.value[m.pos]) {
+ if m.pos <= 0 {
+ break
+ }
+ // ignore series of whitespace before cursor
+ blink = m.setCursor(m.pos - 1)
+ }
+
+ for m.pos > 0 {
+ if !unicode.IsSpace(m.value[m.pos]) {
+ blink = m.setCursor(m.pos - 1)
+ } else {
+ if m.pos > 0 {
+ // keep the previous space
+ blink = m.setCursor(m.pos + 1)
+ }
+ break
+ }
+ }
+
+ if oldPos > len(m.value) {
+ m.value = m.value[:m.pos]
+ } else {
+ m.value = append(m.value[:m.pos], m.value[oldPos:]...)
+ }
+
+ return blink
+}
+
+// deleteWordRight deletes the word right to the cursor. Returns whether or not
+// the cursor blink should be reset. If input is masked delete everything after
+// the cursor so as not to reveal word breaks in the masked input.
+func (m *Model) deleteWordRight() bool {
+ if m.pos >= len(m.value) || len(m.value) == 0 {
+ return false
+ }
+
+ if m.EchoMode != EchoNormal {
+ return m.deleteAfterCursor()
+ }
+
+ oldPos := m.pos
+ m.setCursor(m.pos + 1)
+ for unicode.IsSpace(m.value[m.pos]) {
+ // ignore series of whitespace after cursor
+ m.setCursor(m.pos + 1)
+
+ if m.pos >= len(m.value) {
+ break
+ }
+ }
+
+ for m.pos < len(m.value) {
+ if !unicode.IsSpace(m.value[m.pos]) {
+ m.setCursor(m.pos + 1)
+ } else {
+ break
+ }
+ }
+
+ if m.pos > len(m.value) {
+ m.value = m.value[:oldPos]
+ } else {
+ m.value = append(m.value[:oldPos], m.value[m.pos:]...)
+ }
+
+ return m.setCursor(oldPos)
+}
+
+// wordLeft moves the cursor one word to the left. Returns whether or not the
+// cursor blink should be reset. If input is masked, move input to the start
+// so as not to reveal word breaks in the masked input.
+func (m *Model) wordLeft() bool {
+ if m.pos == 0 || len(m.value) == 0 {
+ return false
+ }
+
+ if m.EchoMode != EchoNormal {
+ return m.cursorStart()
+ }
+
+ blink := false
+ i := m.pos - 1
+ for i >= 0 {
+ if unicode.IsSpace(m.value[i]) {
+ blink = m.setCursor(m.pos - 1)
+ i--
+ } else {
+ break
+ }
+ }
+
+ for i >= 0 {
+ if !unicode.IsSpace(m.value[i]) {
+ blink = m.setCursor(m.pos - 1)
+ i--
+ } else {
+ break
+ }
+ }
+
+ return blink
+}
+
+// wordRight moves the cursor one word to the right. Returns whether or not the
+// cursor blink should be reset. If the input is masked, move input to the end
+// so as not to reveal word breaks in the masked input.
+func (m *Model) wordRight() bool {
+ if m.pos >= len(m.value) || len(m.value) == 0 {
+ return false
+ }
+
+ if m.EchoMode != EchoNormal {
+ return m.cursorEnd()
+ }
+
+ blink := false
+ i := m.pos
+ for i < len(m.value) {
+ if unicode.IsSpace(m.value[i]) {
+ blink = m.setCursor(m.pos + 1)
+ i++
+ } else {
+ break
+ }
+ }
+
+ for i < len(m.value) {
+ if !unicode.IsSpace(m.value[i]) {
+ blink = m.setCursor(m.pos + 1)
+ i++
+ } else {
+ break
+ }
+ }
+
+ return blink
+}
+
+func (m Model) echoTransform(v string) string {
+ switch m.EchoMode {
+ case EchoPassword:
+ return strings.Repeat(string(m.EchoCharacter), rw.StringWidth(v))
+ case EchoNone:
+ return ""
+
+ default:
+ return v
+ }
+}
+
+// Update is the Bubble Tea update loop.
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ if !m.focus {
+ m.blink = true
+ return m, nil
+ }
+
+ var resetBlink bool
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.Type {
+ case tea.KeyBackspace, tea.KeyCtrlH: // delete character before cursor
+ m.Err = nil
+
+ if msg.Alt {
+ resetBlink = m.deleteWordLeft()
+ } else {
+ if len(m.value) > 0 {
+ m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
+ if m.pos > 0 {
+ resetBlink = m.setCursor(m.pos - 1)
+ }
+ }
+ }
+ case tea.KeyLeft, tea.KeyCtrlB:
+ if msg.Alt { // alt+left arrow, back one word
+ resetBlink = m.wordLeft()
+ break
+ }
+ if m.pos > 0 { // left arrow, ^F, back one character
+ resetBlink = m.setCursor(m.pos - 1)
+ }
+ case tea.KeyRight, tea.KeyCtrlF:
+ if msg.Alt { // alt+right arrow, forward one word
+ resetBlink = m.wordRight()
+ break
+ }
+ if m.pos < len(m.value) { // right arrow, ^F, forward one character
+ resetBlink = m.setCursor(m.pos + 1)
+ }
+ case tea.KeyCtrlW: // ^W, delete word left of cursor
+ resetBlink = m.deleteWordLeft()
+ case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
+ resetBlink = m.cursorStart()
+ case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
+ if len(m.value) > 0 && m.pos < len(m.value) {
+ m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
+ }
+ case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
+ resetBlink = m.cursorEnd()
+ case tea.KeyCtrlK: // ^K, kill text after cursor
+ resetBlink = m.deleteAfterCursor()
+ case tea.KeyCtrlU: // ^U, kill text before cursor
+ resetBlink = m.deleteBeforeCursor()
+ case tea.KeyCtrlV: // ^V paste
+ return m, Paste
+ case tea.KeyRunes, tea.KeySpace: // input regular characters
+ if msg.Alt && len(msg.Runes) == 1 {
+ if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
+ resetBlink = m.deleteWordRight()
+ break
+ }
+ if msg.Runes[0] == 'b' { // alt+b, back one word
+ resetBlink = m.wordLeft()
+ break
+ }
+ if msg.Runes[0] == 'f' { // alt+f, forward one word
+ resetBlink = m.wordRight()
+ break
+ }
+ }
+
+ // Input a regular character
+ if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
+ runes := msg.Runes
+
+ value := make([]rune, len(m.value))
+ copy(value, m.value)
+ value = append(value[:m.pos], append(runes, value[m.pos:]...)...)
+ m.SetValue(string(value))
+ if m.Err == nil {
+ resetBlink = m.setCursor(m.pos + len(runes))
+ }
+ }
+ }
+
+ case initialBlinkMsg:
+ // We accept all initialBlinkMsgs genrated by the Blink command.
+
+ if m.cursorMode != CursorBlink || !m.focus {
+ return m, nil
+ }
+
+ cmd := m.blinkCmd()
+ return m, cmd
+
+ case blinkMsg:
+ // We're choosy about whether to accept blinkMsgs so that our cursor
+ // only exactly when it should.
+
+ // Is this model blinkable?
+ if m.cursorMode != CursorBlink || !m.focus {
+ return m, nil
+ }
+
+ // Were we expecting this blink message?
+ if msg.id != m.id || msg.tag != m.blinkTag {
+ return m, nil
+ }
+
+ var cmd tea.Cmd
+ if m.cursorMode == CursorBlink {
+ m.blink = !m.blink
+ cmd = m.blinkCmd()
+ }
+ return m, cmd
+
+ case blinkCanceled: // no-op
+ return m, nil
+
+ case pasteMsg:
+ resetBlink = m.handlePaste(string(msg))
+
+ case pasteErrMsg:
+ m.Err = msg
+ }
+
+ var cmd tea.Cmd
+ if resetBlink {
+ cmd = m.blinkCmd()
+ }
+
+ m.handleOverflow()
+ return m, cmd
+}
+
+// View renders the textinput in its current state.
+func (m Model) View() string {
+ // Placeholder text
+ if len(m.value) == 0 && m.Placeholder != "" {
+ return m.placeholderView()
+ }
+
+ styleText := m.TextStyle.Inline(true).Render
+
+ value := m.value[m.offset:m.offsetRight]
+ pos := max(0, m.pos-m.offset)
+ v := styleText(m.echoTransform(string(value[:pos])))
+
+ if pos < len(value) {
+ v += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it
+ v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
+ } else {
+ v += m.cursorView(" ")
+ }
+
+ // If a max width and background color were set fill the empty spaces with
+ // the background color.
+ valWidth := rw.StringWidth(string(value))
+ if m.Width > 0 && valWidth <= m.Width {
+ padding := max(0, m.Width-valWidth)
+ if valWidth+padding <= m.Width && pos < len(value) {
+ padding++
+ }
+ v += styleText(strings.Repeat(" ", padding))
+ }
+
+ return m.PromptStyle.Render(m.Prompt) + v
+}
+
+// placeholderView returns the prompt and placeholder view, if any.
+func (m Model) placeholderView() string {
+ var (
+ v string
+ p = m.Placeholder
+ style = m.PlaceholderStyle.Inline(true).Render
+ )
+
+ // Cursor
+ if m.blink {
+ v += m.cursorView(style(p[:1]))
+ } else {
+ v += m.cursorView(p[:1])
+ }
+
+ // The rest of the placeholder text
+ v += style(p[1:])
+
+ return m.PromptStyle.Render(m.Prompt) + v
+}
+
+// cursorView styles the cursor.
+func (m Model) cursorView(v string) string {
+ if m.blink {
+ return m.TextStyle.Render(v)
+ }
+ return m.CursorStyle.Inline(true).Reverse(true).Render(v)
+}
+
+// blinkCmd is an internal command used to manage cursor blinking.
+func (m *Model) blinkCmd() tea.Cmd {
+ if m.cursorMode != CursorBlink {
+ return nil
+ }
+
+ if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
+ m.blinkCtx.cancel()
+ }
+
+ ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
+ m.blinkCtx.cancel = cancel
+
+ m.blinkTag++
+
+ return func() tea.Msg {
+ defer cancel()
+ <-ctx.Done()
+ if ctx.Err() == context.DeadlineExceeded {
+ return blinkMsg{id: m.id, tag: m.blinkTag}
+ }
+ return blinkCanceled{}
+ }
+}
+
+// Blink is a command used to initialize cursor blinking.
+func Blink() tea.Msg {
+ return initialBlinkMsg{}
+}
+
+// Paste is a command for pasting from the clipboard into the text input.
+func Paste() tea.Msg {
+ str, err := clipboard.ReadAll()
+ if err != nil {
+ return pasteErrMsg{err}
+ }
+ return pasteMsg(str)
+}
+
+func clamp(v, low, high int) int {
+ if high < low {
+ low, high = high, low
+ }
+ return min(high, max(low, v))
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/vendor/github.com/muesli/ansi/README.md b/vendor/github.com/muesli/ansi/README.md
index cb28c65..f9d0fe9 100644
--- a/vendor/github.com/muesli/ansi/README.md
+++ b/vendor/github.com/muesli/ansi/README.md
@@ -1,2 +1,31 @@
# ansi
+
+[](https://github.com/muesli/ansi/releases)
+[](https://github.com/muesli/ansi/actions)
+[](https://coveralls.io/github/muesli/ansi?branch=master)
+[](https://goreportcard.com/report/muesli/ansi)
+[](https://pkg.go.dev/github.com/muesli/ansi)
+
Raw ANSI sequence helpers
+
+## ANSI Writer
+
+```go
+import "github.com/muesli/ansi"
+
+w := ansi.Writer{Forward: os.Stdout}
+w.Write([]byte("\x1b[31mHello, world!\x1b[0m"))
+w.Close()
+```
+
+## Compressor
+
+The ANSI compressor eliminates unnecessary/redundant ANSI sequences.
+
+```go
+import "github.com/muesli/ansi/compressor"
+
+w := compressor.Writer{Forward: os.Stdout}
+w.Write([]byte("\x1b[31mHello, world!\x1b[0m"))
+w.Close()
+```
diff --git a/vendor/github.com/sahilm/fuzzy/.editorconfig b/vendor/github.com/sahilm/fuzzy/.editorconfig
new file mode 100644
index 0000000..8a8f6a5
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/.editorconfig
@@ -0,0 +1,18 @@
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.sh]
+indent_size = 4
+
+[Makefile]
+indent_style = tab
+indent_size = 4
+
+[*.go]
+indent_style = tab
+indent_size = 4
diff --git a/vendor/github.com/sahilm/fuzzy/.gitignore b/vendor/github.com/sahilm/fuzzy/.gitignore
new file mode 100644
index 0000000..d6c59ee
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/.gitignore
@@ -0,0 +1,2 @@
+vendor/
+coverage/
diff --git a/vendor/github.com/sahilm/fuzzy/.travis.yml b/vendor/github.com/sahilm/fuzzy/.travis.yml
new file mode 100644
index 0000000..6756d80
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/.travis.yml
@@ -0,0 +1,5 @@
+language: go
+go:
+ - 1.x
+script:
+ - make
diff --git a/vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md b/vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md
new file mode 100644
index 0000000..7068ce1
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md
@@ -0,0 +1 @@
+Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise to respond promptly.
diff --git a/vendor/github.com/sahilm/fuzzy/Gopkg.lock b/vendor/github.com/sahilm/fuzzy/Gopkg.lock
new file mode 100644
index 0000000..6e3a7fe
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/Gopkg.lock
@@ -0,0 +1,20 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+ branch = "master"
+ digest = "1:ee97ec8a00b2424570c1ce53d7b410e96fbd4c241b29df134276ff6aa3750335"
+ name = "github.com/kylelemons/godebug"
+ packages = [
+ "diff",
+ "pretty",
+ ]
+ pruneopts = ""
+ revision = "d65d576e9348f5982d7f6d83682b694e731a45c6"
+
+[solve-meta]
+ analyzer-name = "dep"
+ analyzer-version = 1
+ input-imports = ["github.com/kylelemons/godebug/pretty"]
+ solver-name = "gps-cdcl"
+ solver-version = 1
diff --git a/vendor/github.com/sahilm/fuzzy/Gopkg.toml b/vendor/github.com/sahilm/fuzzy/Gopkg.toml
new file mode 100644
index 0000000..8f96b11
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/Gopkg.toml
@@ -0,0 +1,4 @@
+# Test dependency
+[[constraint]]
+ branch = "master"
+ name = "github.com/kylelemons/godebug"
diff --git a/vendor/github.com/sahilm/fuzzy/LICENSE b/vendor/github.com/sahilm/fuzzy/LICENSE
new file mode 100644
index 0000000..f848719
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Sahil Muthoo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/sahilm/fuzzy/Makefile b/vendor/github.com/sahilm/fuzzy/Makefile
new file mode 100644
index 0000000..7fa2be4
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/Makefile
@@ -0,0 +1,57 @@
+.PHONY: all
+all: setup lint test
+
+.PHONY: test
+test: setup
+ go test -bench ./...
+
+.PHONY: cover
+cover: setup
+ mkdir -p coverage
+ gocov test ./... | gocov-html > coverage/coverage.html
+
+sources = $(shell find . -name '*.go' -not -path './vendor/*')
+.PHONY: goimports
+goimports: setup
+ goimports -w $(sources)
+
+.PHONY: lint
+lint: setup
+ gometalinter ./... --enable=goimports --disable=gocyclo --vendor -t
+
+.PHONY: install
+install: setup
+ go install
+
+BIN_DIR := $(GOPATH)/bin
+GOIMPORTS := $(BIN_DIR)/goimports
+GOMETALINTER := $(BIN_DIR)/gometalinter
+DEP := $(BIN_DIR)/dep
+GOCOV := $(BIN_DIR)/gocov
+GOCOV_HTML := $(BIN_DIR)/gocov-html
+
+$(GOIMPORTS):
+ go get -u golang.org/x/tools/cmd/goimports
+
+$(GOMETALINTER):
+ go get -u github.com/alecthomas/gometalinter
+ gometalinter --install &> /dev/null
+
+$(GOCOV):
+ go get -u github.com/axw/gocov/gocov
+
+$(GOCOV_HTML):
+ go get -u gopkg.in/matm/v1/gocov-html
+
+$(DEP):
+ go get -u github.com/golang/dep/cmd/dep
+
+tools: $(GOIMPORTS) $(GOMETALINTER) $(GOCOV) $(GOCOV_HTML) $(DEP)
+
+vendor: $(DEP)
+ dep ensure
+
+setup: tools vendor
+
+updatedeps:
+ dep ensure -update
diff --git a/vendor/github.com/sahilm/fuzzy/README.md b/vendor/github.com/sahilm/fuzzy/README.md
new file mode 100644
index 0000000..c632da5
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/README.md
@@ -0,0 +1,184 @@
+
+
+# fuzzy
+[](https://travis-ci.org/sahilm/fuzzy)
+[](https://godoc.org/github.com/sahilm/fuzzy)
+
+Go library that provides fuzzy string matching optimized for filenames and code symbols in the style of Sublime Text,
+VSCode, IntelliJ IDEA et al. This library is external dependency-free. It only depends on the Go standard library.
+
+## Features
+
+- Intuitive matching. Results are returned in descending order of match quality. Quality is determined by:
+ - The first character in the pattern matches the first character in the match string.
+ - The matched character is camel cased.
+ - The matched character follows a separator such as an underscore character.
+ - The matched character is adjacent to a previous match.
+
+- Speed. Matches are returned in milliseconds. It's perfect for interactive search boxes.
+
+- The positions of matches is returned. Allows you to highlight matching characters.
+
+- Unicode aware.
+
+## Demo
+
+Here is a [demo](_example/main.go) of matching various patterns against ~16K files from the Unreal Engine 4 codebase.
+
+
+
+You can run the demo yourself like so:
+
+```
+cd _example/
+go get github.com/jroimartin/gocui
+go run main.go
+```
+
+## Usage
+
+The following example prints out matches with the matched chars in bold.
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/sahilm/fuzzy"
+)
+
+func main() {
+ const bold = "\033[1m%s\033[0m"
+ pattern := "mnr"
+ data := []string{"game.cpp", "moduleNameResolver.ts", "my name is_Ramsey"}
+
+ matches := fuzzy.Find(pattern, data)
+
+ for _, match := range matches {
+ for i := 0; i < len(match.Str); i++ {
+ if contains(i, match.MatchedIndexes) {
+ fmt.Print(fmt.Sprintf(bold, string(match.Str[i])))
+ } else {
+ fmt.Print(string(match.Str[i]))
+ }
+ }
+ fmt.Println()
+ }
+}
+
+func contains(needle int, haystack []int) bool {
+ for _, i := range haystack {
+ if needle == i {
+ return true
+ }
+ }
+ return false
+}
+```
+If the data you want to match isn't a slice of strings, you can use `FindFromSource` by implementing
+the provided `Source` interface. Here's an example:
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/sahilm/fuzzy"
+)
+
+type employee struct {
+ name string
+ age int
+}
+
+type employees []employee
+
+func (e employees) String(i int) string {
+ return e[i].name
+}
+
+func (e employees) Len() int {
+ return len(e)
+}
+
+func main() {
+ emps := employees{
+ {
+ name: "Alice",
+ age: 45,
+ },
+ {
+ name: "Bob",
+ age: 35,
+ },
+ {
+ name: "Allie",
+ age: 35,
+ },
+ }
+ results := fuzzy.FindFrom("al", emps)
+ fmt.Println(results)
+}
+```
+
+Check out the [godoc](https://godoc.org/github.com/sahilm/fuzzy) for detailed documentation.
+
+## Installation
+
+`go get github.com/sahilm/fuzzy` or use your favorite dependency management tool.
+
+## Speed
+
+Here are a few benchmark results on a normal laptop.
+
+```
+BenchmarkFind/with_unreal_4_(~16K_files)-4 100 12915315 ns/op
+BenchmarkFind/with_linux_kernel_(~60K_files)-4 50 30885038 ns/op
+```
+
+Matching a pattern against ~60K files from the Linux kernel takes about 30ms.
+
+## Contributing
+
+Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise
+to respond promptly.
+
+## Credits
+
+* [@ericpauley](https://github.com/ericpauley) & [@lunixbochs](https://github.com/lunixbochs) contributed Unicode awareness and various performance optimisations.
+
+* The algorithm is based of the awesome work of [forrestthewoods](https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js).
+See [this](https://blog.forrestthewoods.com/reverse-engineering-sublime-text-s-fuzzy-match-4cffeed33fdb#.d05n81yjy)
+blog post for details of the algorithm.
+
+* The artwork is by my lovely wife Sanah. It's based on the Go Gopher.
+
+* The Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
+The design is licensed under the Creative Commons 3.0 Attributions license.
+
+## License
+
+The MIT License (MIT)
+
+Copyright (c) 2017 Sahil Muthoo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/sahilm/fuzzy/fuzzy.go b/vendor/github.com/sahilm/fuzzy/fuzzy.go
new file mode 100644
index 0000000..bd66ee6
--- /dev/null
+++ b/vendor/github.com/sahilm/fuzzy/fuzzy.go
@@ -0,0 +1,235 @@
+/*
+Package fuzzy provides fuzzy string matching optimized
+for filenames and code symbols in the style of Sublime Text,
+VSCode, IntelliJ IDEA et al.
+*/
+package fuzzy
+
+import (
+ "sort"
+ "unicode"
+ "unicode/utf8"
+)
+
+// Match represents a matched string.
+type Match struct {
+ // The matched string.
+ Str string
+ // The index of the matched string in the supplied slice.
+ Index int
+ // The indexes of matched characters. Useful for highlighting matches.
+ MatchedIndexes []int
+ // Score used to rank matches
+ Score int
+}
+
+const (
+ firstCharMatchBonus = 10
+ matchFollowingSeparatorBonus = 20
+ camelCaseMatchBonus = 20
+ adjacentMatchBonus = 5
+ unmatchedLeadingCharPenalty = -5
+ maxUnmatchedLeadingCharPenalty = -15
+)
+
+var separators = []rune("/-_ .\\")
+
+// Matches is a slice of Match structs
+type Matches []Match
+
+func (a Matches) Len() int { return len(a) }
+func (a Matches) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a Matches) Less(i, j int) bool { return a[i].Score >= a[j].Score }
+
+// Source represents an abstract source of a list of strings. Source must be iterable type such as a slice.
+// The source will be iterated over till Len() with String(i) being called for each element where i is the
+// index of the element. You can find a working example in the README.
+type Source interface {
+ // The string to be matched at position i.
+ String(i int) string
+ // The length of the source. Typically is the length of the slice of things that you want to match.
+ Len() int
+}
+
+type stringSource []string
+
+func (ss stringSource) String(i int) string {
+ return ss[i]
+}
+
+func (ss stringSource) Len() int { return len(ss) }
+
+/*
+Find looks up pattern in data and returns matches
+in descending order of match quality. Match quality
+is determined by a set of bonus and penalty rules.
+
+The following types of matches apply a bonus:
+
+* The first character in the pattern matches the first character in the match string.
+
+* The matched character is camel cased.
+
+* The matched character follows a separator such as an underscore character.
+
+* The matched character is adjacent to a previous match.
+
+Penalties are applied for every character in the search string that wasn't matched and all leading
+characters upto the first match.
+*/
+func Find(pattern string, data []string) Matches {
+ return FindFrom(pattern, stringSource(data))
+}
+
+/*
+FindFrom is an alternative implementation of Find using a Source
+instead of a list of strings.
+*/
+func FindFrom(pattern string, data Source) Matches {
+ if len(pattern) == 0 {
+ return nil
+ }
+ runes := []rune(pattern)
+ var matches Matches
+ var matchedIndexes []int
+ for i := 0; i < data.Len(); i++ {
+ var match Match
+ match.Str = data.String(i)
+ match.Index = i
+ if matchedIndexes != nil {
+ match.MatchedIndexes = matchedIndexes
+ } else {
+ match.MatchedIndexes = make([]int, 0, len(runes))
+ }
+ var score int
+ patternIndex := 0
+ bestScore := -1
+ matchedIndex := -1
+ currAdjacentMatchBonus := 0
+ var last rune
+ var lastIndex int
+ nextc, nextSize := utf8.DecodeRuneInString(data.String(i))
+ var candidate rune
+ var candidateSize int
+ for j := 0; j < len(data.String(i)); j += candidateSize {
+ candidate, candidateSize = nextc, nextSize
+ if equalFold(candidate, runes[patternIndex]) {
+ score = 0
+ if j == 0 {
+ score += firstCharMatchBonus
+ }
+ if unicode.IsLower(last) && unicode.IsUpper(candidate) {
+ score += camelCaseMatchBonus
+ }
+ if j != 0 && isSeparator(last) {
+ score += matchFollowingSeparatorBonus
+ }
+ if len(match.MatchedIndexes) > 0 {
+ lastMatch := match.MatchedIndexes[len(match.MatchedIndexes)-1]
+ bonus := adjacentCharBonus(lastIndex, lastMatch, currAdjacentMatchBonus)
+ score += bonus
+ // adjacent matches are incremental and keep increasing based on previous adjacent matches
+ // thus we need to maintain the current match bonus
+ currAdjacentMatchBonus += bonus
+ }
+ if score > bestScore {
+ bestScore = score
+ matchedIndex = j
+ }
+ }
+ var nextp rune
+ if patternIndex < len(runes)-1 {
+ nextp = runes[patternIndex+1]
+ }
+ if j+candidateSize < len(data.String(i)) {
+ if data.String(i)[j+candidateSize] < utf8.RuneSelf { // Fast path for ASCII
+ nextc, nextSize = rune(data.String(i)[j+candidateSize]), 1
+ } else {
+ nextc, nextSize = utf8.DecodeRuneInString(data.String(i)[j+candidateSize:])
+ }
+ } else {
+ nextc, nextSize = 0, 0
+ }
+ // We apply the best score when we have the next match coming up or when the search string has ended.
+ // Tracking when the next match is coming up allows us to exhaustively find the best match and not necessarily
+ // the first match.
+ // For example given the pattern "tk" and search string "The Black Knight", exhaustively matching allows us
+ // to match the second k thus giving this string a higher score.
+ if equalFold(nextp, nextc) || nextc == 0 {
+ if matchedIndex > -1 {
+ if len(match.MatchedIndexes) == 0 {
+ penalty := matchedIndex * unmatchedLeadingCharPenalty
+ bestScore += max(penalty, maxUnmatchedLeadingCharPenalty)
+ }
+ match.Score += bestScore
+ match.MatchedIndexes = append(match.MatchedIndexes, matchedIndex)
+ score = 0
+ bestScore = -1
+ patternIndex++
+ }
+ }
+ lastIndex = j
+ last = candidate
+ }
+ // apply penalty for each unmatched character
+ penalty := len(match.MatchedIndexes) - len(data.String(i))
+ match.Score += penalty
+ if len(match.MatchedIndexes) == len(runes) {
+ matches = append(matches, match)
+ matchedIndexes = nil
+ } else {
+ matchedIndexes = match.MatchedIndexes[:0] // Recycle match index slice
+ }
+ }
+ sort.Stable(matches)
+ return matches
+}
+
+// Taken from strings.EqualFold
+func equalFold(tr, sr rune) bool {
+ if tr == sr {
+ return true
+ }
+ if tr < sr {
+ tr, sr = sr, tr
+ }
+ // Fast check for ASCII.
+ if tr < utf8.RuneSelf {
+ // ASCII, and sr is upper case. tr must be lower case.
+ if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
+ return true
+ }
+ return false
+ }
+
+ // General case. SimpleFold(x) returns the next equivalent rune > x
+ // or wraps around to smaller values.
+ r := unicode.SimpleFold(sr)
+ for r != sr && r < tr {
+ r = unicode.SimpleFold(r)
+ }
+ return r == tr
+}
+
+func adjacentCharBonus(i int, lastMatch int, currentBonus int) int {
+ if lastMatch == i {
+ return currentBonus*2 + adjacentMatchBonus
+ }
+ return 0
+}
+
+func isSeparator(s rune) bool {
+ for _, sep := range separators {
+ if s == sep {
+ return true
+ }
+ }
+ return false
+}
+
+func max(x int, y int) int {
+ if x > y {
+ return x
+ }
+ return y
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a3d9226..2d075e0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -6,13 +6,20 @@ github.com/VividCortex/ewma
github.com/acarl005/stripansi
# github.com/alessio/shellescape v1.4.1
## explicit; go 1.14
+# github.com/atotto/clipboard v0.1.4
+## explicit
+github.com/atotto/clipboard
# github.com/aymanbagabas/go-osc52 v1.0.3
## explicit; go 1.16
github.com/aymanbagabas/go-osc52
# github.com/charmbracelet/bubbles v0.14.0
## explicit; go 1.13
+github.com/charmbracelet/bubbles/help
github.com/charmbracelet/bubbles/key
+github.com/charmbracelet/bubbles/list
+github.com/charmbracelet/bubbles/paginator
github.com/charmbracelet/bubbles/spinner
+github.com/charmbracelet/bubbles/textinput
# github.com/charmbracelet/bubbletea v0.23.1
## explicit; go 1.16
github.com/charmbracelet/bubbletea
@@ -82,7 +89,7 @@ github.com/mattn/go-runewidth
# github.com/mitchellh/mapstructure v1.5.0
## explicit; go 1.14
github.com/mitchellh/mapstructure
-# github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
+# github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70
## explicit; go 1.17
github.com/muesli/ansi
github.com/muesli/ansi/compressor
@@ -121,6 +128,9 @@ github.com/rivo/uniseg
# github.com/rogpeppe/go-internal v1.9.0
## explicit; go 1.17
github.com/rogpeppe/go-internal/fmtsort
+# github.com/sahilm/fuzzy v0.1.0
+## explicit
+github.com/sahilm/fuzzy
# github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
## explicit
github.com/skratchdot/open-golang/open