From b0314187c72bcbdca97a676594d3c13aa18d9581 Mon Sep 17 00:00:00 2001 From: cpendery Date: Sat, 9 Sep 2023 01:28:40 -0400 Subject: [PATCH] fix: update ui & add mock completion Signed-off-by: cpendery --- autocomplete/completion.go | 11 +++- go.mod | 6 +- ui/suggestions/suggestions.go | 121 ++++++++++++++++++++++++++-------- ui/ui.go | 21 ++++-- ui/utils/utils.go | 14 ++++ 5 files changed, 134 insertions(+), 39 deletions(-) diff --git a/autocomplete/completion.go b/autocomplete/completion.go index 2a4d22b..5d01a5c 100644 --- a/autocomplete/completion.go +++ b/autocomplete/completion.go @@ -2,11 +2,16 @@ package autocomplete import "strings" -func LoadSuggestions(cmd string) []string { +type Suggestion struct { + Name string + Description string +} + +func LoadSuggestions(cmd string) []Suggestion { params := strings.Split(cmd, " ") lastParam := params[len(params)-1] if lastParam == "git" { - return []string{"go", "get"} + return []Suggestion{{"go", "the only way to go"}, {"get", "the right way to get"}, {"gone", "here today, bye tomorrow"}} } - return []string{} + return []Suggestion{} } diff --git a/go.mod b/go.mod index 658eb07..edc6cd8 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/lipgloss v0.8.0 + github.com/mattn/go-runewidth v0.0.14 + github.com/muesli/reflow v0.3.0 + golang.org/x/term v0.6.0 ) require ( @@ -15,14 +18,11 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.7.0 // indirect - golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/ui/suggestions/suggestions.go b/ui/suggestions/suggestions.go index 64c4c8c..d863439 100644 --- a/ui/suggestions/suggestions.go +++ b/ui/suggestions/suggestions.go @@ -1,25 +1,33 @@ package suggestions import ( - "log" + "runtime" "strings" + "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/cpendery/clac/autocomplete" "github.com/cpendery/clac/ui/utils" + "github.com/mattn/go-runewidth" + "github.com/muesli/reflow/wordwrap" + "github.com/muesli/reflow/wrap" ) type Model struct { - cursor int - suggestions []string - keyMap KeyMap + cursor int + userInputCursorLocation int + suggestions []autocomplete.Suggestion + keyMap KeyMap + windowWidth int + windowHeight int } const ( - SuggestionWidth = 50 - BorderOffset = 2 + SuggestionWidth = 40 + DescriptionWidth = 30 + BorderOffset = 2 ) type KeyMap struct { @@ -45,9 +53,8 @@ func New() Model { ), } return Model{ - cursor: 0, - suggestions: []string{"div", "table"}, - keyMap: keyBindings, + cursor: 0, + keyMap: keyBindings, } } @@ -59,31 +66,75 @@ func (m *Model) cursorDown() { m.cursor = utils.Clamp(m.cursor+1, 0, len(m.suggestions)-1) } -func (m Model) ActiveSuggestion() string { - return m.suggestions[m.cursor] +func (m Model) HasActiveSuggestion() bool { + return len(m.suggestions) > 0 } -func (m Model) Update(msg tea.Msg, command string) Model { +func (m Model) ActiveSuggestion() string { + return m.suggestions[m.cursor].Name +} + +func (m Model) Update(msg tea.Msg, command string, userInputCursorLocation int) Model { switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, m.keyMap.LineUp): - log.Println("up") m.cursorUp() case key.Matches(msg, m.keyMap.LineDown): - log.Println("down") m.cursorDown() } + case cursor.BlinkMsg: + if runtime.GOOS == "windows" { + m.windowWidth, m.windowHeight = utils.GetWindowSize() + } + case tea.WindowSizeMsg: + m.windowHeight = msg.Height + m.windowWidth = msg.Width } + m.userInputCursorLocation = userInputCursorLocation m.suggestions = autocomplete.LoadSuggestions(command) return m } -func (m Model) renderSuggestion(suggestion string, position int) string { +func (m Model) renderSuggestion(suggestion string, position int, width int) string { + content := wordTrunc(suggestion, width) if position == m.cursor { - return lipgloss.NewStyle().Background(lipgloss.Color("#7D56F4")).Width(SuggestionWidth).Render(suggestion) + return lipgloss.NewStyle().Background(lipgloss.Color("#7D56F4")).Width(width).Render(content) } - return lipgloss.NewStyle().Render(suggestion) + return lipgloss.NewStyle().Render(content) +} + +func wordWrap(content string, width int) string { + return wrap.String(wordwrap.String(content, width), width) +} + +func wordTrunc(content string, width int) string { + return runewidth.Truncate(content, width, "…") +} + +func (m Model) renderSuggestions(width int) string { + var style = lipgloss.NewStyle(). + Bold(true). + Width(width). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + + r := make([]string, len(m.suggestions)) + for idx, suggestion := range m.suggestions { + r[idx] = m.renderSuggestion(suggestion.Name, idx, width) + } + + return style.Render(strings.Join(r, "\n")) +} + +func (m Model) renderDescription(width int) string { + var style = lipgloss.NewStyle(). + Bold(true). + Width(width). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + + return style.Render(wordWrap(m.suggestions[m.cursor].Description, width)) } func (m Model) View() string { @@ -91,16 +142,32 @@ func (m Model) View() string { return "" } - var style = lipgloss.NewStyle(). - Bold(true). - Width(SuggestionWidth). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("63")) + suggestionWidth := min(m.windowWidth-BorderOffset, SuggestionWidth) + descriptionWidth := min(m.windowWidth-BorderOffset, DescriptionWidth) - r := make([]string, len(m.suggestions)) - for idx, suggestion := range m.suggestions { - r[idx] = m.renderSuggestion(suggestion, idx) + suggestionsView := m.renderSuggestions(suggestionWidth) + descriptionView := m.renderDescription(descriptionWidth) + + maxLeftPadding := m.windowWidth - suggestionWidth - BorderOffset + maxLeftPaddingWhenJoined := maxLeftPadding - BorderOffset - descriptionWidth + showDescriptionRight := m.userInputCursorLocation+suggestionWidth+descriptionWidth+BorderOffset*2 <= m.windowWidth + showDescriptionLeft := !showDescriptionRight && (suggestionWidth+descriptionWidth+BorderOffset*2 <= m.windowWidth) + + if showDescriptionRight { + return lipgloss. + NewStyle(). + MarginLeft(utils.Clamp(m.userInputCursorLocation, 0, maxLeftPaddingWhenJoined)). + Render(lipgloss.JoinHorizontal(lipgloss.Top, suggestionsView, descriptionView)) + + } else if showDescriptionLeft { + return lipgloss. + NewStyle(). + MarginLeft(utils.Clamp(m.userInputCursorLocation-DescriptionWidth-BorderOffset, 0, maxLeftPaddingWhenJoined)). + Render(lipgloss.JoinHorizontal(lipgloss.Top, descriptionView, suggestionsView)) + } else { + return lipgloss. + NewStyle(). + MarginLeft(utils.Clamp(m.userInputCursorLocation, 0, maxLeftPadding)). + Render(lipgloss.JoinVertical(lipgloss.Left, suggestionsView, descriptionView)) } - - return style.Render(strings.Join(r, "\n")) } diff --git a/ui/ui.go b/ui/ui.go index 230cfb2..49d541a 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -2,7 +2,9 @@ package ui import ( "fmt" + "runtime" + "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -44,12 +46,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit case tea.KeyTab: + if !m.suggestions.HasActiveSuggestion() { + return m, nil + } activeSuggestion := m.suggestions.ActiveSuggestion() s := m.textInput.Value() + activeSuggestion m.textInput.SetValue(s) m.textInput.SetCursor(len(s)) return m, nil } + case cursor.BlinkMsg: + if runtime.GOOS == "windows" { + m.windowWidth, m.windowHeight = utils.GetWindowSize() + m.textInput.Width = m.windowWidth - widthOffset + } case tea.WindowSizeMsg: m.windowHeight = msg.Height m.windowWidth = msg.Width @@ -57,19 +67,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } var cmd tea.Cmd - m.suggestions = m.suggestions.Update(msg, m.textInput.Value()) + m.textInput, cmd = m.textInput.Update(msg) + cursorLocation := len(m.textInput.Value()) + len(m.textInput.Prompt) + m.suggestions = m.suggestions.Update(msg, m.textInput.Value(), cursorLocation) + return m, cmd } func (m model) View() string { - suggestionsSpacing := lipgloss.NewStyle().MarginLeft( - utils.Clamp(len(m.textInput.Value())+len(m.textInput.Prompt), 0, m.windowWidth-suggestions.SuggestionWidth-suggestions.BorderOffset), - ) - return fmt.Sprintf( "%s\n%s", m.textInput.View(), - suggestionsSpacing.Render(m.suggestions.View()), + m.suggestions.View(), ) } diff --git a/ui/utils/utils.go b/ui/utils/utils.go index afcdb0b..5783831 100644 --- a/ui/utils/utils.go +++ b/ui/utils/utils.go @@ -1,8 +1,22 @@ package utils +import ( + "os" + + "golang.org/x/term" +) + func Clamp(v, low, high int) int { if high < low { low, high = high, low } return min(high, max(low, v)) } + +func GetWindowSize() (width int, height int) { + fd := int(os.Stdout.Fd()) + if term.IsTerminal(fd) { + width, height, _ = term.GetSize(fd) + } + return +}