Detailed changes
@@ -26,7 +26,7 @@ require (
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
github.com/charmbracelet/x/exp/ordered v0.1.0
- github.com/charmbracelet/x/exp/slice v0.0.0-20251201173703-9f73bfd934ff
+ github.com/charmbracelet/x/exp/slice v0.0.0-20251212161403-a3028fabe6bc
github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4
github.com/charmbracelet/x/term v0.2.2
github.com/denisbrodbeck/machineid v1.0.1
@@ -56,9 +56,9 @@ require (
github.com/tidwall/sjson v1.2.5
github.com/zeebo/xxh3 v1.0.2
golang.org/x/mod v0.30.0
- golang.org/x/net v0.47.0
- golang.org/x/sync v0.18.0
- golang.org/x/text v0.31.0
+ golang.org/x/net v0.48.0
+ golang.org/x/sync v0.19.0
+ golang.org/x/text v0.32.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
@@ -153,8 +153,8 @@ require (
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
- github.com/yuin/goldmark v1.7.8 // indirect
- github.com/yuin/goldmark-emoji v1.0.5 // indirect
+ github.com/yuin/goldmark v1.7.13 // indirect
+ github.com/yuin/goldmark-emoji v1.0.6 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
@@ -163,12 +163,12 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
- golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/image v0.27.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/term v0.37.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/term v0.38.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.239.0 // indirect
google.golang.org/genai v1.37.0 // indirect
@@ -112,6 +112,8 @@ github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sB
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/slice v0.0.0-20251201173703-9f73bfd934ff h1:Uwr+/JS+qnRcO/++xjYEDtW7x+P5E4+4cBiOHTt2Xfk=
github.com/charmbracelet/x/exp/slice v0.0.0-20251201173703-9f73bfd934ff/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
+github.com/charmbracelet/x/exp/slice v0.0.0-20251212161403-a3028fabe6bc h1:NNtbWezz9vYKrWbOwfQ3Vxtj6Feysb33wdpSV8x13ck=
+github.com/charmbracelet/x/exp/slice v0.0.0-20251212161403-a3028fabe6bc/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM=
github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4 h1:i/XilBPYK4L1Yo/mc9FPx0SyJzIsN0y4sj1MWq9Sscc=
@@ -336,8 +338,12 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
+github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
+github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
+github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
@@ -372,6 +378,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
@@ -396,6 +404,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -407,6 +417,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -425,6 +437,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -438,6 +452,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -449,6 +465,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -2,8 +2,8 @@ package common
import (
"github.com/charmbracelet/crush/internal/ui/styles"
- "github.com/charmbracelet/glamour/v2"
- gstyles "github.com/charmbracelet/glamour/v2/styles"
+ "charm.land/glamour/v2"
+ gstyles "charm.land/glamour/v2/styles"
)
// MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with
@@ -27,15 +27,16 @@ type ListItem interface {
// SessionItem wraps a [session.Session] to implement the [ListItem] interface.
type SessionItem struct {
session.Session
- t *styles.Styles
- m fuzzy.Match
+ t *styles.Styles
+ m fuzzy.Match
+ focused bool
}
var _ ListItem = &SessionItem{}
// Filter returns the filterable value of the session.
func (s *SessionItem) Filter() string {
- return s.Session.Title
+ return s.Title
}
// ID returns the unique identifier of the session.
@@ -48,16 +49,23 @@ func (s *SessionItem) SetMatch(m fuzzy.Match) {
s.m = m
}
+// SetFocused set the current items focus state
+func (s *SessionItem) SetFocused(focused bool) {
+ s.focused = focused
+}
+
// Render returns the string representation of the session item.
func (s *SessionItem) Render(width int) string {
- age := humanize.Time(time.Unix(s.Session.UpdatedAt, 0))
- age = s.t.Subtle.Render(age)
- age = " " + age
- ageLen := lipgloss.Width(age)
- title := s.Session.Title
+ lastUpdated := humanize.Time(time.Unix(s.UpdatedAt, 0))
+ if !s.focused {
+ lastUpdated = s.t.Subtle.Render(lastUpdated)
+ }
+ lastUpdated = " " + lastUpdated
+ ageLen := lipgloss.Width(lastUpdated)
+ title := s.Title
titleLen := lipgloss.Width(title)
title = ansi.Truncate(title, max(0, width-ageLen), "…")
- right := lipgloss.NewStyle().AlignHorizontal(lipgloss.Right).Width(width - titleLen).Render(age)
+ right := lipgloss.NewStyle().AlignHorizontal(lipgloss.Right).Width(width - titleLen).Render(lastUpdated)
if matches := len(s.m.MatchedIndexes); matches > 0 {
var lastPos int
@@ -32,3 +32,10 @@ type MouseClickable interface {
// It returns true if the event was handled, false otherwise.
HandleMouseClick(btn ansi.MouseButton, x, y int) bool
}
+
+// FocusAware represents an item that needs to know its focus state for
+// internal rendering decisions.
+type FocusAware interface {
+ // SetFocused is called before Render to inform the item of its focus state.
+ SetFocused(focused bool)
+}
@@ -158,6 +158,12 @@ func (l *List) renderItem(idx int, process bool) renderedItem {
}
}
+ // Notify item of focus state if it cares.
+ isFocused := l.focused && idx == l.selectedIdx
+ if focusAware, ok := l.items[idx].(FocusAware); ok {
+ focusAware.SetFocused(isFocused)
+ }
+
ri, ok := l.renderedItems[idx]
if !ok {
item := l.items[idx]
@@ -413,11 +419,23 @@ func (l *List) AppendItems(items ...Item) {
// Focus sets the focus state of the list.
func (l *List) Focus() {
l.focused = true
+ // Invalidate the selected item if it's focus-aware.
+ if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
+ if _, ok := l.items[l.selectedIdx].(FocusAware); ok {
+ l.invalidateItem(l.selectedIdx)
+ }
+ }
}
// Blur removes the focus state from the list.
func (l *List) Blur() {
l.focused = false
+ // Invalidate the selected item if it's focus-aware.
+ if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
+ if _, ok := l.items[l.selectedIdx].(FocusAware); ok {
+ l.invalidateItem(l.selectedIdx)
+ }
+ }
}
// ScrollToTop scrolls the list to the top.
@@ -497,38 +515,66 @@ func (l *List) SelectedItemInView() bool {
// SetSelected sets the selected item index in the list.
func (l *List) SetSelected(index int) {
+ oldIdx := l.selectedIdx
if index < 0 || index >= len(l.items) {
l.selectedIdx = -1
} else {
l.selectedIdx = index
}
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
+}
+
+// invalidateFocusAwareItems invalidates the cache for items that implement
+// FocusAware when their focus state changes.
+func (l *List) invalidateFocusAwareItems(oldIdx, newIdx int) {
+ if oldIdx == newIdx {
+ return
+ }
+ if oldIdx >= 0 && oldIdx < len(l.items) {
+ if _, ok := l.items[oldIdx].(FocusAware); ok {
+ l.invalidateItem(oldIdx)
+ }
+ }
+ if newIdx >= 0 && newIdx < len(l.items) {
+ if _, ok := l.items[newIdx].(FocusAware); ok {
+ l.invalidateItem(newIdx)
+ }
+ }
}
// SelectPrev selects the previous item in the list.
func (l *List) SelectPrev() {
if l.selectedIdx > 0 {
+ oldIdx := l.selectedIdx
l.selectedIdx--
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
}
}
// SelectNext selects the next item in the list.
func (l *List) SelectNext() {
if l.selectedIdx < len(l.items)-1 {
+ oldIdx := l.selectedIdx
l.selectedIdx++
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
}
}
// SelectFirst selects the first item in the list.
func (l *List) SelectFirst() {
if len(l.items) > 0 {
+ oldIdx := l.selectedIdx
l.selectedIdx = 0
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
}
}
// SelectLast selects the last item in the list.
func (l *List) SelectLast() {
if len(l.items) > 0 {
+ oldIdx := l.selectedIdx
l.selectedIdx = len(l.items) - 1
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
}
}
@@ -544,13 +590,17 @@ func (l *List) SelectedItem() Item {
// SelectFirstInView selects the first item currently in view.
func (l *List) SelectFirstInView() {
startIdx, _ := l.findVisibleItems()
+ oldIdx := l.selectedIdx
l.selectedIdx = startIdx
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
}
// SelectLastInView selects the last item currently in view.
func (l *List) SelectLastInView() {
_, endIdx := l.findVisibleItems()
+ oldIdx := l.selectedIdx
l.selectedIdx = endIdx
+ l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
}
// HandleMouseDown handles mouse down events at the given line in the viewport.