Added Bubble Tea TUI, and adjusted tape files

This commit is contained in:
luke-hagar-sp
2022-12-21 19:20:10 -06:00
parent d72e7dbdd6
commit c24a74b6c0
38 changed files with 4245 additions and 82 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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)

4
go.mod
View File

@@ -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

6
go.sum
View File

@@ -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=

View File

@@ -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)
}
}

99
internal/tui/tui.go Normal file
View File

@@ -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
}

22
vendor/github.com/atotto/clipboard/.travis.yml generated vendored Normal file
View File

@@ -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 .

27
vendor/github.com/atotto/clipboard/LICENSE generated vendored Normal file
View File

@@ -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.

48
vendor/github.com/atotto/clipboard/README.md generated vendored Normal file
View File

@@ -0,0 +1,48 @@
[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard)
[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](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

20
vendor/github.com/atotto/clipboard/clipboard.go generated vendored Normal file
View File

@@ -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

52
vendor/github.com/atotto/clipboard/clipboard_darwin.go generated vendored Normal file
View File

@@ -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()
}

42
vendor/github.com/atotto/clipboard/clipboard_plan9.go generated vendored Normal file
View File

@@ -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
}

149
vendor/github.com/atotto/clipboard/clipboard_unix.go generated vendored Normal file
View File

@@ -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()
}

157
vendor/github.com/atotto/clipboard/clipboard_windows.go generated vendored Normal file
View File

@@ -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
}

233
vendor/github.com/charmbracelet/bubbles/help/help.go generated vendored Normal file
View File

@@ -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
}

View File

@@ -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
}

97
vendor/github.com/charmbracelet/bubbles/list/keys.go generated vendored Normal file
View File

@@ -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")),
}
}

1264
vendor/github.com/charmbracelet/bubbles/list/list.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

99
vendor/github.com/charmbracelet/bubbles/list/style.go generated vendored Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,2 +1,31 @@
# ansi
[![Latest Release](https://img.shields.io/github/release/muesli/ansi.svg)](https://github.com/muesli/ansi/releases)
[![Build Status](https://github.com/muesli/ansi/workflows/build/badge.svg)](https://github.com/muesli/ansi/actions)
[![Coverage Status](https://coveralls.io/repos/github/muesli/ansi/badge.svg?branch=master)](https://coveralls.io/github/muesli/ansi?branch=master)
[![Go ReportCard](https://goreportcard.com/badge/muesli/ansi)](https://goreportcard.com/report/muesli/ansi)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](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()
```

18
vendor/github.com/sahilm/fuzzy/.editorconfig generated vendored Normal file
View File

@@ -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

2
vendor/github.com/sahilm/fuzzy/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
vendor/
coverage/

5
vendor/github.com/sahilm/fuzzy/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,5 @@
language: go
go:
- 1.x
script:
- make

1
vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1 @@
Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise to respond promptly.

20
vendor/github.com/sahilm/fuzzy/Gopkg.lock generated vendored Normal file
View File

@@ -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

4
vendor/github.com/sahilm/fuzzy/Gopkg.toml generated vendored Normal file
View File

@@ -0,0 +1,4 @@
# Test dependency
[[constraint]]
branch = "master"
name = "github.com/kylelemons/godebug"

21
vendor/github.com/sahilm/fuzzy/LICENSE generated vendored Normal file
View File

@@ -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.

57
vendor/github.com/sahilm/fuzzy/Makefile generated vendored Normal file
View File

@@ -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

184
vendor/github.com/sahilm/fuzzy/README.md generated vendored Normal file
View File

@@ -0,0 +1,184 @@
<img src="assets/search-gopher-1.png" alt="gopher looking for stuff"> <img src="assets/search-gopher-2.png" alt="gopher found stuff">
# fuzzy
[![Build Status](https://travis-ci.org/sahilm/fuzzy.svg?branch=master)](https://travis-ci.org/sahilm/fuzzy)
[![Documentation](https://godoc.org/github.com/sahilm/fuzzy?status.svg)](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.
![demo](assets/demo.gif)
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.

235
vendor/github.com/sahilm/fuzzy/fuzzy.go generated vendored Normal file
View File

@@ -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
}

12
vendor/modules.txt vendored
View File

@@ -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