diff --git a/autocomplete/completion.go b/autocomplete/completion.go new file mode 100644 index 0000000..2a4d22b --- /dev/null +++ b/autocomplete/completion.go @@ -0,0 +1,12 @@ +package autocomplete + +import "strings" + +func LoadSuggestions(cmd string) []string { + params := strings.Split(cmd, " ") + lastParam := params[len(params)-1] + if lastParam == "git" { + return []string{"go", "get"} + } + return []string{} +} diff --git a/main.go b/main.go index 9f47cd4..6ee8443 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,21 @@ package main import ( + "fmt" "log" + "os" tea "github.com/charmbracelet/bubbletea" "github.com/cpendery/clac/ui" ) func main() { + f, err := tea.LogToFile("debug.log", "debug") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + defer f.Close() p := tea.NewProgram(ui.Start()) if _, err := p.Run(); err != nil { log.Fatal(err) diff --git a/ui/suggestions/suggestions.go b/ui/suggestions/suggestions.go new file mode 100644 index 0000000..64c4c8c --- /dev/null +++ b/ui/suggestions/suggestions.go @@ -0,0 +1,106 @@ +package suggestions + +import ( + "log" + "strings" + + "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" +) + +type Model struct { + cursor int + suggestions []string + keyMap KeyMap +} + +const ( + SuggestionWidth = 50 + BorderOffset = 2 +) + +type KeyMap struct { + LineUp key.Binding + LineDown key.Binding + PageUp key.Binding + PageDown key.Binding +} + +func New() Model { + keyBindings := KeyMap{ + LineUp: key.NewBinding( + key.WithKeys("up"), + ), + LineDown: key.NewBinding( + key.WithKeys("down"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup"), + ), + PageDown: key.NewBinding( + key.WithKeys("pgdown"), + ), + } + return Model{ + cursor: 0, + suggestions: []string{"div", "table"}, + keyMap: keyBindings, + } +} + +func (m *Model) cursorUp() { + m.cursor = utils.Clamp(m.cursor-1, 0, len(m.suggestions)-1) +} + +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) Update(msg tea.Msg, command string) 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() + } + } + m.suggestions = autocomplete.LoadSuggestions(command) + return m +} + +func (m Model) renderSuggestion(suggestion string, position int) string { + if position == m.cursor { + return lipgloss.NewStyle().Background(lipgloss.Color("#7D56F4")).Width(SuggestionWidth).Render(suggestion) + } + return lipgloss.NewStyle().Render(suggestion) +} + +func (m Model) View() string { + if len(m.suggestions) == 0 { + return "" + } + + var style = lipgloss.NewStyle(). + Bold(true). + Width(SuggestionWidth). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + + r := make([]string, len(m.suggestions)) + for idx, suggestion := range m.suggestions { + r[idx] = m.renderSuggestion(suggestion, idx) + } + + return style.Render(strings.Join(r, "\n")) +} diff --git a/ui/ui.go b/ui/ui.go index 16366e6..230cfb2 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -6,21 +6,31 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/cpendery/clac/ui/suggestions" + "github.com/cpendery/clac/ui/utils" ) - - type model struct { textInput textinput.Model + suggestions suggestions.Model windowHeight int windowWidth int } +const ( + prompt = "> " + promptOffset = len(prompt) + cursorOffset = 1 + widthOffset = promptOffset + cursorOffset +) + func Start() model { ti := textinput.New() ti.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) ti.Focus() - return model{textInput: ti} + sug := suggestions.New() + + return model{textInput: ti, suggestions: sug} } func (m model) Init() tea.Cmd { @@ -32,44 +42,34 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc: - // return m, tea.ClearScreen return m, tea.Quit + case tea.KeyTab: + activeSuggestion := m.suggestions.ActiveSuggestion() + s := m.textInput.Value() + activeSuggestion + m.textInput.SetValue(s) + m.textInput.SetCursor(len(s)) + return m, nil } case tea.WindowSizeMsg: m.windowHeight = msg.Height m.windowWidth = msg.Width + m.textInput.Width = m.windowWidth - widthOffset } var cmd tea.Cmd + m.suggestions = m.suggestions.Update(msg, m.textInput.Value()) m.textInput, cmd = m.textInput.Update(msg) return m, cmd } func (m model) View() string { - - sugWidth := 22 - marginLeft := clamp(len(m.textInput.Value())+len(m.textInput.Prompt), 0, m.windowWidth-sugWidth) - - var style = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#FAFAFA")). - Background(lipgloss.Color("#7D56F4")). - PaddingTop(2). - PaddingLeft(4). - Width(sugWidth). - MarginLeft(marginLeft) + 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(), - style.Render("Hello, kitty"), + suggestionsSpacing.Render(m.suggestions.View()), ) } - -// work -func clamp(v, low, high int) int { - if high < low { - low, high = high, low - } - return min(high, max(low, v)) -} diff --git a/ui/utils/utils.go b/ui/utils/utils.go new file mode 100644 index 0000000..afcdb0b --- /dev/null +++ b/ui/utils/utils.go @@ -0,0 +1,8 @@ +package utils + +func Clamp(v, low, high int) int { + if high < low { + low, high = high, low + } + return min(high, max(low, v)) +}