diff --git a/go.mod b/go.mod index eee049c..3d0e2a1 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/Eun/bubbleviews go 1.19 require ( - github.com/charmbracelet/bubbles v0.16.1 - github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/bubbles v0.17.1 + github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.9.1 github.com/muesli/reflow v0.3.0 ) @@ -21,7 +21,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.3 // indirect - github.com/sahilm/fuzzy v0.1.0 // indirect + github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.6.0 // indirect diff --git a/go.sum b/go.sum index 6341046..e49371c 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,10 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= -github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= -github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= +github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= @@ -32,8 +32,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= -github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/vendor/github.com/charmbracelet/bubbles/list/list.go b/vendor/github.com/charmbracelet/bubbles/list/list.go index f286274..7b47879 100644 --- a/vendor/github.com/charmbracelet/bubbles/list/list.go +++ b/vendor/github.com/charmbracelet/bubbles/list/list.go @@ -87,7 +87,7 @@ type Rank struct { // 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) + ranks := fuzzy.Find(term, targets) sort.Stable(ranks) result := make([]Rank, len(ranks)) for i, r := range ranks { @@ -99,6 +99,20 @@ func DefaultFilter(term string, targets []string) []Rank { return result } +// UnsortedFilter uses the sahilm/fuzzy to filter through the list. It does not +// sort the results. +func UnsortedFilter(term string, targets []string) []Rank { + ranks := fuzzy.FindNoSort(term, targets) + 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. @@ -261,7 +275,7 @@ func (m Model) ShowTitle() bool { return m.showTitle } -// SetShowFilter shows or hides the filer bar. Note that this does not disable +// SetShowFilter shows or hides the filter 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. @@ -1160,7 +1174,7 @@ func (m Model) populatedView() string { if m.filterState == Filtering { return "" } - return m.Styles.NoItems.Render("No " + m.itemNamePlural + " found.") + return m.Styles.NoItems.Render("No " + m.itemNamePlural + ".") } if len(items) > 0 { @@ -1204,11 +1218,11 @@ func filterItems(m Model) tea.Cmd { return FilterMatchesMsg(m.itemsAsFilterItems()) // return nothing } - targets := []string{} items := m.items + targets := make([]string, len(items)) - for _, t := range items { - targets = append(targets, t.FilterValue()) + for i, t := range items { + targets[i] = t.FilterValue() } filterMatches := []filteredItem{} diff --git a/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go index f8990f6..964d28f 100644 --- a/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go +++ b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go @@ -1,6 +1,7 @@ package textinput import ( + "reflect" "strings" "time" "unicode" @@ -32,8 +33,6 @@ const ( // EchoNone displays nothing as characters are entered. This is commonly // seen for password fields on the command line. EchoNone - - // EchoOnEdit. ) // ValidateFunc is a function that returns an error if the input is invalid. @@ -54,6 +53,9 @@ type KeyMap struct { LineStart key.Binding LineEnd key.Binding Paste key.Binding + AcceptSuggestion key.Binding + NextSuggestion key.Binding + PrevSuggestion key.Binding } // DefaultKeyMap is the default set of key bindings for navigating and acting @@ -72,6 +74,9 @@ var DefaultKeyMap = KeyMap{ LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")), LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")), Paste: key.NewBinding(key.WithKeys("ctrl+v")), + AcceptSuggestion: key.NewBinding(key.WithKeys("tab")), + NextSuggestion: key.NewBinding(key.WithKeys("down", "ctrl+n")), + PrevSuggestion: key.NewBinding(key.WithKeys("up", "ctrl+p")), } // Model is the Bubble Tea model for this text input element. @@ -95,6 +100,7 @@ type Model struct { PromptStyle lipgloss.Style TextStyle lipgloss.Style PlaceholderStyle lipgloss.Style + CompletionStyle lipgloss.Style // Deprecated: use Cursor.Style instead. CursorStyle lipgloss.Style @@ -134,6 +140,15 @@ type Model struct { // rune sanitizer for input. rsan runeutil.Sanitizer + + // Should the input suggest to complete + ShowSuggestions bool + + // suggestions is a list of suggestions that may be used to complete the + // input. + suggestions [][]rune + matchedSuggestions [][]rune + currentSuggestionIndex int } // New creates a new model with default settings. @@ -143,12 +158,15 @@ func New() Model { EchoCharacter: '*', CharLimit: 0, PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + ShowSuggestions: false, + CompletionStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), Cursor: cursor.New(), KeyMap: DefaultKeyMap, - value: nil, - focus: false, - pos: 0, + suggestions: [][]rune{}, + value: nil, + focus: false, + pos: 0, } } @@ -239,6 +257,17 @@ func (m *Model) Reset() { m.SetCursor(0) } +// SetSuggestions sets the suggestions for the input. +func (m *Model) SetSuggestions(suggestions []string) { + m.suggestions = [][]rune{} + + for _, s := range suggestions { + m.suggestions = append(m.suggestions, []rune(s)) + } + + m.updateSuggestions() +} + // rsan initializes or retrieves the rune sanitizer. func (m *Model) san() runeutil.Sanitizer { if m.rsan == nil { @@ -529,6 +558,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } + // Need to check for completion before, because key is configurable and might be double assigned + keyMsg, ok := msg.(tea.KeyMsg) + if ok && key.Matches(keyMsg, m.KeyMap.AcceptSuggestion) { + if m.canAcceptSuggestion() { + m.value = append(m.value, m.matchedSuggestions[m.currentSuggestionIndex][len(m.value):]...) + m.CursorEnd() + } + } + // Let's remember where the position of the cursor currently is so that if // the cursor position changes, we can reset the blink. oldPos := m.pos //nolint @@ -577,11 +615,19 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, Paste case key.Matches(msg, m.KeyMap.DeleteWordForward): m.deleteWordForward() + case key.Matches(msg, m.KeyMap.NextSuggestion): + m.nextSuggestion() + case key.Matches(msg, m.KeyMap.PrevSuggestion): + m.previousSuggestion() default: // Input one or more regular characters. m.insertRunesFromUserInput(msg.Runes) } + // Check again if can be completed + // because value might be something that does not match the completion prefix + m.updateSuggestions() + case pasteMsg: m.insertRunesFromUserInput([]rune(msg)) @@ -622,9 +668,23 @@ func (m Model) View() string { m.Cursor.SetChar(char) v += m.Cursor.View() // cursor and text under it v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor + v += m.completionView(0) // suggested completion } else { - m.Cursor.SetChar(" ") - v += m.Cursor.View() + if m.canAcceptSuggestion() { + suggestion := m.matchedSuggestions[m.currentSuggestionIndex] + if len(value) < len(suggestion) { + m.Cursor.TextStyle = m.CompletionStyle + m.Cursor.SetChar(m.echoTransform(string(suggestion[pos]))) + v += m.Cursor.View() + v += m.completionView(1) + } else { + m.Cursor.SetChar(" ") + v += m.Cursor.View() + } + } else { + m.Cursor.SetChar(" ") + v += m.Cursor.View() + } } // If a max width and background color were set fill the empty spaces with @@ -645,16 +705,16 @@ func (m Model) View() string { func (m Model) placeholderView() string { var ( v string - p = m.Placeholder + p = []rune(m.Placeholder) style = m.PlaceholderStyle.Inline(true).Render ) m.Cursor.TextStyle = m.PlaceholderStyle - m.Cursor.SetChar(p[:1]) + m.Cursor.SetChar(string(p[:1])) v += m.Cursor.View() // The rest of the placeholder text - v += style(p[1:]) + v += style(string(p[1:])) return m.PromptStyle.Render(m.Prompt) + v } @@ -721,3 +781,81 @@ func (m Model) CursorMode() CursorMode { func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd { return m.Cursor.SetMode(cursor.Mode(mode)) } + +func (m Model) completionView(offset int) string { + var ( + value = m.value + style = m.PlaceholderStyle.Inline(true).Render + ) + + if m.canAcceptSuggestion() { + suggestion := m.matchedSuggestions[m.currentSuggestionIndex] + if len(value) < len(suggestion) { + return style(string(suggestion[len(value)+offset:])) + } + } + return "" +} + +// AvailableSuggestions returns the list of available suggestions. +func (m *Model) AvailableSuggestions() []string { + suggestions := []string{} + for _, s := range m.suggestions { + suggestions = append(suggestions, string(s)) + } + + return suggestions +} + +// CurrentSuggestion returns the currently selected suggestion. +func (m *Model) CurrentSuggestion() string { + return string(m.matchedSuggestions[m.currentSuggestionIndex]) +} + +// canAcceptSuggestion returns whether there is an acceptable suggestion to +// autocomplete the current value. +func (m *Model) canAcceptSuggestion() bool { + return len(m.matchedSuggestions) > 0 +} + +// updateSuggestions refreshes the list of matching suggestions. +func (m *Model) updateSuggestions() { + if !m.ShowSuggestions { + return + } + + if len(m.value) <= 0 || len(m.suggestions) <= 0 { + m.matchedSuggestions = [][]rune{} + return + } + + matches := [][]rune{} + for _, s := range m.suggestions { + suggestion := string(s) + + if strings.HasPrefix(strings.ToLower(suggestion), strings.ToLower(string(m.value))) { + matches = append(matches, []rune(suggestion)) + } + } + if !reflect.DeepEqual(matches, m.matchedSuggestions) { + m.currentSuggestionIndex = 0 + } + + m.matchedSuggestions = matches +} + +// nextSuggestion selects the next suggestion. +func (m *Model) nextSuggestion() { + m.currentSuggestionIndex = (m.currentSuggestionIndex + 1) + if m.currentSuggestionIndex >= len(m.matchedSuggestions) { + m.currentSuggestionIndex = 0 + } +} + +// previousSuggestion selects the previous suggestion. +func (m *Model) previousSuggestion() { + m.currentSuggestionIndex = (m.currentSuggestionIndex - 1) + if m.currentSuggestionIndex < 0 { + m.currentSuggestionIndex = len(m.matchedSuggestions) - 1 + } +} diff --git a/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go b/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go index b13e33c..019da7d 100644 --- a/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go +++ b/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go @@ -332,17 +332,17 @@ func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) { } case tea.MouseMsg: - if !m.MouseWheelEnabled { + if !m.MouseWheelEnabled || msg.Action != tea.MouseActionPress { break } - switch msg.Type { - case tea.MouseWheelUp: + switch msg.Button { + case tea.MouseButtonWheelUp: lines := m.LineUp(m.MouseWheelDelta) if m.HighPerformanceRendering { cmd = ViewUp(m, lines) } - case tea.MouseWheelDown: + case tea.MouseButtonWheelDown: lines := m.LineDown(m.MouseWheelDelta) if m.HighPerformanceRendering { cmd = ViewDown(m, lines) diff --git a/vendor/github.com/charmbracelet/bubbletea/.gitattributes b/vendor/github.com/charmbracelet/bubbletea/.gitattributes new file mode 100644 index 0000000..6c929d4 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/.gitattributes @@ -0,0 +1 @@ +*.golden -text diff --git a/vendor/github.com/charmbracelet/bubbletea/README.md b/vendor/github.com/charmbracelet/bubbletea/README.md index 0a88caa..0834246 100644 --- a/vendor/github.com/charmbracelet/bubbletea/README.md +++ b/vendor/github.com/charmbracelet/bubbletea/README.md @@ -341,8 +341,10 @@ For some Bubble Tea programs in production, see: * [gh-dash](https://www.github.com/dlvhdr/gh-dash): a GitHub CLI extension for PRs and issues * [gitflow-toolkit](https://github.com/mritd/gitflow-toolkit): a GitFlow submission tool * [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser, and online markdown stash +* [go-sweep](https://github.com/maxpaulus43/go-sweep): Minesweeper in the terminal * [gocovsh](https://github.com/orlangure/gocovsh): explore Go coverage reports from the CLI * [got](https://github.com/fedeztk/got): a simple translator and text-to-speech app build on top of simplytranslate's APIs +* [hiSHtory](https://github.com/ddworken/hishtory): your shell history in context, synced, and queryable * [httpit](https://github.com/gonetx/httpit): a rapid http(s) benchmark tool * [IDNT](https://github.com/r-darwish/idnt): a batch software uninstaller * [kboard](https://github.com/CamiloGarciaLaRotta/kboard): a typing game @@ -351,15 +353,18 @@ For some Bubble Tea programs in production, see: * [mergestat](https://github.com/mergestat/mergestat): run SQL queries on git repositories * [Neon Modem Overdrive](https://github.com/mrusme/neonmodem): a BBS-style TUI client for Discourse, Lemmy, Lobste.rs and Hacker News * [Noted](https://github.com/torbratsberg/noted): a note viewer and manager +* [nom](https://github.com/guyfedwards/nom): RSS reader and manager * [pathos](https://github.com/chip/pathos): a PATH env variable editor * [portal](https://github.com/ZinoKader/portal): secure transfers between computers * [redis-viewer](https://github.com/SaltFishPr/redis-viewer): a Redis databases browser +* [scrabbler](https://github.com/wI2L/scrabbler): Automatic draw TUI for your duplicate Scrabble games * [sku](https://github.com/fedeztk/sku): Sudoku on the CLI * [Slides](https://github.com/maaslalani/slides): a markdown-based presentation tool * [SlurmCommander](https://github.com/CLIP-HPC/SlurmCommander): a Slurm workload manager TUI * [Soft Serve](https://github.com/charmbracelet/soft-serve): a command-line-first Git server that runs a TUI over SSH * [solitaire-tui](https://github.com/brianstrauch/solitaire-tui): Klondike Solitaire for the terminal * [StormForge Optimize Controller](https://github.com/thestormforge/optimize-controller): a tool for experimenting with application configurations in Kubernetes +* [Storydb](https://github.com/grrlopes/storydb): a bash/zsh ctrl+r improved command history finder. * [STTG](https://github.com/wille1101/sttg): a teletext client for SVT, Sweden’s national public television station * [sttr](https://github.com/abhimanyu003/sttr): a general-purpose text transformer * [tasktimer](https://github.com/caarlos0/tasktimer): a dead-simple task timer @@ -367,8 +372,10 @@ For some Bubble Tea programs in production, see: * [ticker](https://github.com/achannarasappa/ticker): a terminal stock viewer and stock position tracker * [tran](https://github.com/abdfnx/tran): securely transfer stuff between computers (based on [portal](https://github.com/ZinoKader/portal)) * [Typer](https://github.com/maaslalani/typer): a typing test +* [typioca](https://github.com/bloznelis/typioca): Cozy typing speed tester in terminal * [tz](https://github.com/oz/tz): an aid for scheduling across multiple time zones * [ugm](https://github.com/ariasmn/ugm): a unix user and group browser +* [walk](https://github.com/antonmedv/walk): a terminal navigator * [wander](https://github.com/robinovitch61/wander): a HashiCorp Nomad terminal client * [WG Commander](https://github.com/AndrianBdn/wg-cmd): a TUI for a simple WireGuard VPN setup * [wishlist](https://github.com/charmbracelet/wishlist): an SSH directory diff --git a/vendor/github.com/charmbracelet/bubbletea/commands.go b/vendor/github.com/charmbracelet/bubbletea/commands.go index 7c30a12..7b139b8 100644 --- a/vendor/github.com/charmbracelet/bubbletea/commands.go +++ b/vendor/github.com/charmbracelet/bubbletea/commands.go @@ -13,7 +13,7 @@ import ( // return tea.Batch(someCommand, someOtherCommand) // } func Batch(cmds ...Cmd) Cmd { - var validCmds []Cmd + var validCmds []Cmd //nolint:prealloc for _, c := range cmds { if c == nil { continue @@ -170,3 +170,20 @@ func Sequentially(cmds ...Cmd) Cmd { return nil } } + +// setWindowTitleMsg is an internal message used to set the window title. +type setWindowTitleMsg string + +// SetWindowTitle produces a command that sets the terminal title. +// +// For example: +// +// func (m model) Init() Cmd { +// // Set title. +// return tea.SetWindowTitle("My App") +// } +func SetWindowTitle(title string) Cmd { + return func() Msg { + return setWindowTitleMsg(title) + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/key.go b/vendor/github.com/charmbracelet/bubbletea/key.go index c2e5e3a..f851490 100644 --- a/vendor/github.com/charmbracelet/bubbletea/key.go +++ b/vendor/github.com/charmbracelet/bubbletea/key.go @@ -1,11 +1,11 @@ package tea import ( - "errors" + "context" + "fmt" "io" + "regexp" "unicode/utf8" - - "github.com/mattn/go-localereader" ) // KeyMsg contains information about a keypress. KeyMsgs are always sent to @@ -338,85 +338,73 @@ var keyNames = map[KeyType]string{ // Sequence mappings. var sequences = map[string]Key{ // Arrow keys - "\x1b[A": {Type: KeyUp}, - "\x1b[B": {Type: KeyDown}, - "\x1b[C": {Type: KeyRight}, - "\x1b[D": {Type: KeyLeft}, - "\x1b[1;2A": {Type: KeyShiftUp}, - "\x1b[1;2B": {Type: KeyShiftDown}, - "\x1b[1;2C": {Type: KeyShiftRight}, - "\x1b[1;2D": {Type: KeyShiftLeft}, - "\x1b[OA": {Type: KeyShiftUp}, // DECCKM - "\x1b[OB": {Type: KeyShiftDown}, // DECCKM - "\x1b[OC": {Type: KeyShiftRight}, // DECCKM - "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM - "\x1b[a": {Type: KeyShiftUp}, // urxvt - "\x1b[b": {Type: KeyShiftDown}, // urxvt - "\x1b[c": {Type: KeyShiftRight}, // urxvt - "\x1b[d": {Type: KeyShiftLeft}, // urxvt - "\x1b[1;3A": {Type: KeyUp, Alt: true}, - "\x1b[1;3B": {Type: KeyDown, Alt: true}, - "\x1b[1;3C": {Type: KeyRight, Alt: true}, - "\x1b[1;3D": {Type: KeyLeft, Alt: true}, - "\x1b\x1b[A": {Type: KeyUp, Alt: true}, // urxvt - "\x1b\x1b[B": {Type: KeyDown, Alt: true}, // urxvt - "\x1b\x1b[C": {Type: KeyRight, Alt: true}, // urxvt - "\x1b\x1b[D": {Type: KeyLeft, Alt: true}, // urxvt - "\x1b[1;4A": {Type: KeyShiftUp, Alt: true}, - "\x1b[1;4B": {Type: KeyShiftDown, Alt: true}, - "\x1b[1;4C": {Type: KeyShiftRight, Alt: true}, - "\x1b[1;4D": {Type: KeyShiftLeft, Alt: true}, - "\x1b\x1b[a": {Type: KeyShiftUp, Alt: true}, // urxvt - "\x1b\x1b[b": {Type: KeyShiftDown, Alt: true}, // urxvt - "\x1b\x1b[c": {Type: KeyShiftRight, Alt: true}, // urxvt - "\x1b\x1b[d": {Type: KeyShiftLeft, Alt: true}, // urxvt - "\x1b[1;5A": {Type: KeyCtrlUp}, - "\x1b[1;5B": {Type: KeyCtrlDown}, - "\x1b[1;5C": {Type: KeyCtrlRight}, - "\x1b[1;5D": {Type: KeyCtrlLeft}, - "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt - "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt - "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt - "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt - "\x1b[1;6A": {Type: KeyCtrlShiftUp}, - "\x1b[1;6B": {Type: KeyCtrlShiftDown}, - "\x1b[1;6C": {Type: KeyCtrlShiftRight}, - "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, - "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, - "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, - "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, - "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, - "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, - "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, - "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, - "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, + "\x1b[A": {Type: KeyUp}, + "\x1b[B": {Type: KeyDown}, + "\x1b[C": {Type: KeyRight}, + "\x1b[D": {Type: KeyLeft}, + "\x1b[1;2A": {Type: KeyShiftUp}, + "\x1b[1;2B": {Type: KeyShiftDown}, + "\x1b[1;2C": {Type: KeyShiftRight}, + "\x1b[1;2D": {Type: KeyShiftLeft}, + "\x1b[OA": {Type: KeyShiftUp}, // DECCKM + "\x1b[OB": {Type: KeyShiftDown}, // DECCKM + "\x1b[OC": {Type: KeyShiftRight}, // DECCKM + "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM + "\x1b[a": {Type: KeyShiftUp}, // urxvt + "\x1b[b": {Type: KeyShiftDown}, // urxvt + "\x1b[c": {Type: KeyShiftRight}, // urxvt + "\x1b[d": {Type: KeyShiftLeft}, // urxvt + "\x1b[1;3A": {Type: KeyUp, Alt: true}, + "\x1b[1;3B": {Type: KeyDown, Alt: true}, + "\x1b[1;3C": {Type: KeyRight, Alt: true}, + "\x1b[1;3D": {Type: KeyLeft, Alt: true}, + + "\x1b[1;4A": {Type: KeyShiftUp, Alt: true}, + "\x1b[1;4B": {Type: KeyShiftDown, Alt: true}, + "\x1b[1;4C": {Type: KeyShiftRight, Alt: true}, + "\x1b[1;4D": {Type: KeyShiftLeft, Alt: true}, + + "\x1b[1;5A": {Type: KeyCtrlUp}, + "\x1b[1;5B": {Type: KeyCtrlDown}, + "\x1b[1;5C": {Type: KeyCtrlRight}, + "\x1b[1;5D": {Type: KeyCtrlLeft}, + "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt + "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt + "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt + "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt + "\x1b[1;6A": {Type: KeyCtrlShiftUp}, + "\x1b[1;6B": {Type: KeyCtrlShiftDown}, + "\x1b[1;6C": {Type: KeyCtrlShiftRight}, + "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, + "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, + "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, + "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, + "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, + "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, + "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, + "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, + "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, // Miscellaneous keys "\x1b[Z": {Type: KeyShiftTab}, - "\x1b[2~": {Type: KeyInsert}, - "\x1b[3;2~": {Type: KeyInsert, Alt: true}, - "\x1b\x1b[2~": {Type: KeyInsert, Alt: true}, // urxvt - - "\x1b[3~": {Type: KeyDelete}, - "\x1b[3;3~": {Type: KeyDelete, Alt: true}, - "\x1b\x1b[3~": {Type: KeyDelete, Alt: true}, // urxvt - - "\x1b[5~": {Type: KeyPgUp}, - "\x1b[5;3~": {Type: KeyPgUp, Alt: true}, - "\x1b\x1b[5~": {Type: KeyPgUp, Alt: true}, // urxvt - "\x1b[5;5~": {Type: KeyCtrlPgUp}, - "\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt - "\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true}, - "\x1b\x1b[5^": {Type: KeyCtrlPgUp, Alt: true}, // urxvt - - "\x1b[6~": {Type: KeyPgDown}, - "\x1b[6;3~": {Type: KeyPgDown, Alt: true}, - "\x1b\x1b[6~": {Type: KeyPgDown, Alt: true}, // urxvt - "\x1b[6;5~": {Type: KeyCtrlPgDown}, - "\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt - "\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true}, - "\x1b\x1b[6^": {Type: KeyCtrlPgDown, Alt: true}, // urxvt + "\x1b[2~": {Type: KeyInsert}, + "\x1b[3;2~": {Type: KeyInsert, Alt: true}, + + "\x1b[3~": {Type: KeyDelete}, + "\x1b[3;3~": {Type: KeyDelete, Alt: true}, + + "\x1b[5~": {Type: KeyPgUp}, + "\x1b[5;3~": {Type: KeyPgUp, Alt: true}, + "\x1b[5;5~": {Type: KeyCtrlPgUp}, + "\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt + "\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true}, + + "\x1b[6~": {Type: KeyPgDown}, + "\x1b[6;3~": {Type: KeyPgDown, Alt: true}, + "\x1b[6;5~": {Type: KeyCtrlPgDown}, + "\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt + "\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true}, "\x1b[1~": {Type: KeyHome}, "\x1b[H": {Type: KeyHome}, // xterm, lxterm @@ -438,23 +426,15 @@ var sequences = map[string]Key{ "\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm "\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm - "\x1b[7~": {Type: KeyHome}, // urxvt - "\x1b\x1b[7~": {Type: KeyHome, Alt: true}, // urxvt - "\x1b[7^": {Type: KeyCtrlHome}, // urxvt - "\x1b\x1b[7^": {Type: KeyCtrlHome, Alt: true}, // urxvt - "\x1b[7$": {Type: KeyShiftHome}, // urxvt - "\x1b\x1b[7$": {Type: KeyShiftHome, Alt: true}, // urxvt - "\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt - "\x1b\x1b[7@": {Type: KeyCtrlShiftHome, Alt: true}, // urxvt - - "\x1b[8~": {Type: KeyEnd}, // urxvt - "\x1b\x1b[8~": {Type: KeyEnd, Alt: true}, // urxvt - "\x1b[8^": {Type: KeyCtrlEnd}, // urxvt - "\x1b\x1b[8^": {Type: KeyCtrlEnd, Alt: true}, // urxvt - "\x1b[8$": {Type: KeyShiftEnd}, // urxvt - "\x1b\x1b[8$": {Type: KeyShiftEnd, Alt: true}, // urxvt - "\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt - "\x1b\x1b[8@": {Type: KeyCtrlShiftEnd, Alt: true}, // urxvt + "\x1b[7~": {Type: KeyHome}, // urxvt + "\x1b[7^": {Type: KeyCtrlHome}, // urxvt + "\x1b[7$": {Type: KeyShiftHome}, // urxvt + "\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt + + "\x1b[8~": {Type: KeyEnd}, // urxvt + "\x1b[8^": {Type: KeyCtrlEnd}, // urxvt + "\x1b[8$": {Type: KeyShiftEnd}, // urxvt + "\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt // Function keys, Linux console "\x1b[[A": {Type: KeyF1}, // linux console @@ -479,29 +459,16 @@ var sequences = map[string]Key{ "\x1b[13~": {Type: KeyF3}, // urxvt "\x1b[14~": {Type: KeyF4}, // urxvt - "\x1b\x1b[11~": {Type: KeyF1, Alt: true}, // urxvt - "\x1b\x1b[12~": {Type: KeyF2, Alt: true}, // urxvt - "\x1b\x1b[13~": {Type: KeyF3, Alt: true}, // urxvt - "\x1b\x1b[14~": {Type: KeyF4, Alt: true}, // urxvt - "\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt "\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt - "\x1b\x1b[15~": {Type: KeyF5, Alt: true}, // urxvt - "\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt "\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt "\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt "\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt "\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt - "\x1b\x1b[17~": {Type: KeyF6, Alt: true}, // urxvt - "\x1b\x1b[18~": {Type: KeyF7, Alt: true}, // urxvt - "\x1b\x1b[19~": {Type: KeyF8, Alt: true}, // urxvt - "\x1b\x1b[20~": {Type: KeyF9, Alt: true}, // urxvt - "\x1b\x1b[21~": {Type: KeyF10, Alt: true}, // urxvt - "\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm "\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm "\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm @@ -514,9 +481,6 @@ var sequences = map[string]Key{ "\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm "\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm - "\x1b\x1b[23~": {Type: KeyF11, Alt: true}, // urxvt - "\x1b\x1b[24~": {Type: KeyF12, Alt: true}, // urxvt - "\x1b[1;2P": {Type: KeyF13}, "\x1b[1;2Q": {Type: KeyF14}, @@ -526,9 +490,6 @@ var sequences = map[string]Key{ "\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm "\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm - "\x1b\x1b[25~": {Type: KeyF13, Alt: true}, // urxvt - "\x1b\x1b[26~": {Type: KeyF14, Alt: true}, // urxvt - "\x1b[1;2R": {Type: KeyF15}, "\x1b[1;2S": {Type: KeyF16}, @@ -538,9 +499,6 @@ var sequences = map[string]Key{ "\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm "\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm - "\x1b\x1b[28~": {Type: KeyF15, Alt: true}, // urxvt - "\x1b\x1b[29~": {Type: KeyF16, Alt: true}, // urxvt - "\x1b[15;2~": {Type: KeyF17}, "\x1b[17;2~": {Type: KeyF18}, "\x1b[18;2~": {Type: KeyF19}, @@ -551,11 +509,6 @@ var sequences = map[string]Key{ "\x1b[33~": {Type: KeyF19}, "\x1b[34~": {Type: KeyF20}, - "\x1b\x1b[31~": {Type: KeyF17, Alt: true}, // urxvt - "\x1b\x1b[32~": {Type: KeyF18, Alt: true}, // urxvt - "\x1b\x1b[33~": {Type: KeyF19, Alt: true}, // urxvt - "\x1b\x1b[34~": {Type: KeyF20, Alt: true}, // urxvt - // Powershell sequences. "\x1bOA": {Type: KeyUp, Alt: false}, "\x1bOB": {Type: KeyDown, Alt: false}, @@ -563,102 +516,171 @@ var sequences = map[string]Key{ "\x1bOD": {Type: KeyLeft, Alt: false}, } -// readInputs reads keypress and mouse inputs from a TTY and returns messages +// unknownInputByteMsg is reported by the input reader when an invalid +// utf-8 byte is detected on the input. Currently, it is not handled +// further by bubbletea. However, having this event makes it possible +// to troubleshoot invalid inputs. +type unknownInputByteMsg byte + +func (u unknownInputByteMsg) String() string { + return fmt.Sprintf("?%#02x?", int(u)) +} + +// unknownCSISequenceMsg is reported by the input reader when an +// unrecognized CSI sequence is detected on the input. Currently, it +// is not handled further by bubbletea. However, having this event +// makes it possible to troubleshoot invalid inputs. +type unknownCSISequenceMsg []byte + +func (u unknownCSISequenceMsg) String() string { + return fmt.Sprintf("?CSI%+v?", []byte(u)[2:]) +} + +var spaceRunes = []rune{' '} + +// readInputs reads keypress and mouse inputs from a TTY and produces messages // containing information about the key or mouse events accordingly. -func readInputs(input io.Reader) ([]Msg, error) { +func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { var buf [256]byte - // Read and block - numBytes, err := input.Read(buf[:]) - if err != nil { - return nil, err - } - b := buf[:numBytes] - b, err = localereader.UTF8(b) - if err != nil { - return nil, err - } + var leftOverFromPrevIteration []byte +loop: + for { + // Read and block. + numBytes, err := input.Read(buf[:]) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + b := buf[:numBytes] + if leftOverFromPrevIteration != nil { + b = append(leftOverFromPrevIteration, b...) + } - // Check if it's a mouse event. For now we're parsing X10-type mouse events - // only. - mouseEvent, err := parseX10MouseEvents(b) - if err == nil { - var m []Msg - for _, v := range mouseEvent { - m = append(m, MouseMsg(v)) + // If we had a short read (numBytes < len(buf)), we're sure that + // the end of this read is an event boundary, so there is no doubt + // if we are encountering the end of the buffer while parsing a message. + // However, if we've succeeded in filling up the buffer, there may + // be more data in the OS buffer ready to be read in, to complete + // the last message in the input. In that case, we will retry with + // the left over data in the next iteration. + canHaveMoreData := numBytes == len(buf) + + var i, w int + for i, w = 0, 07; i < len(b); i += w { + var msg Msg + w, msg = detectOneMsg(b[i:], canHaveMoreData) + if w == 0 { + // Expecting more bytes beyond the current buffer. Try waiting + // for more input. + leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf)) + leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...) + continue loop + } + + select { + case msgs <- msg: + case <-ctx.Done(): + err := ctx.Err() + if err != nil { + err = fmt.Errorf("found context error while reading input: %w", err) + } + return err + } } - return m, nil + leftOverFromPrevIteration = nil } +} - var runeSets [][]rune - var runes []rune +var ( + unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`) + mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) +) - // Translate input into runes. In most cases we'll receive exactly one - // rune, but there are cases, particularly when an input method editor is - // used, where we can receive multiple runes at once. - for i, w := 0, 0; i < len(b); i += w { - r, width := utf8.DecodeRune(b[i:]) - if r == utf8.RuneError { - return nil, errors.New("could not decode rune") +func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) { + // Detect mouse events. + // X10 mouse events have a length of 6 bytes + const mouseEventX10Len = 6 + if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' { + switch b[2] { + case 'M': + return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b)) + case '<': + if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil { + // SGR mouse events length is the length of the match plus the length of the escape sequence + mouseEventSGRLen := matchIndices[1] + 3 + return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b)) + } } + } - if r == '\x1b' && len(runes) > 1 { - // a new key sequence has started - runeSets = append(runeSets, runes) - runes = []rune{} - } + // Detect escape sequence and control characters other than NUL, + // possibly with an escape character in front to mark the Alt + // modifier. + var foundSeq bool + foundSeq, w, msg = detectSequence(b) + if foundSeq { + return + } - runes = append(runes, r) - w = width + // No non-NUL control character or escape sequence. + // If we are seeing at least an escape character, remember it for later below. + alt := false + i := 0 + if b[0] == '\x1b' { + alt = true + i++ } - // add the final set of runes we decoded - runeSets = append(runeSets, runes) - if len(runeSets) == 0 { - return nil, errors.New("received 0 runes from input") + // Are we seeing a standalone NUL? This is not handled by detectSequence(). + if i < len(b) && b[i] == 0 { + return i + 1, KeyMsg{Type: keyNUL, Alt: alt} } - var msgs []Msg - for _, runes := range runeSets { - // Is it a sequence, like an arrow key? - if k, ok := sequences[string(runes)]; ok { - msgs = append(msgs, KeyMsg(k)) - continue + // Find the longest sequence of runes that are not control + // characters from this point. + var runes []rune + for rw := 0; i < len(b); i += rw { + var r rune + r, rw = utf8.DecodeRune(b[i:]) + if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' { + // Rune errors are handled below; control characters and spaces will + // be handled by detectSequence in the next call to detectOneMsg. + break } - - // Is this an unrecognized CSI sequence? If so, ignore it. - if len(runes) > 2 && runes[0] == 0x1b && (runes[1] == '[' || - (len(runes) > 3 && runes[1] == 0x1b && runes[2] == '[')) { - continue + runes = append(runes, r) + if alt { + // We only support a single rune after an escape alt modifier. + i += rw + break } + } + if i >= len(b) && canHaveMoreData { + // We have encountered the end of the input buffer. Alas, we can't + // be sure whether the data in the remainder of the buffer is + // complete (maybe there was a short read). Instead of sending anything + // dumb to the message channel, do a short read. The outer loop will + // handle this case by extending the buffer as necessary. + return 0, nil + } - // Is the alt key pressed? If so, the buffer will be prefixed with an - // escape. - alt := false - if len(runes) > 1 && runes[0] == 0x1b { - alt = true - runes = runes[1:] + // If we found at least one rune, we report the bunch of them as + // a single KeyRunes or KeySpace event. + if len(runes) > 0 { + k := Key{Type: KeyRunes, Runes: runes, Alt: alt} + if len(runes) == 1 && runes[0] == ' ' { + k.Type = KeySpace } + return i, KeyMsg(k) + } - for _, v := range runes { - // Is the first rune a control character? - r := KeyType(v) - if r <= keyUS || r == keyDEL { - msgs = append(msgs, KeyMsg(Key{Type: r, Alt: alt})) - continue - } - - // If it's a space, override the type with KeySpace (but still include - // the rune). - if r == ' ' { - msgs = append(msgs, KeyMsg(Key{Type: KeySpace, Runes: []rune{v}, Alt: alt})) - continue - } - - // Welp, just regular, ol' runes. - msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: []rune{v}, Alt: alt})) - } + // We didn't find an escape sequence, nor a valid rune. Was this a + // lone escape character at the end of the input? + if alt && len(b) == 1 { + return 1, KeyMsg(Key{Type: KeyEscape}) } - return msgs, nil + // The character at the current position is neither an escape + // sequence, a valid rune start or a sole escape character. Report + // it as an invalid byte. + return 1, unknownInputByteMsg(b[0]) } diff --git a/vendor/github.com/charmbracelet/bubbletea/key_sequences.go b/vendor/github.com/charmbracelet/bubbletea/key_sequences.go new file mode 100644 index 0000000..cc200f8 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/key_sequences.go @@ -0,0 +1,71 @@ +package tea + +import "sort" + +// extSequences is used by the map-based algorithm below. It contains +// the sequences plus their alternatives with an escape character +// prefixed, plus the control chars, plus the space. +// It does not contain the NUL character, which is handled specially +// by detectOneMsg. +var extSequences = func() map[string]Key { + s := map[string]Key{} + for seq, key := range sequences { + key := key + s[seq] = key + if !key.Alt { + key.Alt = true + s["\x1b"+seq] = key + } + } + for i := keyNUL + 1; i <= keyDEL; i++ { + if i == keyESC { + continue + } + s[string([]byte{byte(i)})] = Key{Type: i} + s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true} + if i == keyUS { + i = keyDEL - 1 + } + } + s[" "] = Key{Type: KeySpace, Runes: spaceRunes} + s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes} + s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true} + return s +}() + +// seqLengths is the sizes of valid sequences, starting with the +// largest size. +var seqLengths = func() []int { + sizes := map[int]struct{}{} + for seq := range extSequences { + sizes[len(seq)] = struct{}{} + } + lsizes := make([]int, 0, len(sizes)) + for sz := range sizes { + lsizes = append(lsizes, sz) + } + sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] }) + return lsizes +}() + +// detectSequence uses a longest prefix match over the input +// sequence and a hash map. +func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) { + seqs := extSequences + for _, sz := range seqLengths { + if sz > len(input) { + continue + } + prefix := input[:sz] + key, ok := seqs[string(prefix)] + if ok { + return true, sz, KeyMsg(key) + } + } + // Is this an unknown CSI sequence? + if loc := unknownCSIRe.FindIndex(input); loc != nil { + return true, loc[1], unknownCSISequenceMsg(input[:loc[1]]) + } + + return false, 0, nil +} diff --git a/vendor/github.com/charmbracelet/bubbletea/logging.go b/vendor/github.com/charmbracelet/bubbletea/logging.go index 59258d4..a531181 100644 --- a/vendor/github.com/charmbracelet/bubbletea/logging.go +++ b/vendor/github.com/charmbracelet/bubbletea/logging.go @@ -1,6 +1,7 @@ package tea import ( + "fmt" "io" "log" "os" @@ -32,9 +33,9 @@ type LogOptionsSetter interface { // LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter. func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) { - f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd if err != nil { - return nil, err + return nil, fmt.Errorf("error opening file for logging: %w", err) } log.SetOutput(f) diff --git a/vendor/github.com/charmbracelet/bubbletea/mouse.go b/vendor/github.com/charmbracelet/bubbletea/mouse.go index f918d20..add8d02 100644 --- a/vendor/github.com/charmbracelet/bubbletea/mouse.go +++ b/vendor/github.com/charmbracelet/bubbletea/mouse.go @@ -1,23 +1,36 @@ package tea -import ( - "bytes" - "errors" -) +import "strconv" -// MouseMsg contains information about a mouse event and is sent to a program's +// MouseMsg contains information about a mouse event and are sent to a programs // update function when mouse activity occurs. Note that the mouse must first // be enabled in order for the mouse events to be received. type MouseMsg MouseEvent +// String returns a string representation of a mouse event. +func (m MouseMsg) String() string { + return MouseEvent(m).String() +} + // MouseEvent represents a mouse event, which could be a click, a scroll wheel // movement, a cursor movement, or a combination. type MouseEvent struct { - X int - Y int + X int + Y int + Shift bool + Alt bool + Ctrl bool + Action MouseAction + Button MouseButton + + // Deprecated: Use MouseAction & MouseButton instead. Type MouseEventType - Alt bool - Ctrl bool +} + +// IsWheel returns true if the mouse event is a wheel event. +func (m MouseEvent) IsWheel() bool { + return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown || + m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight } // String returns a string representation of a mouse event. @@ -28,122 +41,268 @@ func (m MouseEvent) String() (s string) { if m.Alt { s += "alt+" } - s += mouseEventTypes[m.Type] + if m.Shift { + s += "shift+" + } + + if m.Button == MouseButtonNone { + if m.Action == MouseActionMotion || m.Action == MouseActionRelease { + s += mouseActions[m.Action] + } else { + s += "unknown" + } + } else if m.IsWheel() { + s += mouseButtons[m.Button] + } else { + btn := mouseButtons[m.Button] + if btn != "" { + s += btn + } + act := mouseActions[m.Action] + if act != "" { + s += " " + act + } + } + return s } +// MouseAction represents the action that occurred during a mouse event. +type MouseAction int + +// Mouse event actions. +const ( + MouseActionPress MouseAction = iota + MouseActionRelease + MouseActionMotion +) + +var mouseActions = map[MouseAction]string{ + MouseActionPress: "press", + MouseActionRelease: "release", + MouseActionMotion: "motion", +} + +// MouseButton represents the button that was pressed during a mouse event. +type MouseButton int + +// Mouse event buttons +// +// This is based on X11 mouse button codes. +// +// 1 = left button +// 2 = middle button (pressing the scroll wheel) +// 3 = right button +// 4 = turn scroll wheel up +// 5 = turn scroll wheel down +// 6 = push scroll wheel left +// 7 = push scroll wheel right +// 8 = 4th button (aka browser backward button) +// 9 = 5th button (aka browser forward button) +// 10 +// 11 +// +// Other buttons are not supported. +const ( + MouseButtonNone MouseButton = iota + MouseButtonLeft + MouseButtonMiddle + MouseButtonRight + MouseButtonWheelUp + MouseButtonWheelDown + MouseButtonWheelLeft + MouseButtonWheelRight + MouseButtonBackward + MouseButtonForward + MouseButton10 + MouseButton11 +) + +var mouseButtons = map[MouseButton]string{ + MouseButtonNone: "none", + MouseButtonLeft: "left", + MouseButtonMiddle: "middle", + MouseButtonRight: "right", + MouseButtonWheelUp: "wheel up", + MouseButtonWheelDown: "wheel down", + MouseButtonWheelLeft: "wheel left", + MouseButtonWheelRight: "wheel right", + MouseButtonBackward: "backward", + MouseButtonForward: "forward", + MouseButton10: "button 10", + MouseButton11: "button 11", +} + // MouseEventType indicates the type of mouse event occurring. +// +// Deprecated: Use MouseAction & MouseButton instead. type MouseEventType int // Mouse event types. +// +// Deprecated: Use MouseAction & MouseButton instead. const ( MouseUnknown MouseEventType = iota MouseLeft MouseRight MouseMiddle - MouseRelease + MouseRelease // mouse button release (X10 only) MouseWheelUp MouseWheelDown + MouseWheelLeft + MouseWheelRight + MouseBackward + MouseForward MouseMotion ) -var mouseEventTypes = map[MouseEventType]string{ - MouseUnknown: "unknown", - MouseLeft: "left", - MouseRight: "right", - MouseMiddle: "middle", - MouseRelease: "release", - MouseWheelUp: "wheel up", - MouseWheelDown: "wheel down", - MouseMotion: "motion", +// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events +// look like: +// +// ESC [ < Cb ; Cx ; Cy (M or m) +// +// where: +// +// Cb is the encoded button code +// Cx is the x-coordinate of the mouse +// Cy is the y-coordinate of the mouse +// M is for button press, m is for button release +// +// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates +func parseSGRMouseEvent(buf []byte) MouseEvent { + str := string(buf[3:]) + matches := mouseSGRRegex.FindStringSubmatch(str) + if len(matches) != 5 { + // Unreachable, we already checked the regex in `detectOneMsg`. + panic("invalid mouse event") + } + + b, _ := strconv.Atoi(matches[1]) + px := matches[2] + py := matches[3] + release := matches[4] == "m" + m := parseMouseButton(b, true) + + // Wheel buttons don't have release events + // Motion can be reported as a release event in some terminals (Windows Terminal) + if m.Action != MouseActionMotion && !m.IsWheel() && release { + m.Action = MouseActionRelease + m.Type = MouseRelease + } + + x, _ := strconv.Atoi(px) + y, _ := strconv.Atoi(py) + + // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). + m.X = x - 1 + m.Y = y - 1 + + return m } +const x10MouseByteOffset = 32 + // Parse X10-encoded mouse events; the simplest kind. The last release of X10 -// was December 1986, by the way. +// was December 1986, by the way. The original X10 mouse protocol limits the Cx +// and Cy coordinates to 223 (=255-032). // // X10 mouse events look like: // // ESC [M Cb Cx Cy // // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking -func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) { - var r []MouseEvent +func parseX10MouseEvent(buf []byte) MouseEvent { + v := buf[3:6] + m := parseMouseButton(int(v[0]), false) - seq := []byte("\x1b[M") - if !bytes.Contains(buf, seq) { - return r, errors.New("not an X10 mouse event") + // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). + m.X = int(v[1]) - x10MouseByteOffset - 1 + m.Y = int(v[2]) - x10MouseByteOffset - 1 + + return m +} + +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates +func parseMouseButton(b int, isSGR bool) MouseEvent { + var m MouseEvent + e := b + if !isSGR { + e -= x10MouseByteOffset } - for _, v := range bytes.Split(buf, seq) { - if len(v) == 0 { - continue - } - if len(v) != 3 { - return r, errors.New("not an X10 mouse event") - } + const ( + bitShift = 0b0000_0100 + bitAlt = 0b0000_1000 + bitCtrl = 0b0001_0000 + bitMotion = 0b0010_0000 + bitWheel = 0b0100_0000 + bitAdd = 0b1000_0000 // additional buttons 8-11 - var m MouseEvent - const byteOffset = 32 - e := v[0] - byteOffset - - const ( - bitShift = 0b0000_0100 - bitAlt = 0b0000_1000 - bitCtrl = 0b0001_0000 - bitMotion = 0b0010_0000 - bitWheel = 0b0100_0000 - - bitsMask = 0b0000_0011 - - bitsLeft = 0b0000_0000 - bitsMiddle = 0b0000_0001 - bitsRight = 0b0000_0010 - bitsRelease = 0b0000_0011 - - bitsWheelUp = 0b0000_0000 - bitsWheelDown = 0b0000_0001 - ) - - if e&bitWheel != 0 { - // Check the low two bits. - switch e & bitsMask { - case bitsWheelUp: - m.Type = MouseWheelUp - case bitsWheelDown: - m.Type = MouseWheelDown - } - } else { - // Check the low two bits. - // We do not separate clicking and dragging. - switch e & bitsMask { - case bitsLeft: - m.Type = MouseLeft - case bitsMiddle: - m.Type = MouseMiddle - case bitsRight: - m.Type = MouseRight - case bitsRelease: - if e&bitMotion != 0 { - m.Type = MouseMotion - } else { - m.Type = MouseRelease - } - } - } + bitsMask = 0b0000_0011 + ) - if e&bitAlt != 0 { - m.Alt = true - } - if e&bitCtrl != 0 { - m.Ctrl = true + if e&bitAdd != 0 { + m.Button = MouseButtonBackward + MouseButton(e&bitsMask) + } else if e&bitWheel != 0 { + m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask) + } else { + m.Button = MouseButtonLeft + MouseButton(e&bitsMask) + // X10 reports a button release as 0b0000_0011 (3) + if e&bitsMask == bitsMask { + m.Action = MouseActionRelease + m.Button = MouseButtonNone } + } - // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - m.X = int(v[1]) - byteOffset - 1 - m.Y = int(v[2]) - byteOffset - 1 + // Motion bit doesn't get reported for wheel events. + if e&bitMotion != 0 && !m.IsWheel() { + m.Action = MouseActionMotion + } + + // Modifiers + m.Alt = e&bitAlt != 0 + m.Ctrl = e&bitCtrl != 0 + m.Shift = e&bitShift != 0 - r = append(r, m) + // backward compatibility + switch { + case m.Button == MouseButtonLeft && m.Action == MouseActionPress: + m.Type = MouseLeft + case m.Button == MouseButtonMiddle && m.Action == MouseActionPress: + m.Type = MouseMiddle + case m.Button == MouseButtonRight && m.Action == MouseActionPress: + m.Type = MouseRight + case m.Button == MouseButtonNone && m.Action == MouseActionRelease: + m.Type = MouseRelease + case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress: + m.Type = MouseWheelUp + case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress: + m.Type = MouseWheelDown + case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress: + m.Type = MouseWheelLeft + case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress: + m.Type = MouseWheelRight + case m.Button == MouseButtonBackward && m.Action == MouseActionPress: + m.Type = MouseBackward + case m.Button == MouseButtonForward && m.Action == MouseActionPress: + m.Type = MouseForward + case m.Action == MouseActionMotion: + m.Type = MouseMotion + switch m.Button { + case MouseButtonLeft: + m.Type = MouseLeft + case MouseButtonMiddle: + m.Type = MouseMiddle + case MouseButtonRight: + m.Type = MouseRight + case MouseButtonBackward: + m.Type = MouseBackward + case MouseButtonForward: + m.Type = MouseForward + } + default: + m.Type = MouseUnknown } - return r, nil + return m } diff --git a/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go b/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go index f5637aa..1b1d440 100644 --- a/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go +++ b/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go @@ -17,3 +17,5 @@ func (n nilRenderer) enableMouseCellMotion() {} func (n nilRenderer) disableMouseCellMotion() {} func (n nilRenderer) enableMouseAllMotion() {} func (n nilRenderer) disableMouseAllMotion() {} +func (n nilRenderer) enableMouseSGRMode() {} +func (n nilRenderer) disableMouseSGRMode() {} diff --git a/vendor/github.com/charmbracelet/bubbletea/options.go b/vendor/github.com/charmbracelet/bubbletea/options.go index 30f7e6c..71e9449 100644 --- a/vendor/github.com/charmbracelet/bubbletea/options.go +++ b/vendor/github.com/charmbracelet/bubbletea/options.go @@ -3,6 +3,7 @@ package tea import ( "context" "io" + "sync/atomic" "github.com/muesli/termenv" ) @@ -76,7 +77,7 @@ func WithoutCatchPanics() ProgramOption { // This is mainly useful for testing. func WithoutSignals() ProgramOption { return func(p *Program) { - p.ignoreSignals = true + atomic.StoreUint32(&p.ignoreSignals, 1) } } @@ -107,6 +108,9 @@ func WithAltScreen() ProgramOption { // movement events are also captured if a mouse button is pressed (i.e., drag // events). Cell motion mode is better supported than all motion mode. // +// This will try to enable the mouse in extended mode (SGR), if that is not +// supported by the terminal it will fall back to normal mode (X10). +// // To enable mouse cell motion once the program has already started running use // the EnableMouseCellMotion command. To disable the mouse when the program is // running use the DisableMouse command. @@ -126,6 +130,9 @@ func WithMouseCellMotion() ProgramOption { // wheel, and motion events, which are delivered regardless of whether a mouse // button is pressed, effectively enabling support for hover interactions. // +// This will try to enable the mouse in extended mode (SGR), if that is not +// supported by the terminal it will fall back to normal mode (X10). +// // Many modern terminals support this, but not all. If in doubt, use // EnableMouseCellMotion instead. // @@ -200,3 +207,12 @@ func WithFilter(filter func(Model, Msg) Msg) ProgramOption { p.filter = filter } } + +// WithFPS sets a custom maximum FPS at which the renderer should run. If +// less than 1, the default value of 60 will be used. If over 120, the FPS +// will be capped at 120. +func WithFPS(fps int) ProgramOption { + return func(p *Program) { + p.fps = fps + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/renderer.go b/vendor/github.com/charmbracelet/bubbletea/renderer.go index a6f4162..5a3ee3c 100644 --- a/vendor/github.com/charmbracelet/bubbletea/renderer.go +++ b/vendor/github.com/charmbracelet/bubbletea/renderer.go @@ -40,16 +40,22 @@ type renderer interface { // events if a mouse button is pressed (i.e., drag events). enableMouseCellMotion() - // DisableMouseCellMotion disables Mouse Cell Motion tracking. + // disableMouseCellMotion disables Mouse Cell Motion tracking. disableMouseCellMotion() - // EnableMouseAllMotion enables mouse click, release, wheel and motion + // enableMouseAllMotion enables mouse click, release, wheel and motion // events, regardless of whether a mouse button is pressed. Many modern // terminals support this, but not all. enableMouseAllMotion() - // DisableMouseAllMotion disables All Motion mouse tracking. + // disableMouseAllMotion disables All Motion mouse tracking. disableMouseAllMotion() + + // enableMouseSGRMode enables mouse extended mode (SGR). + enableMouseSGRMode() + + // disableMouseSGRMode disables mouse extended mode (SGR). + disableMouseSGRMode() } // repaintMsg forces a full repaint. diff --git a/vendor/github.com/charmbracelet/bubbletea/screen.go b/vendor/github.com/charmbracelet/bubbletea/screen.go index 899db3d..d064222 100644 --- a/vendor/github.com/charmbracelet/bubbletea/screen.go +++ b/vendor/github.com/charmbracelet/bubbletea/screen.go @@ -167,3 +167,8 @@ func (p *Program) EnableMouseAllMotion() { func (p *Program) DisableMouseAllMotion() { p.renderer.disableMouseAllMotion() } + +// SetWindowTitle sets the terminal window title. +func (p *Program) SetWindowTitle(title string) { + p.output.SetWindowTitle(title) +} diff --git a/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go b/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go index 0ab9473..1573a1c 100644 --- a/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go +++ b/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go @@ -16,7 +16,8 @@ import ( const ( // defaultFramerate specifies the maximum interval at which we should // update the view. - defaultFramerate = time.Second / 60 + defaultFPS = 60 + maxFPS = 120 ) // standardRenderer is a framerate-based terminal renderer, updating the view @@ -54,12 +55,17 @@ type standardRenderer struct { // newRenderer creates a new renderer. Normally you'll want to initialize it // with os.Stdout as the first argument. -func newRenderer(out *termenv.Output, useANSICompressor bool) renderer { +func newRenderer(out *termenv.Output, useANSICompressor bool, fps int) renderer { + if fps < 1 { + fps = defaultFPS + } else if fps > maxFPS { + fps = maxFPS + } r := &standardRenderer{ out: out, mtx: &sync.Mutex{}, done: make(chan struct{}), - framerate: defaultFramerate, + framerate: time.Second / time.Duration(fps), useANSICompressor: useANSICompressor, queuedMessageLines: []string{}, } @@ -202,10 +208,8 @@ func (r *standardRenderer) flush() { // Merge the set of lines we're skipping as a rendering optimization with // the set of lines we've explicitly asked the renderer to ignore. - if r.ignoreLines != nil { - for k, v := range r.ignoreLines { - skipLines[k] = v - } + for k, v := range r.ignoreLines { + skipLines[k] = v } // Paint new lines @@ -392,6 +396,20 @@ func (r *standardRenderer) disableMouseAllMotion() { r.out.DisableMouseAllMotion() } +func (r *standardRenderer) enableMouseSGRMode() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.out.EnableMouseExtendedMode() +} + +func (r *standardRenderer) disableMouseSGRMode() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.out.DisableMouseExtendedMode() +} + // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea // renderer. func (r *standardRenderer) setIgnoredLines(from int, to int) { diff --git a/vendor/github.com/charmbracelet/bubbletea/tea.go b/vendor/github.com/charmbracelet/bubbletea/tea.go index 2e024ed..f18cb87 100644 --- a/vendor/github.com/charmbracelet/bubbletea/tea.go +++ b/vendor/github.com/charmbracelet/bubbletea/tea.go @@ -18,6 +18,7 @@ import ( "os/signal" "runtime/debug" "sync" + "sync/atomic" "syscall" "github.com/containerd/console" @@ -58,8 +59,6 @@ type Model interface { // update function. type Cmd func() Msg -type handlers []chan struct{} - type inputType int const ( @@ -102,6 +101,29 @@ const ( withoutCatchPanics ) +// handlers manages series of channels returned by various processes. It allows +// us to wait for those processes to terminate before exiting the program. +type handlers []chan struct{} + +// Adds a channel to the list of handlers. We wait for all handlers to terminate +// gracefully on shutdown. +func (h *handlers) add(ch chan struct{}) { + *h = append(*h, ch) +} + +// shutdown waits for all handlers to terminate. +func (h handlers) shutdown() { + var wg sync.WaitGroup + for _, ch := range h { + wg.Add(1) + go func(ch chan struct{}) { + <-ch + wg.Done() + }(ch) + } + wg.Wait() +} + // Program is a terminal user interface. type Program struct { initialModel Model @@ -132,7 +154,7 @@ type Program struct { // was the altscreen active before releasing the terminal? altScreenWasActive bool - ignoreSignals bool + ignoreSignals uint32 // Stores the original reference to stdin for cases where input is not a // TTY on windows and we've automatically opened CONIN$ to receive input. @@ -144,6 +166,10 @@ type Program struct { windowsStdin *os.File //nolint:golint,structcheck,unused filter func(Model, Msg) Msg + + // fps is the frames per second we should set on the renderer, if + // applicable, + fps int } // Quit is a special command that tells the Bubble Tea program to exit. @@ -213,7 +239,7 @@ func (p *Program) handleSignals() chan struct{} { return case <-sig: - if !p.ignoreSignals { + if atomic.LoadUint32(&p.ignoreSignals) == 0 { p.msgs <- QuitMsg{} return } @@ -275,6 +301,12 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { return ch } +func (p *Program) disableMouse() { + p.renderer.disableMouseCellMotion() + p.renderer.disableMouseAllMotion() + p.renderer.disableMouseSGRMode() +} + // eventLoop is the central message loop. It receives and handles the default // Bubble Tea messages, update the model and triggers redraws. func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { @@ -309,15 +341,18 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case exitAltScreenMsg: p.renderer.exitAltScreen() - case enableMouseCellMotionMsg: - p.renderer.enableMouseCellMotion() - - case enableMouseAllMotionMsg: - p.renderer.enableMouseAllMotion() + case enableMouseCellMotionMsg, enableMouseAllMotionMsg: + switch msg.(type) { + case enableMouseCellMotionMsg: + p.renderer.enableMouseCellMotion() + case enableMouseAllMotionMsg: + p.renderer.enableMouseAllMotion() + } + // mouse mode (1006) is a no-op if the terminal doesn't support it. + p.renderer.enableMouseSGRMode() case disableMouseMsg: - p.renderer.disableMouseCellMotion() - p.renderer.disableMouseAllMotion() + p.disableMouse() case showCursorMsg: p.renderer.showCursor() @@ -362,6 +397,9 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { p.Send(msg) } }() + + case setWindowTitleMsg: + p.SetWindowTitle(string(msg)) } // Process internal messages for the renderer. @@ -445,7 +483,7 @@ func (p *Program) Run() (Model, error) { // If no renderer is set use the standard one. if p.renderer == nil { - p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor)) + p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps) } // Check if output is a TTY before entering raw mode, hiding the cursor and @@ -460,8 +498,10 @@ func (p *Program) Run() (Model, error) { } if p.startupOptions&withMouseCellMotion != 0 { p.renderer.enableMouseCellMotion() + p.renderer.enableMouseSGRMode() } else if p.startupOptions&withMouseAllMotion != 0 { p.renderer.enableMouseAllMotion() + p.renderer.enableMouseSGRMode() } // Initialize the program. @@ -607,7 +647,7 @@ func (p *Program) shutdown(kill bool) { // ReleaseTerminal restores the original terminal state and cancels the input // reader. You can return control to the Program with RestoreTerminal. func (p *Program) ReleaseTerminal() error { - p.ignoreSignals = true + atomic.StoreUint32(&p.ignoreSignals, 1) p.cancelReader.Cancel() p.waitForReadLoop() @@ -623,7 +663,7 @@ func (p *Program) ReleaseTerminal() error { // terminal to the former state when the program was running, and repaints. // Use it to reinitialize a Program after running ReleaseTerminal. func (p *Program) RestoreTerminal() error { - p.ignoreSignals = false + atomic.StoreUint32(&p.ignoreSignals, 0) if err := p.initTerminal(); err != nil { return err @@ -674,22 +714,3 @@ func (p *Program) Printf(template string, args ...interface{}) { messageBody: fmt.Sprintf(template, args...), } } - -// Adds a handler to the list of handlers. We wait for all handlers to terminate -// gracefully on shutdown. -func (h *handlers) add(ch chan struct{}) { - *h = append(*h, ch) -} - -// Shutdown waits for all handlers to terminate. -func (h handlers) shutdown() { - var wg sync.WaitGroup - for _, ch := range h { - wg.Add(1) - go func(ch chan struct{}) { - <-ch - wg.Done() - }(ch) - } - wg.Wait() -} diff --git a/vendor/github.com/charmbracelet/bubbletea/tty.go b/vendor/github.com/charmbracelet/bubbletea/tty.go index 3ab6639..01f084d 100644 --- a/vendor/github.com/charmbracelet/bubbletea/tty.go +++ b/vendor/github.com/charmbracelet/bubbletea/tty.go @@ -2,11 +2,13 @@ package tea import ( "errors" + "fmt" "io" "os" "time" isatty "github.com/mattn/go-isatty" + localereader "github.com/mattn/go-localereader" "github.com/muesli/cancelreader" "golang.org/x/term" ) @@ -20,7 +22,7 @@ func (p *Program) initTerminal() error { if p.console != nil { err = p.console.SetRaw() if err != nil { - return err + return fmt.Errorf("error entering raw mode: %w", err) } } @@ -33,21 +35,20 @@ func (p *Program) initTerminal() error { func (p *Program) restoreTerminalState() error { if p.renderer != nil { p.renderer.showCursor() - p.renderer.disableMouseCellMotion() - p.renderer.disableMouseAllMotion() + p.disableMouse() if p.renderer.altScreen() { p.renderer.exitAltScreen() // give the terminal a moment to catch up - time.Sleep(time.Millisecond * 10) + time.Sleep(time.Millisecond * 10) //nolint:gomnd } } if p.console != nil { err := p.console.Reset() if err != nil { - return err + return fmt.Errorf("error restoring terminal state: %w", err) } } @@ -59,7 +60,7 @@ func (p *Program) initCancelReader() error { var err error p.cancelReader, err = cancelreader.NewReader(p.input) if err != nil { - return err + return fmt.Errorf("error creating cancelreader: %w", err) } p.readLoopDone = make(chan struct{}) @@ -71,25 +72,12 @@ func (p *Program) initCancelReader() error { func (p *Program) readLoop() { defer close(p.readLoopDone) - for { - if p.ctx.Err() != nil { - return - } - - msgs, err := readInputs(p.cancelReader) - if err != nil { - if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { - select { - case <-p.ctx.Done(): - case p.errs <- err: - } - } - - return - } - - for _, msg := range msgs { - p.msgs <- msg + input := localereader.NewReader(p.cancelReader) + err := readInputs(p.ctx, p.msgs, input) + if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { + select { + case <-p.ctx.Done(): + case p.errs <- err: } } } @@ -98,7 +86,7 @@ func (p *Program) readLoop() { func (p *Program) waitForReadLoop() { select { case <-p.readLoopDone: - case <-time.After(500 * time.Millisecond): + case <-time.After(500 * time.Millisecond): //nolint:gomnd // The read loop hangs, which means the input // cancelReader's cancel function has returned true even // though it was not able to cancel the read. diff --git a/vendor/github.com/charmbracelet/bubbletea/tty_unix.go b/vendor/github.com/charmbracelet/bubbletea/tty_unix.go index b6f5ffd..a3a25b8 100644 --- a/vendor/github.com/charmbracelet/bubbletea/tty_unix.go +++ b/vendor/github.com/charmbracelet/bubbletea/tty_unix.go @@ -4,6 +4,7 @@ package tea import ( + "fmt" "os" "github.com/containerd/console" @@ -28,7 +29,9 @@ func (p *Program) initInput() error { // program exits. func (p *Program) restoreInput() error { if p.console != nil { - return p.console.Reset() + if err := p.console.Reset(); err != nil { + return fmt.Errorf("error restoring console: %w", err) + } } return nil } @@ -36,7 +39,7 @@ func (p *Program) restoreInput() error { func openInputTTY() (*os.File, error) { f, err := os.Open("/dev/tty") if err != nil { - return nil, err + return nil, fmt.Errorf("could not open a new TTY: %w", err) } return f, nil } diff --git a/vendor/github.com/sahilm/fuzzy/.travis.yml b/vendor/github.com/sahilm/fuzzy/.travis.yml index 6756d80..f77acde 100644 --- a/vendor/github.com/sahilm/fuzzy/.travis.yml +++ b/vendor/github.com/sahilm/fuzzy/.travis.yml @@ -1,3 +1,6 @@ +arch: + - amd64 + - ppc64le language: go go: - 1.x diff --git a/vendor/github.com/sahilm/fuzzy/README.md b/vendor/github.com/sahilm/fuzzy/README.md index c632da5..ea7bf22 100644 --- a/vendor/github.com/sahilm/fuzzy/README.md +++ b/vendor/github.com/sahilm/fuzzy/README.md @@ -17,7 +17,7 @@ VSCode, IntelliJ IDEA et al. This library is external dependency-free. It only d - 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. +- The positions of matches are returned. Allows you to highlight matching characters. - Unicode aware. @@ -76,7 +76,7 @@ func contains(needle int, haystack []int) bool { return false } ``` -If the data you want to match isn't a slice of strings, you can use `FindFromSource` by implementing +If the data you want to match isn't a slice of strings, you can use `FindFrom` by implementing the provided `Source` interface. Here's an example: ```go @@ -119,7 +119,9 @@ func main() { }, } results := fuzzy.FindFrom("al", emps) - fmt.Println(results) + for _, r := range results { + fmt.Println(emps[r.Index]) + } } ``` diff --git a/vendor/github.com/sahilm/fuzzy/fuzzy.go b/vendor/github.com/sahilm/fuzzy/fuzzy.go index bd66ee6..5125821 100644 --- a/vendor/github.com/sahilm/fuzzy/fuzzy.go +++ b/vendor/github.com/sahilm/fuzzy/fuzzy.go @@ -76,16 +76,36 @@ The following types of matches apply a bonus: Penalties are applied for every character in the search string that wasn't matched and all leading characters upto the first match. + +Results are sorted by best match. */ func Find(pattern string, data []string) Matches { return FindFrom(pattern, stringSource(data)) } +/* +FindNoSort is an alternative Find implementation that does not sort +the results in the end. +*/ +func FindNoSort(pattern string, data []string) Matches { + return FindFromNoSort(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 { + matches := FindFromNoSort(pattern, data) + sort.Stable(matches) + return matches +} + +/* +FindFromNoSort is an alternative FindFrom implementation that does +not sort results in the end. +*/ +func FindFromNoSort(pattern string, data Source) Matches { if len(pattern) == 0 { return nil } @@ -181,7 +201,6 @@ func FindFrom(pattern string, data Source) Matches { matchedIndexes = match.MatchedIndexes[:0] // Recycle match index slice } } - sort.Stable(matches) return matches } diff --git a/vendor/modules.txt b/vendor/modules.txt index 9d7c2c7..852ebf9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,7 +4,7 @@ github.com/atotto/clipboard # github.com/aymanbagabas/go-osc52/v2 v2.0.1 ## explicit; go 1.16 github.com/aymanbagabas/go-osc52/v2 -# github.com/charmbracelet/bubbles v0.16.1 +# github.com/charmbracelet/bubbles v0.17.1 ## explicit; go 1.17 github.com/charmbracelet/bubbles/cursor github.com/charmbracelet/bubbles/help @@ -15,7 +15,7 @@ github.com/charmbracelet/bubbles/runeutil github.com/charmbracelet/bubbles/spinner github.com/charmbracelet/bubbles/textinput github.com/charmbracelet/bubbles/viewport -# github.com/charmbracelet/bubbletea v0.24.2 +# github.com/charmbracelet/bubbletea v0.25.0 ## explicit; go 1.17 github.com/charmbracelet/bubbletea # github.com/charmbracelet/lipgloss v0.9.1 @@ -55,7 +55,7 @@ github.com/muesli/termenv # github.com/rivo/uniseg v0.4.3 ## explicit; go 1.18 github.com/rivo/uniseg -# github.com/sahilm/fuzzy v0.1.0 +# github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f ## explicit github.com/sahilm/fuzzy # golang.org/x/sync v0.1.0