bubble.go

  1package repo
  2
  3import (
  4	"fmt"
  5	"strconv"
  6
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/lipgloss"
  9	"github.com/charmbracelet/soft-serve/internal/tui/style"
 10	"github.com/charmbracelet/soft-serve/pkg/git"
 11	gitui "github.com/charmbracelet/soft-serve/pkg/tui"
 12	"github.com/charmbracelet/soft-serve/pkg/tui/common"
 13	"github.com/muesli/reflow/truncate"
 14	"github.com/muesli/reflow/wrap"
 15)
 16
 17const (
 18	repoNameMaxWidth = 32
 19)
 20
 21type Bubble struct {
 22	name         string
 23	host         string
 24	port         int
 25	repo         common.GitRepo
 26	styles       *style.Styles
 27	width        int
 28	widthMargin  int
 29	height       int
 30	heightMargin int
 31	box          *gitui.Bubble
 32
 33	Active bool
 34}
 35
 36func NewBubble(repo common.GitRepo, host string, port int, styles *style.Styles, width, wm, height, hm int) *Bubble {
 37	b := &Bubble{
 38		name:         repo.Name(),
 39		host:         host,
 40		port:         port,
 41		width:        width,
 42		widthMargin:  wm,
 43		height:       height,
 44		heightMargin: hm,
 45		styles:       styles,
 46	}
 47	b.repo = repo
 48	b.box = gitui.NewBubble(repo, styles, width, wm+styles.RepoBody.GetHorizontalBorderSize(), height, hm+lipgloss.Height(b.headerView())-styles.RepoBody.GetVerticalBorderSize())
 49	return b
 50}
 51
 52func (b *Bubble) Init() tea.Cmd {
 53	return b.box.Init()
 54}
 55
 56func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 57	switch msg := msg.(type) {
 58	case tea.WindowSizeMsg:
 59		if msg.Width == b.width && msg.Height == b.height {
 60			return b, nil
 61		}
 62		b.width = msg.Width
 63		b.height = msg.Height
 64	}
 65	box, cmd := b.box.Update(msg)
 66	b.box = box.(*gitui.Bubble)
 67	return b, cmd
 68}
 69
 70func (b *Bubble) Help() []common.HelpEntry {
 71	return b.box.Help()
 72}
 73
 74func (b Bubble) headerView() string {
 75	// Render repo title
 76	title := b.name
 77	if title == "config" {
 78		title = "Home"
 79	}
 80	title = truncate.StringWithTail(title, repoNameMaxWidth, "…")
 81	title = b.styles.RepoTitle.Render(title)
 82
 83	// Render clone command
 84	var note string
 85	if b.name == "config" {
 86		note = ""
 87	} else {
 88		note = fmt.Sprintf("git clone %s", b.sshAddress())
 89	}
 90	noteWidth := b.width -
 91		b.widthMargin -
 92		lipgloss.Width(title) -
 93		b.styles.RepoTitleBox.GetHorizontalFrameSize()
 94	// Hard-wrap the clone command only, without the usual word-wrapping. since
 95	// a long repo name isn't going to be a series of space-separated "words",
 96	// we'll always want it to be perfectly hard-wrapped.
 97	note = wrap.String(note, noteWidth-b.styles.RepoNote.GetHorizontalFrameSize())
 98	note = b.styles.RepoNote.Copy().Width(noteWidth).Render(note)
 99
100	// Render borders on name and command
101	height := common.Max(lipgloss.Height(title), lipgloss.Height(note))
102	titleBoxStyle := b.styles.RepoTitleBox.Copy().Height(height)
103	noteBoxStyle := b.styles.RepoNoteBox.Copy().Height(height)
104	if b.Active {
105		titleBoxStyle = titleBoxStyle.BorderForeground(b.styles.ActiveBorderColor)
106		noteBoxStyle = noteBoxStyle.BorderForeground(b.styles.ActiveBorderColor)
107	}
108	title = titleBoxStyle.Render(title)
109	note = noteBoxStyle.Render(note)
110
111	// Render
112	return lipgloss.JoinHorizontal(lipgloss.Top, title, note)
113}
114
115func (b *Bubble) View() string {
116	header := b.headerView()
117	bs := b.styles.RepoBody.Copy()
118	if b.Active {
119		bs = bs.BorderForeground(b.styles.ActiveBorderColor)
120	}
121	body := bs.Width(b.width - b.widthMargin - b.styles.RepoBody.GetVerticalFrameSize()).
122		Height(b.height - b.heightMargin - lipgloss.Height(header)).
123		Render(b.box.View())
124	return header + body
125}
126
127func (b *Bubble) Reference() *git.Reference {
128	return b.box.Reference()
129}
130
131func (b Bubble) sshAddress() string {
132	p := ":" + strconv.Itoa(int(b.port))
133	if p == ":22" {
134		p = ""
135	}
136	return fmt.Sprintf("ssh://%s%s/%s", b.host, p, b.name)
137}