wip: fix readme wrap

Ayman Bagabas created

Change summary

go.mod                             |  2 
go.sum                             |  2 +
ui/components/code/code.go         |  1 
ui/components/selector/selector.go | 49 +++++++++++++++++++++++++++----
ui/components/yankable/yankable.go | 22 +------------
ui/pages/selection/selection.go    | 31 ++++++++++++++++---
ui/styles/styles.go                |  4 -
7 files changed, 76 insertions(+), 35 deletions(-)

Detailed changes

go.mod 🔗

@@ -22,7 +22,7 @@ require (
 )
 
 require (
-	github.com/aymanbagabas/go-osc52 v1.0.0
+	github.com/aymanbagabas/go-osc52 v1.0.1
 	github.com/charmbracelet/keygen v0.3.0
 	github.com/gobwas/glob v0.2.3
 	github.com/gogs/git-module v1.6.0

go.sum 🔗

@@ -19,6 +19,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52 v1.0.0 h1:yjRSILSEUSIhuRcyOJ55tbSKtvBLmJDgbPErH8pXcxM=
 github.com/aymanbagabas/go-osc52 v1.0.0/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
+github.com/aymanbagabas/go-osc52 v1.0.1 h1:juDXgeKhMfVnylcoA4S7p9E4q+9DErUZGkX8t2ZR2j8=
+github.com/aymanbagabas/go-osc52 v1.0.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k=

ui/components/code/code.go 🔗

@@ -70,6 +70,7 @@ func (r *Code) Init() tea.Cmd {
 	if err != nil {
 		return common.ErrorCmd(err)
 	}
+	// FIXME reset underline and color
 	c = wrap.String(f, w)
 	r.viewport.Viewport.SetContent(c)
 	return nil

ui/components/selector/selector.go 🔗

@@ -9,9 +9,10 @@ import (
 
 // Selector is a list of items that can be selected.
 type Selector struct {
-	list   list.Model
-	common common.Common
-	active int
+	list        list.Model
+	common      common.Common
+	active      int
+	filterState list.FilterState
 }
 
 // IdentifiableItem is an item that can be identified by a string and extends list.Item.
@@ -27,8 +28,12 @@ type SelectMsg string
 type ActiveMsg string
 
 // New creates a new selector.
-func New(common common.Common, items []list.Item, delegate list.ItemDelegate) *Selector {
-	l := list.New(items, delegate, common.Width, common.Height)
+func New(common common.Common, items []IdentifiableItem, delegate list.ItemDelegate) *Selector {
+	itms := make([]list.Item, len(items))
+	for i, item := range items {
+		itms[i] = item
+	}
+	l := list.New(itms, delegate, common.Width, common.Height)
 	l.SetShowTitle(false)
 	l.SetShowHelp(false)
 	l.SetShowStatusBar(false)
@@ -76,12 +81,20 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case key.Matches(msg, s.common.Keymap.Select):
 			cmds = append(cmds, s.selectCmd)
 		}
+	case list.FilterMatchesMsg:
+		cmds = append(cmds, s.activeFilterCmd)
 	}
 	m, cmd := s.list.Update(msg)
 	s.list = m
 	if cmd != nil {
 		cmds = append(cmds, cmd)
 	}
+	// Track filter state and update active item when filter state changes.
+	filterState := s.list.FilterState()
+	if s.filterState != filterState {
+		cmds = append(cmds, s.activeFilterCmd)
+	}
+	s.filterState = filterState
 	// Send ActiveMsg when index change.
 	if s.active != s.list.Index() {
 		cmds = append(cmds, s.activeCmd)
@@ -97,12 +110,34 @@ func (s *Selector) View() string {
 
 func (s *Selector) selectCmd() tea.Msg {
 	item := s.list.SelectedItem()
-	i := item.(IdentifiableItem)
+	i, ok := item.(IdentifiableItem)
+	if !ok {
+		return SelectMsg("")
+	}
 	return SelectMsg(i.ID())
 }
 
 func (s *Selector) activeCmd() tea.Msg {
 	item := s.list.SelectedItem()
-	i := item.(IdentifiableItem)
+	i, ok := item.(IdentifiableItem)
+	if !ok {
+		return ActiveMsg("")
+	}
+	return ActiveMsg(i.ID())
+}
+
+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.list.VisibleItems()
+	if len(items) == 0 {
+		return nil
+	}
+	item := items[0]
+	i, ok := item.(IdentifiableItem)
+	if !ok {
+		return nil
+	}
 	return ActiveMsg(i.ID())
 }

ui/components/yankable/yankable.go 🔗

@@ -1,14 +1,11 @@
 package yankable
 
 import (
-	"fmt"
-	"log"
-	"strings"
+	"io"
 
 	"github.com/aymanbagabas/go-osc52"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/gliderlabs/ssh"
 )
 
 type Yankable struct {
@@ -19,26 +16,13 @@ type Yankable struct {
 	osc52     *osc52.Output
 }
 
-func New(s ssh.Session, style, yankStyle lipgloss.Style, text string) *Yankable {
-	environ := s.Environ()
-	termExists := false
-	for _, env := range environ {
-		if strings.HasPrefix(env, "TERM=") {
-			termExists = true
-			break
-		}
-	}
-	if !termExists {
-		pty, _, _ := s.Pty()
-		environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term))
-	}
-	log.Print(environ)
+func New(w io.Writer, environ []string, style, yankStyle lipgloss.Style, text string) *Yankable {
 	return &Yankable{
 		yankStyle: yankStyle,
 		style:     style,
 		text:      text,
 		clicked:   false,
-		osc52:     osc52.NewOutput(s, environ),
+		osc52:     osc52.NewOutput(w, environ),
 	}
 }
 

ui/pages/selection/selection.go 🔗

@@ -2,6 +2,7 @@ package selection
 
 import (
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/charmbracelet/bubbles/key"
@@ -37,12 +38,14 @@ func New(s session.Session, common common.Common) *Selection {
 	sel := &Selection{
 		s:         s,
 		common:    common,
-		activeBox: 1,
+		activeBox: selectorBox, // start with the selector focused
 	}
 	readme := code.New(common, "", "")
 	readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
 	sel.readme = readme
-	sel.selector = selector.New(common, []list.Item{}, ItemDelegate{common.Styles, &sel.activeBox})
+	sel.selector = selector.New(common,
+		[]selector.IdentifiableItem{},
+		ItemDelegate{common.Styles, &sel.activeBox})
 	return sel
 }
 
@@ -52,7 +55,11 @@ func (s *Selection) SetSize(width, height int) {
 	sw := s.common.Styles.SelectorBox.GetWidth()
 	wm := sw +
 		s.common.Styles.SelectorBox.GetHorizontalFrameSize() +
-		s.common.Styles.ReadmeBox.GetHorizontalFrameSize()
+		s.common.Styles.ReadmeBox.GetHorizontalFrameSize() +
+		// +1 to get wrapping to work.
+		// This is needed because the readme box width has to be -1 from the
+		// readme style in order for wrapping to not break.
+		1
 	hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize()
 	s.readme.SetSize(width-wm, height-hm)
 	s.selector.SetSize(sw, height)
@@ -104,12 +111,26 @@ func (s *Selection) FullHelp() [][]key.Binding {
 
 // Init implements tea.Model.
 func (s *Selection) Init() tea.Cmd {
+	session := s.s.Session()
+	environ := session.Environ()
+	termExists := false
+	for _, env := range environ {
+		if strings.HasPrefix(env, "TERM=") {
+			termExists = true
+			break
+		}
+	}
+	if !termExists {
+		pty, _, _ := session.Pty()
+		environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term))
+	}
 	items := make([]list.Item, 0)
 	cfg := s.s.Config()
 	// TODO clean up this
 	yank := func(text string) *yankable.Yankable {
 		return yankable.New(
-			s.s.Session(),
+			session,
+			environ,
 			lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
 			lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
 			text,
@@ -122,7 +143,7 @@ func (s *Selection) Init() tea.Cmd {
 			Name:        r.Repo,
 			Description: r.Note,
 			LastUpdate:  time.Now(),
-			URL:         yank(repoUrl(cfg, r.Name)),
+			URL:         yank(repoUrl(cfg, r.Repo)),
 		})
 	}
 	for _, r := range cfg.Source.AllRepos() {

ui/styles/styles.go 🔗

@@ -122,9 +122,7 @@ func DefaultStyles() *Styles {
 		Width(64)
 
 	s.ReadmeBox = lipgloss.NewStyle().
-		BorderForeground(s.InactiveBorderColor).
-		Padding(1).
-		MarginRight(1)
+		Margin(1, 1, 1, 0)
 
 	s.RepoTitleBorder = lipgloss.Border{
 		Top:         "─",