1package selection23import (4 "fmt"5 "sort"67 "charm.land/bubbles/v2/key"8 "charm.land/bubbles/v2/list"9 tea "charm.land/bubbletea/v2"10 "charm.land/lipgloss/v2"11 "github.com/charmbracelet/soft-serve/pkg/access"12 "github.com/charmbracelet/soft-serve/pkg/backend"13 "github.com/charmbracelet/soft-serve/pkg/ui/common"14 "github.com/charmbracelet/soft-serve/pkg/ui/components/code"15 "github.com/charmbracelet/soft-serve/pkg/ui/components/selector"16 "github.com/charmbracelet/soft-serve/pkg/ui/components/tabs"17)1819const (20 defaultNoContent = "No readme found.\n\nCreate a `.soft-serve` repository and add a `README.md` file to display readme."21)2223type pane int2425const (26 selectorPane pane = iota27 readmePane28 lastPane29)3031func (p pane) String() string {32 return []string{33 "Repositories",34 "About",35 }[p]36}3738// Selection is the model for the selection screen/page.39type Selection struct {40 common common.Common41 readme *code.Code42 selector *selector.Selector43 activePane pane44 tabs *tabs.Tabs45}4647// New creates a new selection model.48func New(c common.Common) *Selection {49 ts := make([]string, lastPane)50 for i, b := range []pane{selectorPane, readmePane} {51 ts[i] = b.String()52 }53 t := tabs.New(c, ts)54 t.TabSeparator = lipgloss.NewStyle()55 t.TabInactive = c.Styles.TopLevelNormalTab56 t.TabActive = c.Styles.TopLevelActiveTab57 t.TabDot = c.Styles.TopLevelActiveTabDot58 t.UseDot = true59 sel := &Selection{60 common: c,61 activePane: selectorPane, // start with the selector focused62 tabs: t,63 }64 readme := code.New(c, "", "")65 readme.UseGlamour = true66 readme.NoContentStyle = c.Styles.NoContent.67 SetString(defaultNoContent)68 selector := selector.New(c,69 []selector.IdentifiableItem{},70 NewItemDelegate(&c, &sel.activePane))71 selector.SetShowTitle(false)72 selector.SetShowHelp(false)73 selector.SetShowStatusBar(false)74 selector.DisableQuitKeybindings()75 sel.selector = selector76 sel.readme = readme77 return sel78}7980func (s *Selection) getMargins() (wm, hm int) {81 wm = 082 hm = s.common.Styles.Tabs.GetVerticalFrameSize() +83 s.common.Styles.Tabs.GetHeight()84 if s.activePane == selectorPane && s.IsFiltering() {85 // hide tabs when filtering86 hm = 087 }88 return89}9091// FilterState returns the current filter state.92func (s *Selection) FilterState() list.FilterState {93 return s.selector.FilterState()94}9596// SetSize implements common.Component.97func (s *Selection) SetSize(width, height int) {98 s.common.SetSize(width, height)99 wm, hm := s.getMargins()100 s.tabs.SetSize(width, height-hm)101 s.selector.SetSize(width-wm, height-hm)102 s.readme.SetSize(width-wm, height-hm-1) // -1 for readme status line103}104105// IsFiltering returns true if the selector is currently filtering.106func (s *Selection) IsFiltering() bool {107 return s.FilterState() == list.Filtering108}109110// ShortHelp implements help.KeyMap.111func (s *Selection) ShortHelp() []key.Binding {112 k := s.selector.KeyMap113 kb := make([]key.Binding, 0)114 kb = append(kb,115 s.common.KeyMap.UpDown,116 s.common.KeyMap.Section,117 )118 if s.activePane == selectorPane {119 copyKey := s.common.KeyMap.Copy120 copyKey.SetHelp("c", "copy command")121 kb = append(kb,122 s.common.KeyMap.Select,123 k.Filter,124 k.ClearFilter,125 copyKey,126 )127 }128 return kb129}130131// FullHelp implements help.KeyMap.132func (s *Selection) FullHelp() [][]key.Binding {133 b := [][]key.Binding{134 {135 s.common.KeyMap.Section,136 },137 }138 switch s.activePane {139 case readmePane:140 k := s.readme.KeyMap141 b = append(b, []key.Binding{142 k.PageDown,143 k.PageUp,144 })145 b = append(b, []key.Binding{146 k.HalfPageDown,147 k.HalfPageUp,148 })149 b = append(b, []key.Binding{150 k.Down,151 k.Up,152 })153 case selectorPane:154 copyKey := s.common.KeyMap.Copy155 copyKey.SetHelp("c", "copy command")156 k := s.selector.KeyMap157 if !s.IsFiltering() {158 b[0] = append(b[0],159 s.common.KeyMap.Select,160 copyKey,161 )162 }163 b = append(b, []key.Binding{164 k.CursorUp,165 k.CursorDown,166 })167 b = append(b, []key.Binding{168 k.NextPage,169 k.PrevPage,170 k.GoToStart,171 k.GoToEnd,172 })173 b = append(b, []key.Binding{174 k.Filter,175 k.ClearFilter,176 k.CancelWhileFiltering,177 k.AcceptWhileFiltering,178 })179 }180 return b181}182183// Init implements tea.Model.184func (s *Selection) Init() tea.Cmd {185 var readmeCmd tea.Cmd186 cfg := s.common.Config()187 if cfg == nil {188 return nil189 }190191 ctx := s.common.Context()192 be := s.common.Backend()193 pk := s.common.PublicKey()194 if pk == nil && !be.AllowKeyless(ctx) {195 return nil196 }197198 repos, err := be.Repositories(ctx)199 if err != nil {200 return common.ErrorCmd(err)201 }202 sortedItems := make(Items, 0)203 for _, r := range repos {204 if r.Name() == ".soft-serve" {205 readme, path, err := backend.Readme(r, nil)206 if err != nil {207 continue208 }209210 readmeCmd = s.readme.SetContent(readme, path)211 }212213 if r.IsHidden() {214 continue215 }216 al := be.AccessLevelByPublicKey(ctx, r.Name(), pk)217 if al >= access.ReadOnlyAccess {218 item, err := NewItem(s.common, r)219 if err != nil {220 s.common.Logger.Debugf("ui: failed to create item for %s: %v", r.Name(), err)221 continue222 }223 sortedItems = append(sortedItems, item)224 }225 }226 sort.Sort(sortedItems)227 items := make([]selector.IdentifiableItem, len(sortedItems))228 for i, it := range sortedItems {229 items[i] = it230 }231 return tea.Batch(232 s.selector.Init(),233 s.selector.SetItems(items),234 readmeCmd,235 )236}237238// Update implements tea.Model.239func (s *Selection) Update(msg tea.Msg) (common.Model, tea.Cmd) {240 cmds := make([]tea.Cmd, 0)241 switch msg := msg.(type) {242 case tea.WindowSizeMsg:243 r, cmd := s.readme.Update(msg)244 s.readme = r.(*code.Code)245 if cmd != nil {246 cmds = append(cmds, cmd)247 }248 m, cmd := s.selector.Update(msg)249 s.selector = m.(*selector.Selector)250 if cmd != nil {251 cmds = append(cmds, cmd)252 }253 case tea.KeyPressMsg, tea.MouseMsg:254 switch msg := msg.(type) {255 case tea.KeyPressMsg:256 switch {257 case key.Matches(msg, s.common.KeyMap.Back):258 cmds = append(cmds, s.selector.Init())259 }260 }261 t, cmd := s.tabs.Update(msg)262 s.tabs = t.(*tabs.Tabs)263 if cmd != nil {264 cmds = append(cmds, cmd)265 }266 case tabs.ActiveTabMsg:267 s.activePane = pane(msg)268 }269 switch s.activePane {270 case readmePane:271 r, cmd := s.readme.Update(msg)272 s.readme = r.(*code.Code)273 if cmd != nil {274 cmds = append(cmds, cmd)275 }276 case selectorPane:277 m, cmd := s.selector.Update(msg)278 s.selector = m.(*selector.Selector)279 if cmd != nil {280 cmds = append(cmds, cmd)281 }282 }283 return s, tea.Batch(cmds...)284}285286// View implements tea.Model.287func (s *Selection) View() string {288 var view string289 wm, hm := s.getMargins()290 switch s.activePane {291 case selectorPane:292 ss := lipgloss.NewStyle().293 Width(s.common.Width - wm).294 Height(s.common.Height - hm)295 view = ss.Render(s.selector.View())296 case readmePane:297 rs := lipgloss.NewStyle().298 Height(s.common.Height - hm)299 status := fmt.Sprintf("☰ %.f%%", s.readme.ScrollPercent()*100)300 readmeStatus := lipgloss.NewStyle().301 Align(lipgloss.Right).302 Width(s.common.Width - wm).303 Foreground(s.common.Styles.InactiveBorderColor).304 Render(status)305 view = rs.Render(lipgloss.JoinVertical(lipgloss.Left,306 s.readme.View(),307 readmeStatus,308 ))309 }310 if s.activePane != selectorPane || s.FilterState() != list.Filtering {311 tabs := s.common.Styles.Tabs.Render(s.tabs.View())312 view = lipgloss.JoinVertical(lipgloss.Left,313 tabs,314 view,315 )316 }317 return lipgloss.JoinVertical(318 lipgloss.Left,319 view,320 )321}