1package repo
2
3import (
4 "fmt"
5 "strconv"
6
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9 gitui "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git"
10 gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
11 "github.com/charmbracelet/soft-serve/internal/tui/style"
12 "github.com/go-git/go-git/v5/plumbing"
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 gittypes.Repo
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 gittypes.Repo, 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 b.width = msg.Width
60 b.height = msg.Height
61 }
62 box, cmd := b.box.Update(msg)
63 b.box = box.(*gitui.Bubble)
64 return b, cmd
65}
66
67func (b *Bubble) Help() []gittypes.HelpEntry {
68 return b.box.Help()
69}
70
71func (b Bubble) headerView() string {
72 // Render repo title
73 title := b.name
74 if title == "config" {
75 title = "Home"
76 }
77 title = truncate.StringWithTail(title, repoNameMaxWidth, "…")
78 title = b.styles.RepoTitle.Render(title)
79
80 // Render clone command
81 var note string
82 if b.name == "config" {
83 note = ""
84 } else {
85 note = fmt.Sprintf("git clone %s", b.sshAddress())
86 }
87 noteWidth := b.width -
88 b.widthMargin -
89 lipgloss.Width(title) -
90 b.styles.RepoTitleBox.GetHorizontalFrameSize()
91 // Hard-wrap the clone command only, without the usual word-wrapping. since
92 // a long repo name isn't going to be a series of space-separated "words",
93 // we'll always want it to be perfectly hard-wrapped.
94 note = wrap.String(note, noteWidth-b.styles.RepoNote.GetHorizontalFrameSize())
95 note = b.styles.RepoNote.Copy().Width(noteWidth).Render(note)
96
97 // Render borders on name and command
98 height := gittypes.Max(lipgloss.Height(title), lipgloss.Height(note))
99 titleBoxStyle := b.styles.RepoTitleBox.Copy().Height(height)
100 noteBoxStyle := b.styles.RepoNoteBox.Copy().Height(height)
101 if b.Active {
102 titleBoxStyle = titleBoxStyle.BorderForeground(b.styles.ActiveBorderColor)
103 noteBoxStyle = noteBoxStyle.BorderForeground(b.styles.ActiveBorderColor)
104 }
105 title = titleBoxStyle.Render(title)
106 note = noteBoxStyle.Render(note)
107
108 // Render
109 return lipgloss.JoinHorizontal(lipgloss.Top, title, note)
110}
111
112func (b *Bubble) View() string {
113 header := b.headerView()
114 bs := b.styles.RepoBody.Copy()
115 if b.Active {
116 bs = bs.BorderForeground(b.styles.ActiveBorderColor)
117 }
118 body := bs.Width(b.width - b.widthMargin - b.styles.RepoBody.GetVerticalFrameSize()).
119 Height(b.height - b.heightMargin - lipgloss.Height(header)).
120 Render(b.box.View())
121 return header + body
122}
123
124func (b *Bubble) Reference() plumbing.ReferenceName {
125 return b.box.Reference()
126}
127
128func (b Bubble) sshAddress() string {
129 p := ":" + strconv.Itoa(int(b.port))
130 if p == ":22" {
131 p = ""
132 }
133 return fmt.Sprintf("ssh://%s%s/%s", b.host, p, b.name)
134}