Selection box, list repos, tab activate main boxes

Toby Padilla created

Change summary

tui/bubble.go                   | 48 +++++++++++++++++-------
tui/bubbles/commits/bubble.go   |  2 
tui/bubbles/selection/bubble.go | 70 +++++++++++++++++++++++++++++++++++
tui/bubbles/selection/style.go  | 11 +++++
tui/commands.go                 |  9 ++++
tui/style.go                    | 12 ++++++
6 files changed, 136 insertions(+), 16 deletions(-)

Detailed changes

tui/bubble.go 🔗

@@ -3,10 +3,10 @@ package tui
 import (
 	"fmt"
 	"smoothie/git"
-	"smoothie/tui/bubbles/commits"
 	"time"
 
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
 	"github.com/gliderlabs/ssh"
 )
 
@@ -21,14 +21,16 @@ const (
 )
 
 type Model struct {
-	state          sessionState
-	error          string
-	info           string
-	width          int
-	height         int
-	windowChanges  <-chan ssh.Window
-	repoSource     *git.RepoSource
-	commitTimeline *commits.Bubble
+	state         sessionState
+	error         string
+	info          string
+	width         int
+	height        int
+	windowChanges <-chan ssh.Window
+	repoSource    *git.RepoSource
+	repos         []*git.Repo
+	activeBubble  int
+	bubbles       []tea.Model
 }
 
 func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Model {
@@ -37,6 +39,7 @@ func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource
 		height:        height,
 		windowChanges: windowChanges,
 		repoSource:    repoSource,
+		bubbles:       make([]tea.Model, 2),
 	}
 	m.state = startState
 	return m
@@ -56,9 +59,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		switch msg.String() {
 		case "q", "ctrl+c":
 			return m, tea.Quit
-		case "j", "k", "up", "down":
-			_, cmd := m.commitTimeline.Update(msg)
-			cmds = append(cmds, cmd)
+		case "tab":
+			m.activeBubble = (m.activeBubble + 1) % 2
 		}
 	case errMsg:
 		m.error = msg.Error()
@@ -71,11 +73,27 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.WindowSizeMsg:
 		m.width = msg.Width
 		m.height = msg.Height
-		m.commitTimeline.Height = msg.Height
+	}
+	if m.state == loadedState {
+		b, cmd := m.bubbles[m.activeBubble].Update(msg)
+		m.bubbles[m.activeBubble] = b
+		if cmd != nil {
+			cmds = append(cmds, cmd)
+		}
 	}
 	return m, tea.Batch(cmds...)
 }
 
+func (m *Model) viewForBubble(i int, width int) string {
+	var ls lipgloss.Style
+	if i == m.activeBubble {
+		ls = activeBoxStyle.Width(width)
+	} else {
+		ls = inactiveBoxStyle.Width(width)
+	}
+	return ls.Render(m.bubbles[i].View())
+}
+
 func (m *Model) View() string {
 	pad := 6
 	h := headerStyle.Width(m.width - pad).Render("Charm Beta")
@@ -84,7 +102,9 @@ func (m *Model) View() string {
 	content := ""
 	switch m.state {
 	case loadedState:
-		s += m.commitTimeline.View()
+		lb := m.viewForBubble(0, 25)
+		rb := m.viewForBubble(1, 84)
+		s += lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)
 	case errorState:
 		s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error))
 	default:

tui/bubbles/commits/bubble.go 🔗

@@ -62,7 +62,7 @@ func (b *Bubble) renderCommit(rc git.RepoCommit) string {
 	s += " "
 	s += commitAuthorEmailStyle.Render(rc.Commit.Author.Email)
 	s += " "
-	return commitBoxStyle.Width(b.viewport.Height).Render(s)
+	return commitBoxStyle.Width(b.viewport.Width).Render(s)
 }
 
 func (b *Bubble) View() string {

tui/bubbles/selection/bubble.go 🔗

@@ -0,0 +1,70 @@
+package selection
+
+import (
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+)
+
+type SelectedMsg struct {
+	Name  string
+	Index int
+}
+
+type Bubble struct {
+	NormalStyle   lipgloss.Style
+	SelectedStyle lipgloss.Style
+	Items         []string
+	selectedItem  int
+}
+
+func NewBubble(items []string) *Bubble {
+	return &Bubble{
+		NormalStyle:   normalStyle,
+		SelectedStyle: selectedStyle,
+		Items:         items,
+		selectedItem:  -1,
+	}
+}
+
+func (b *Bubble) Init() tea.Cmd {
+	return nil
+}
+
+func (b *Bubble) View() string {
+	s := ""
+	for i, item := range b.Items {
+		if i == b.selectedItem {
+			s += b.SelectedStyle.Render(item) + "\n"
+		} else {
+			s += b.NormalStyle.Render(item) + "\n"
+		}
+	}
+	return s
+}
+
+func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	cmds := make([]tea.Cmd, 0)
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		switch msg.String() {
+		case "k", "up":
+			if b.selectedItem > 0 {
+				b.selectedItem--
+			}
+		case "j", "down":
+			if b.selectedItem < len(b.Items)-1 {
+				b.selectedItem++
+			}
+		case "enter":
+			cmds = append(cmds, b.sendMessage)
+		}
+	}
+	return b, tea.Batch(cmds...)
+}
+
+func (b *Bubble) sendMessage() tea.Msg {
+	return SelectedMsg{
+		Name:  b.Items[b.selectedItem],
+		Index: b.selectedItem,
+	}
+}

tui/bubbles/selection/style.go 🔗

@@ -0,0 +1,11 @@
+package selection
+
+import (
+	"github.com/charmbracelet/lipgloss"
+)
+
+var normalStyle = lipgloss.NewStyle().
+	Foreground(lipgloss.Color("#FFFFFF"))
+
+var selectedStyle = lipgloss.NewStyle().
+	Foreground(lipgloss.Color("#714C7B"))

tui/commands.go 🔗

@@ -2,6 +2,7 @@ package tui
 
 import (
 	"smoothie/tui/bubbles/commits"
+	"smoothie/tui/bubbles/selection"
 
 	tea "github.com/charmbracelet/bubbletea"
 )
@@ -23,7 +24,13 @@ func (m *Model) windowChangesCmd() tea.Msg {
 }
 
 func (m *Model) loadGitCmd() tea.Msg {
-	m.commitTimeline = commits.NewBubble(m.height, 2, 80, m.repoSource.GetCommits(200))
+	rs := make([]string, 0)
+	for _, r := range m.repoSource.AllRepos() {
+		rs = append(rs, r.Name)
+	}
+	m.bubbles[0] = selection.NewBubble(rs)
+	m.bubbles[1] = commits.NewBubble(m.height, 7, 80, m.repoSource.GetCommits(200))
+	m.activeBubble = 0
 	m.state = loadedState
 	return nil
 }

tui/style.go 🔗

@@ -9,6 +9,18 @@ var appBoxStyle = lipgloss.NewStyle().
 	PaddingRight(2).
 	MarginBottom(1)
 
+var inactiveBoxStyle = lipgloss.NewStyle().
+	Foreground(lipgloss.Color("#606060")).
+	BorderStyle(lipgloss.RoundedBorder()).
+	BorderForeground(lipgloss.Color("#303030")).
+	Padding(1)
+
+var activeBoxStyle = lipgloss.NewStyle().
+	Foreground(lipgloss.Color("#FFFFFF")).
+	BorderStyle(lipgloss.RoundedBorder()).
+	BorderForeground(lipgloss.Color("#714C7B")).
+	Padding(1)
+
 var headerStyle = lipgloss.NewStyle().
 	Foreground(lipgloss.Color("#670083")).
 	Align(lipgloss.Right).