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}