diff --git a/go.mod b/go.mod index c0a03a5ca..c9cf57b80 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/glamour v0.6.0 - github.com/charmbracelet/lipgloss v0.7.1 + github.com/charmbracelet/lipgloss v0.7.2-0.20230727003539-83909ad9a917 github.com/charmbracelet/wish v1.1.1 github.com/dustin/go-humanize v1.0.1 - github.com/go-git/go-git/v5 v5.7.0 + github.com/go-git/go-git/v5 v5.8.0 github.com/matryer/is v1.4.1 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.2 diff --git a/go.sum b/go.sum index 6843898e7..cbc8c08d9 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM2 github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= github.com/charmbracelet/keygen v0.4.3 h1:ywOZRwkDlpmkawl0BgLTxaYWDSqp6Y4nfVVmgyyO1Mg= github.com/charmbracelet/keygen v0.4.3/go.mod h1:4e4FT3HSdLU/u83RfJWvzJIaVb8aX4MxtDlfXwpDJaI= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/lipgloss v0.7.2-0.20230727003539-83909ad9a917 h1:3HaVpdNIJYO3FPKpKoMAMX3MIozOg/O6qDNri/qNW9w= +github.com/charmbracelet/lipgloss v0.7.2-0.20230727003539-83909ad9a917/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/charmbracelet/log v0.2.3-0.20230725142510-280c4e3f1ef2 h1:0O3FNIElGsbl/nnUpeUVHqET7ZETJz6cUQocn/CKhoU= github.com/charmbracelet/log v0.2.3-0.20230725142510-280c4e3f1ef2/go.mod h1:ZApwwzDbbETVTIRTk7724yQRJAXIktt98yGVMMaa3y8= github.com/charmbracelet/ssh v0.0.0-20230720143903-5bdd92839155 h1:vJqYhlL0doAWQPz+EX/hK5x/ZYguoua773oRz77zYKo= @@ -55,8 +55,8 @@ github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdbl github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= -github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/go-git/go-git/v5 v5.8.0 h1:Rc543s6Tyq+YcyPwZRvU4jzZGM8rB/wWu94TnTIYALQ= +github.com/go-git/go-git/v5 v5.8.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= diff --git a/server/ssh/session.go b/server/ssh/session.go index a5bd4d168..cd40ef6d1 100644 --- a/server/ssh/session.go +++ b/server/ssh/session.go @@ -5,6 +5,7 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/server/access" "github.com/charmbracelet/soft-serve/server/backend" "github.com/charmbracelet/soft-serve/server/config" @@ -55,8 +56,8 @@ func SessionHandler(s ssh.Session) *tea.Program { } envs := &sessionEnv{s} - output := termenv.NewOutput(s, termenv.WithColorCache(true), termenv.WithEnvironment(envs)) - c := common.NewCommon(ctx, output, pty.Window.Width, pty.Window.Height) + re := lipgloss.NewRenderer(s, termenv.WithColorCache(true), termenv.WithEnvironment(envs)) + c := common.NewCommon(ctx, re, pty.Window.Width, pty.Window.Height) c.SetValue(common.ConfigKey, cfg) m := ui.New(c, initialRepo) p := tea.NewProgram(m, diff --git a/server/ui/common/common.go b/server/ui/common/common.go index 884b7d200..44c5dd6b2 100644 --- a/server/ui/common/common.go +++ b/server/ui/common/common.go @@ -3,6 +3,7 @@ package common import ( "context" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/git" "github.com/charmbracelet/soft-serve/server/backend" @@ -31,12 +32,13 @@ type Common struct { Styles *styles.Styles KeyMap *keymap.KeyMap Zone *zone.Manager + Renderer *lipgloss.Renderer Output *termenv.Output Logger *log.Logger } // NewCommon returns a new Common struct. -func NewCommon(ctx context.Context, out *termenv.Output, width, height int) Common { +func NewCommon(ctx context.Context, re *lipgloss.Renderer, width, height int) Common { if ctx == nil { ctx = context.TODO() } @@ -44,7 +46,7 @@ func NewCommon(ctx context.Context, out *termenv.Output, width, height int) Comm ctx: ctx, Width: width, Height: height, - Output: out, + Output: re.Output(), Styles: styles.DefaultStyles(), KeyMap: keymap.DefaultKeyMap(), Zone: zone.New(), diff --git a/server/ui/components/selector/selector.go b/server/ui/components/selector/selector.go index aaf3191f3..8c8d505dc 100644 --- a/server/ui/components/selector/selector.go +++ b/server/ui/components/selector/selector.go @@ -1,6 +1,8 @@ package selector import ( + "sync" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" @@ -9,10 +11,13 @@ import ( // Selector is a list of items that can be selected. type Selector struct { - list.Model + KeyMap list.KeyMap + + model list.Model common common.Common active int filterState list.FilterState + mtx sync.RWMutex } // IdentifiableItem is an item that can be identified by a string. Implements @@ -42,95 +47,171 @@ func New(common common.Common, items []IdentifiableItem, delegate ItemDelegate) l := list.New(itms, delegate, common.Width, common.Height) l.Styles.NoItems = common.Styles.NoItems s := &Selector{ - Model: l, + model: l, common: common, + KeyMap: list.DefaultKeyMap(), } s.SetSize(common.Width, common.Height) return s } +// FilterState returns the filter state. +func (s *Selector) FilterState() list.FilterState { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.FilterState() +} + +// SelectedItem returns the selected item. +func (s *Selector) SelectedItem() list.Item { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.SelectedItem() +} + +// Items returns the items. +func (s *Selector) Items() []list.Item { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.Items() +} + +// VisibleItems returns the visible items. +func (s *Selector) VisibleItems() []list.Item { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.VisibleItems() +} + // PerPage returns the number of items per page. func (s *Selector) PerPage() int { - return s.Model.Paginator.PerPage + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.Paginator.PerPage } // SetPage sets the current page. func (s *Selector) SetPage(page int) { - s.Model.Paginator.Page = page + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.Paginator.Page = page } // Page returns the current page. func (s *Selector) Page() int { - return s.Model.Paginator.Page + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.Paginator.Page } // TotalPages returns the total number of pages. func (s *Selector) TotalPages() int { - return s.Model.Paginator.TotalPages + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.Paginator.TotalPages } // Select selects the item at the given index. func (s *Selector) Select(index int) { - s.Model.Select(index) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.Select(index) } // SetShowTitle sets the show title flag. func (s *Selector) SetShowTitle(show bool) { - s.Model.SetShowTitle(show) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.SetShowTitle(show) } // SetShowHelp sets the show help flag. func (s *Selector) SetShowHelp(show bool) { - s.Model.SetShowHelp(show) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.SetShowHelp(show) } // SetShowStatusBar sets the show status bar flag. func (s *Selector) SetShowStatusBar(show bool) { - s.Model.SetShowStatusBar(show) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.SetShowStatusBar(show) } // DisableQuitKeybindings disables the quit keybindings. func (s *Selector) DisableQuitKeybindings() { - s.Model.DisableQuitKeybindings() + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.DisableQuitKeybindings() } // SetShowFilter sets the show filter flag. func (s *Selector) SetShowFilter(show bool) { - s.Model.SetShowFilter(show) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.SetShowFilter(show) } // SetShowPagination sets the show pagination flag. func (s *Selector) SetShowPagination(show bool) { - s.Model.SetShowPagination(show) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.SetShowPagination(show) } // SetFilteringEnabled sets the filtering enabled flag. func (s *Selector) SetFilteringEnabled(enabled bool) { - s.Model.SetFilteringEnabled(enabled) + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.SetFilteringEnabled(enabled) } // SetSize implements common.Component. func (s *Selector) SetSize(width, height int) { s.common.SetSize(width, height) - s.Model.SetSize(width, height) + s.mtx.Lock() + s.model.SetSize(width, height) + s.mtx.Unlock() } // SetItems sets the items in the selector. func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd { + s.mtx.Lock() + defer s.mtx.Unlock() its := make([]list.Item, len(items)) for i, item := range items { its[i] = item } - return s.Model.SetItems(its) + return s.model.SetItems(its) } // Index returns the index of the selected item. func (s *Selector) Index() int { - return s.Model.Index() + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.Index() +} + +// CursorUp moves the cursor up. +func (s *Selector) CursorUp() { + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.CursorUp() +} + +// CursorDown moves the cursor down. +func (s *Selector) CursorDown() { + s.mtx.Lock() + defer s.mtx.Unlock() + s.model.CursorDown() } // Init implements tea.Model. func (s *Selector) Init() tea.Cmd { + s.mtx.Lock() + s.model.KeyMap = s.KeyMap + s.mtx.Unlock() return s.activeCmd } @@ -141,26 +222,26 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.MouseMsg: switch msg.Type { case tea.MouseWheelUp: - s.Model.CursorUp() + s.CursorUp() case tea.MouseWheelDown: - s.Model.CursorDown() + s.CursorDown() case tea.MouseLeft: - curIdx := s.Model.Index() - for i, item := range s.Model.Items() { + curIdx := s.Index() + for i, item := range s.Items() { item, _ := item.(IdentifiableItem) // Check each item to see if it's in bounds. if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) { if i == curIdx { cmds = append(cmds, s.selectCmd) } else { - s.Model.Select(i) + s.Select(i) } break } } } case tea.KeyMsg: - filterState := s.Model.FilterState() + filterState := s.FilterState() switch { case key.Matches(msg, s.common.KeyMap.Help): if filterState == list.Filtering { @@ -174,28 +255,32 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case list.FilterMatchesMsg: cmds = append(cmds, s.activeFilterCmd) } - m, cmd := s.Model.Update(msg) - s.Model = m + s.mtx.Lock() + m, cmd := s.model.Update(msg) + s.model = m if cmd != nil { cmds = append(cmds, cmd) } + s.mtx.Unlock() // Track filter state and update active item when filter state changes. - filterState := s.Model.FilterState() + filterState := s.FilterState() if s.filterState != filterState { cmds = append(cmds, s.activeFilterCmd) } s.filterState = filterState // Send ActiveMsg when index change. - if s.active != s.Model.Index() { + if s.active != s.Index() { cmds = append(cmds, s.activeCmd) } - s.active = s.Model.Index() + s.active = s.Index() return s, tea.Batch(cmds...) } // View implements tea.Model. func (s *Selector) View() string { - return s.Model.View() + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.model.View() } // SelectItem is a command that selects the currently active item. @@ -204,7 +289,7 @@ func (s *Selector) SelectItem() tea.Msg { } func (s *Selector) selectCmd() tea.Msg { - item := s.Model.SelectedItem() + item := s.SelectedItem() i, ok := item.(IdentifiableItem) if !ok { return SelectMsg{} @@ -213,7 +298,7 @@ func (s *Selector) selectCmd() tea.Msg { } func (s *Selector) activeCmd() tea.Msg { - item := s.Model.SelectedItem() + item := s.SelectedItem() i, ok := item.(IdentifiableItem) if !ok { return ActiveMsg{} @@ -225,7 +310,7 @@ func (s *Selector) activeFilterCmd() tea.Msg { // Here we use VisibleItems because when list.FilterMatchesMsg is sent, // VisibleItems is the only way to get the list of filtered items. The list // bubble should export something like list.FilterMatchesMsg.Items(). - items := s.Model.VisibleItems() + items := s.VisibleItems() if len(items) == 0 { return nil }