fix(ui): truncate strings on small terminal width

Ayman Bagabas created

Change summary

ui/components/statusbar/statusbar.go | 25 ++++++++++++++++++-------
ui/components/tabs/tabs.go           |  4 +++-
ui/pages/repo/filesitem.go           | 14 +++++++++++---
ui/pages/repo/log.go                 |  2 +-
ui/pages/repo/logitem.go             | 27 +++++++++++++++++++--------
ui/pages/repo/refsitem.go            | 13 +++++++------
ui/pages/repo/repo.go                | 11 +++++++----
ui/styles/styles.go                  |  3 ++-
8 files changed, 68 insertions(+), 31 deletions(-)

Detailed changes

ui/components/statusbar/statusbar.go 🔗

@@ -7,6 +7,7 @@ import (
 	"github.com/muesli/reflow/truncate"
 )
 
+// StatusBarMsg is a message sent to the status bar.
 type StatusBarMsg struct {
 	Key    string
 	Value  string
@@ -14,16 +15,19 @@ type StatusBarMsg struct {
 	Branch string
 }
 
+// StatusBar is a status bar model.
 type StatusBar struct {
 	common common.Common
 	msg    StatusBarMsg
 }
 
+// Model is an interface that supports setting the status bar information.
 type Model interface {
 	StatusBarValue() string
 	StatusBarInfo() string
 }
 
+// New creates a new status bar component.
 func New(c common.Common) *StatusBar {
 	s := &StatusBar{
 		common: c,
@@ -31,15 +35,18 @@ func New(c common.Common) *StatusBar {
 	return s
 }
 
+// SetSize implements common.Component.
 func (s *StatusBar) SetSize(width, height int) {
 	s.common.Width = width
 	s.common.Height = height
 }
 
+// Init implements tea.Model.
 func (s *StatusBar) Init() tea.Cmd {
 	return nil
 }
 
+// Update implements tea.Model.
 func (s *StatusBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case StatusBarMsg:
@@ -48,6 +55,7 @@ func (s *StatusBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return s, nil
 }
 
+// View implements tea.Model.
 func (s *StatusBar) View() string {
 	st := s.common.Styles
 	w := lipgloss.Width
@@ -64,11 +72,14 @@ func (s *StatusBar) View() string {
 		Width(maxWidth).
 		Render(v)
 
-	return lipgloss.JoinHorizontal(lipgloss.Top,
-		key,
-		value,
-		info,
-		branch,
-		help,
-	)
+	return lipgloss.NewStyle().MaxWidth(s.common.Width).
+		Render(
+			lipgloss.JoinHorizontal(lipgloss.Top,
+				key,
+				value,
+				info,
+				branch,
+				help,
+			),
+		)
 }

ui/components/tabs/tabs.go 🔗

@@ -84,7 +84,9 @@ func (t *Tabs) View() string {
 			s.WriteString(sep.String())
 		}
 	}
-	return s.String()
+	return lipgloss.NewStyle().
+		MaxWidth(t.common.Width).
+		Render(s.String())
 }
 
 func (t *Tabs) activeTabCmd() tea.Msg {

ui/pages/repo/filesitem.go 🔗

@@ -129,9 +129,17 @@ func (d FileItemDelegate) Render(w io.Writer, m list.Model, index int, listItem
 		cs.GetMarginLeft() +
 		sizeStyle.GetHorizontalFrameSize()
 	name = common.TruncateString(name, m.Width()-leftMargin)
+	name = cs.Render(name)
+	size = sizeStyle.Render(size)
+	modeStr := s.TreeFileMode.Render(mode.String())
+	truncate := lipgloss.NewStyle().MaxWidth(m.Width() -
+		s.TreeItemSelector.GetHorizontalFrameSize() -
+		s.TreeItemSelector.GetWidth())
 	fmt.Fprint(w,
-		s.TreeFileMode.Render(mode.String()),
-		sizeStyle.Render(size),
-		cs.Render(name),
+		truncate.Render(fmt.Sprintf("%s%s%s",
+			modeStr,
+			size,
+			name,
+		)),
 	)
 }

ui/pages/repo/log.go 🔗

@@ -470,5 +470,5 @@ func (l *Log) renderDiff(diff *ggit.Diff) string {
 	} else {
 		s.WriteString(fmt.Sprintf("\n%s", pr.String()))
 	}
-	return wrap.String(s.String(), l.common.Width-2)
+	return wrap.String(s.String(), l.common.Width)
 }

ui/pages/repo/logitem.go 🔗

@@ -12,6 +12,7 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/ui/common"
+	"github.com/muesli/reflow/truncate"
 )
 
 // LogItem is a item in the log list that displays a git commit.
@@ -88,31 +89,41 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
 		return
 	}
 
-	width := lipgloss.Width
 	titleStyle := styles.LogItemTitle.Copy()
 	style := styles.LogItemInactive
 	if index == m.Index() {
 		titleStyle.Bold(true)
 		style = styles.LogItemActive
 	}
-	hash := " " + i.Commit.ID.String()[:7]
+	hash := i.Commit.ID.String()[:7]
 	if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
 		hash = "copied"
 	}
 	title := titleStyle.Render(
-		common.TruncateString(i.Title(), m.Width()-style.GetHorizontalFrameSize()-width(hash)-2),
+		common.TruncateString(i.Title(),
+			m.Width()-
+				style.GetHorizontalFrameSize()-
+				// 9 is the length of the hash (7) + the left padding (1) + the
+				// title truncation symbol (1)
+				9),
 	)
 	hashStyle := styles.LogItemHash.Copy().
 		Align(lipgloss.Right).
+		PaddingLeft(1).
 		Width(m.Width() -
 			style.GetHorizontalFrameSize() -
-			width(title) -
-			// FIXME where this "1" is coming from?
-			1)
+			lipgloss.Width(title) - 1) // 1 is for the left padding
 	if index == m.Index() {
 		hashStyle = hashStyle.Bold(true)
 	}
 	hash = hashStyle.Render(hash)
+	if m.Width()-style.GetHorizontalFrameSize()-hashStyle.GetHorizontalFrameSize()-hashStyle.GetWidth() <= 0 {
+		hash = ""
+		title = titleStyle.Render(
+			common.TruncateString(i.Title(),
+				m.Width()-style.GetHorizontalFrameSize()),
+		)
+	}
 	author := i.Author.Name
 	commiter := i.Committer.Name
 	who := ""
@@ -133,10 +144,10 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
 	fmt.Fprint(w,
 		style.Render(
 			lipgloss.JoinVertical(lipgloss.Top,
-				lipgloss.JoinHorizontal(lipgloss.Left,
+				truncate.String(fmt.Sprintf("%s%s",
 					title,
 					hash,
-				),
+				), uint(m.Width()-style.GetHorizontalFrameSize())),
 				who,
 			),
 		),

ui/pages/repo/refsitem.go 🔗

@@ -91,20 +91,21 @@ func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
 	}
 
 	ref := i.Short()
+	ref = s.RefItemBranch.Render(ref)
 	if i.Reference.IsTag() {
 		ref = s.RefItemTag.Render(ref)
 	}
-	ref = s.RefItemBranch.Render(ref)
 	refMaxWidth := m.Width() -
 		s.RefItemSelector.GetMarginLeft() -
 		s.RefItemSelector.GetWidth() -
 		s.RefItemInactive.GetMarginLeft()
 	ref = common.TruncateString(ref, refMaxWidth)
+	refStyle := s.RefItemInactive
+	selector := s.RefItemSelector.Render(" ")
 	if index == m.Index() {
-		fmt.Fprint(w, s.RefItemSelector.Render(">")+
-			s.RefItemActive.Render(ref))
-	} else {
-		fmt.Fprint(w, s.RefItemSelector.Render(" ")+
-			s.RefItemInactive.Render(ref))
+		selector = s.RefItemSelector.Render(">")
+		refStyle = s.RefItemActive
 	}
+	ref = refStyle.Render(ref)
+	fmt.Fprint(w, selector, ref)
 }

ui/pages/repo/repo.go 🔗

@@ -173,7 +173,9 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tabs.ActiveTabMsg:
 		r.activeTab = tab(msg)
 		if r.selectedRepo != nil {
-			cmds = append(cmds, r.updateStatusBarCmd)
+			cmds = append(cmds,
+				r.updateStatusBarCmd,
+			)
 		}
 	case tea.KeyMsg, tea.MouseMsg:
 		t, cmd := r.tabs.Update(msg)
@@ -261,6 +263,7 @@ func (r *Repo) headerView() string {
 		return ""
 	}
 	cfg := r.cfg
+	truncate := lipgloss.NewStyle().MaxWidth(r.common.Width)
 	name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
 	desc := r.selectedRepo.Description()
 	if desc == "" {
@@ -279,11 +282,11 @@ func (r *Repo) headerView() string {
 	style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
 	return style.Render(
 		lipgloss.JoinVertical(lipgloss.Top,
-			name,
-			lipgloss.JoinHorizontal(lipgloss.Left,
+			truncate.Render(name),
+			truncate.Render(lipgloss.JoinHorizontal(lipgloss.Left,
 				desc,
 				url,
-			),
+			)),
 		),
 	)
 }

ui/styles/styles.go 🔗

@@ -301,7 +301,8 @@ func DefaultStyles() *Styles {
 		Width(1).
 		Foreground(lipgloss.Color("#B083EA"))
 
-	s.RefItemActive = s.RefItemInactive.Copy().
+	s.RefItemActive = lipgloss.NewStyle().
+		MarginLeft(1).
 		Bold(true)
 
 	s.RefItemBranch = lipgloss.NewStyle()