Detailed changes
@@ -110,6 +110,7 @@ homebrew_casks:
- repository:
owner: charmbracelet
name: homebrew-tap
+ token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
npms:
- name: "@charmland/crush"
@@ -134,6 +135,32 @@ nfpms:
- src: ./manpages/crush.1.gz
dst: /usr/share/man/man1/crush.1.gz
+nix:
+ - repository:
+ owner: "charmbracelet"
+ name: nur
+ token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
+ extra_install: |-
+ installManPage ./manpages/crush.1.gz.
+ installShellCompletion ./completions/*
+
+winget:
+ - publisher: charmbracelet
+ copyright: Charmbracelet, Inc
+ repository:
+ owner: "charmbracelet"
+ name: winget-pkgs
+ token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
+ branch: "crush-{{.Version}}"
+ pull_request:
+ enabled: true
+ draft: false
+ check_boxes: true
+ base:
+ owner: microsoft
+ name: winget-pkgs
+ branch: master
+
changelog:
sort: asc
disable: "{{ .IsNightly }}"
@@ -17,7 +17,7 @@ import (
"github.com/charmbracelet/crush/internal/log"
)
-const catwalkURL = "https://catwalk.charm.sh"
+const defaultCatwalkURL = "https://catwalk.charm.sh"
// LoadReader config via io.Reader.
func LoadReader(fd io.Reader) (*Config, error) {
@@ -1,6 +1,7 @@
package config
import (
+ "cmp"
"encoding/json"
"fmt"
"log/slog"
@@ -74,6 +75,7 @@ func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
}
func Providers() ([]catwalk.Provider, error) {
+ catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
client := catwalk.NewWithURL(catwalkURL)
path := providerCacheFileData()
return loadProvidersOnce(client, path)
@@ -112,15 +112,6 @@ func (s *Slice[T]) Len() int {
return len(s.inner)
}
-// Slice returns a copy of the underlying slice.
-func (s *Slice[T]) Slice() []T {
- s.mu.RLock()
- defer s.mu.RUnlock()
- result := make([]T, len(s.inner))
- copy(result, s.inner)
- return result
-}
-
// SetSlice replaces the entire slice with a new one.
func (s *Slice[T]) SetSlice(items []T) {
s.mu.Lock()
@@ -138,10 +129,8 @@ func (s *Slice[T]) Clear() {
// Seq returns an iterator that yields elements from the slice.
func (s *Slice[T]) Seq() iter.Seq[T] {
- // Take a snapshot to avoid holding the lock during iteration
- items := s.Slice()
return func(yield func(T) bool) {
- for _, v := range items {
+ for _, v := range s.Seq2() {
if !yield(v) {
return
}
@@ -151,8 +140,10 @@ func (s *Slice[T]) Seq() iter.Seq[T] {
// Seq2 returns an iterator that yields index-value pairs from the slice.
func (s *Slice[T]) Seq2() iter.Seq2[int, T] {
- // Take a snapshot to avoid holding the lock during iteration
- items := s.Slice()
+ s.mu.RLock()
+ items := make([]T, len(s.inner))
+ copy(items, s.inner)
+ s.mu.RUnlock()
return func(yield func(int, T) bool) {
for i, v := range items {
if !yield(i, v) {
@@ -1,6 +1,7 @@
package csync
import (
+ "slices"
"sync"
"sync/atomic"
"testing"
@@ -145,7 +146,7 @@ func TestSlice(t *testing.T) {
assert.Equal(t, 4, s.Len())
expected := []int{1, 2, 4, 5}
- actual := s.Slice()
+ actual := slices.Collect(s.Seq())
assert.Equal(t, expected, actual)
// Delete out of bounds
@@ -203,7 +204,7 @@ func TestSlice(t *testing.T) {
s.SetSlice(newItems)
assert.Equal(t, 3, s.Len())
- assert.Equal(t, newItems, s.Slice())
+ assert.Equal(t, newItems, slices.Collect(s.Seq()))
// Verify it's a copy
newItems[0] = 999
@@ -224,7 +225,7 @@ func TestSlice(t *testing.T) {
original := []int{1, 2, 3}
s := NewSliceFrom(original)
- copy := s.Slice()
+ copy := slices.Collect(s.Seq())
assert.Equal(t, original, copy)
// Verify it's a copy
@@ -20,7 +20,7 @@ type Spinner struct {
type model struct {
cancel context.CancelFunc
- anim anim.Anim
+ anim *anim.Anim
}
func (m model) Init() tea.Cmd { return m.anim.Init() }
@@ -37,7 +37,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
mm, cmd := m.anim.Update(msg)
- m.anim = mm.(anim.Anim)
+ m.anim = mm.(*anim.Anim)
return m, cmd
}
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
+ "log/slog"
"maps"
"sort"
"strings"
@@ -118,7 +119,13 @@ func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string
return
}
- if diagParams.URI.Path() == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) {
+ path, err := diagParams.URI.Path()
+ if err != nil {
+ slog.Error("Failed to convert diagnostic URI to path", "uri", diagParams.URI, "error", err)
+ return
+ }
+
+ if path == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) {
select {
case diagChan <- struct{}{}:
default:
@@ -216,10 +223,15 @@ func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
diagnostics := client.GetDiagnostics()
if len(diagnostics) > 0 {
for location, diags := range diagnostics {
- isCurrentFile := location.Path() == filePath
+ path, err := location.Path()
+ if err != nil {
+ slog.Error("Failed to convert diagnostic location URI to path", "uri", location, "error", err)
+ continue
+ }
+ isCurrentFile := path == filePath
for _, diag := range diags {
- formattedDiag := formatDiagnostic(location.Path(), diag, lspName)
+ formattedDiag := formatDiagnostic(path, diag, lspName)
if isCurrentFile {
fileDiagnostics = append(fileDiagnostics, formattedDiag)
@@ -449,7 +449,12 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
// If we have any open files, try to get document symbols for one
for uri := range c.openFiles {
- filePath := protocol.DocumentURI(uri).Path()
+ filePath, err := protocol.DocumentURI(uri).Path()
+ if err != nil {
+ slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err)
+ continue
+ }
+
if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
var symbols []protocol.DocumentSymbol
@@ -712,7 +717,11 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
// First collect all URIs that need to be closed
for uri := range c.openFiles {
// Convert URI back to file path using proper URI handling
- filePath := protocol.DocumentURI(uri).Path()
+ filePath, err := protocol.DocumentURI(uri).Path()
+ if err != nil {
+ slog.Error("Failed to convert URI to path for file closing", "uri", uri, "error", err)
+ continue
+ }
filesToClose = append(filesToClose, filePath)
}
c.openFilesMu.Unlock()
@@ -2,6 +2,7 @@ package protocol
import (
"fmt"
+ "log/slog"
)
// PatternInfo is an interface for types that represent glob patterns
@@ -36,21 +37,36 @@ func (g *GlobPattern) AsPattern() (PatternInfo, error) {
return nil, fmt.Errorf("nil pattern")
}
+ var err error
+
switch v := g.Value.(type) {
case string:
return StringPattern{Pattern: v}, nil
+
case RelativePattern:
// Handle BaseURI which could be string or DocumentUri
basePath := ""
switch baseURI := v.BaseURI.Value.(type) {
case string:
- basePath = DocumentURI(baseURI).Path()
+ basePath, err = DocumentURI(baseURI).Path()
+ if err != nil {
+ slog.Error("Failed to convert URI to path", "uri", baseURI, "error", err)
+ return nil, fmt.Errorf("invalid URI: %s", baseURI)
+ }
+
case DocumentURI:
- basePath = baseURI.Path()
+ basePath, err = baseURI.Path()
+ if err != nil {
+ slog.Error("Failed to convert DocumentURI to path", "uri", baseURI, "error", err)
+ return nil, fmt.Errorf("invalid DocumentURI: %s", baseURI)
+ }
+
default:
return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
}
+
return RelativePatternInfo{RP: v, BasePath: basePath}, nil
+
default:
return nil, fmt.Errorf("unknown pattern type: %T", g.Value)
}
@@ -70,7 +70,7 @@ func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
// DocumentUri("").Path() returns the empty string.
//
// Path panics if called on a URI that is not a valid filename.
-func (uri DocumentURI) Path() string {
+func (uri DocumentURI) Path() (string, error) {
filename, err := filename(uri)
if err != nil {
// e.g. ParseRequestURI failed.
@@ -79,22 +79,33 @@ func (uri DocumentURI) Path() string {
// direct string manipulation; all DocumentUris
// received from the client pass through
// ParseRequestURI, which ensures validity.
- panic(err)
+ return "", fmt.Errorf("invalid URI %q: %w", uri, err)
}
- return filepath.FromSlash(filename)
+ return filepath.FromSlash(filename), nil
}
// Dir returns the URI for the directory containing the receiver.
-func (uri DocumentURI) Dir() DocumentURI {
+func (uri DocumentURI) Dir() (DocumentURI, error) {
+ // XXX: Legacy comment:
// This function could be more efficiently implemented by avoiding any call
// to Path(), but at least consolidates URI manipulation.
- return URIFromPath(uri.DirPath())
+
+ path, err := uri.DirPath()
+ if err != nil {
+ return "", fmt.Errorf("invalid URI %q: %w", uri, err)
+ }
+
+ return URIFromPath(path), nil
}
// DirPath returns the file path to the directory containing this URI, which
// must be a file URI.
-func (uri DocumentURI) DirPath() string {
- return filepath.Dir(uri.Path())
+func (uri DocumentURI) DirPath() (string, error) {
+ path, err := uri.Path()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Dir(path), nil
}
func filename(uri DocumentURI) (string, error) {
@@ -11,7 +11,10 @@ import (
)
func applyTextEdits(uri protocol.DocumentURI, edits []protocol.TextEdit) error {
- path := uri.Path()
+ path, err := uri.Path()
+ if err != nil {
+ return fmt.Errorf("invalid URI: %w", err)
+ }
// Read the file content
content, err := os.ReadFile(path)
@@ -148,7 +151,11 @@ func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) {
// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
func applyDocumentChange(change protocol.DocumentChange) error {
if change.CreateFile != nil {
- path := change.CreateFile.URI.Path()
+ path, err := change.CreateFile.URI.Path()
+ if err != nil {
+ return fmt.Errorf("invalid URI: %w", err)
+ }
+
if change.CreateFile.Options != nil {
if change.CreateFile.Options.Overwrite {
// Proceed with overwrite
@@ -164,7 +171,11 @@ func applyDocumentChange(change protocol.DocumentChange) error {
}
if change.DeleteFile != nil {
- path := change.DeleteFile.URI.Path()
+ path, err := change.DeleteFile.URI.Path()
+ if err != nil {
+ return fmt.Errorf("invalid URI: %w", err)
+ }
+
if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to delete directory recursively: %w", err)
@@ -177,8 +188,19 @@ func applyDocumentChange(change protocol.DocumentChange) error {
}
if change.RenameFile != nil {
- oldPath := change.RenameFile.OldURI.Path()
- newPath := change.RenameFile.NewURI.Path()
+ var newPath, oldPath string
+ var err error
+
+ oldPath, err = change.RenameFile.OldURI.Path()
+ if err != nil {
+ return err
+ }
+
+ newPath, err = change.RenameFile.NewURI.Path()
+ if err != nil {
+ return err
+ }
+
if change.RenameFile.Options != nil {
if !change.RenameFile.Options.Overwrite {
if _, err := os.Stat(newPath); err == nil {
@@ -617,7 +617,11 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
return false
}
// For relative patterns
- basePath = protocol.DocumentURI(basePath).Path()
+ if basePath, err = protocol.DocumentURI(basePath).Path(); err != nil {
+ // XXX: Do we want to return here, or send the error up the stack?
+ slog.Error("Error converting base path to URI", "basePath", basePath, "error", err)
+ }
+
basePath = filepath.ToSlash(basePath)
// Make path relative to basePath for matching
@@ -660,7 +664,13 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
// handleFileEvent sends file change notifications
func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
// If the file is open and it's a change event, use didChange notification
- filePath := protocol.DocumentURI(uri).Path()
+ filePath, err := protocol.DocumentURI(uri).Path()
+ if err != nil {
+ // XXX: Do we want to return here, or send the error up the stack?
+ slog.Error("Error converting URI to path", "uri", uri, "error", err)
+ return
+ }
+
if changeType == protocol.FileChangeType(protocol.Deleted) {
w.client.ClearDiagnosticsForURI(protocol.DocumentURI(uri))
} else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
@@ -6,7 +6,6 @@ import (
"image/color"
"math/rand/v2"
"strings"
- "sync"
"sync/atomic"
"time"
@@ -15,6 +14,8 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/lucasb-eyer/go-colorful"
+
+ "github.com/charmbracelet/crush/internal/csync"
)
const (
@@ -72,10 +73,7 @@ type animCache struct {
ellipsisFrames []string
}
-var (
- animCacheMutex sync.RWMutex
- animCacheMap = make(map[string]*animCache)
-)
+var animCacheMap = csync.NewMap[string, *animCache]()
// settingsHash creates a hash key for the settings to use for caching
func settingsHash(opts Settings) string {
@@ -105,22 +103,23 @@ const ()
type Anim struct {
width int
cyclingCharWidth int
- label []string
+ label *csync.Slice[string]
labelWidth int
labelColor color.Color
startTime time.Time
birthOffsets []time.Duration
initialFrames [][]string // frames for the initial characters
- initialized bool
- cyclingFrames [][]string // frames for the cycling characters
- step int // current main frame step
- ellipsisStep int // current ellipsis frame step
- ellipsisFrames []string // ellipsis animation frames
+ initialized atomic.Bool
+ cyclingFrames [][]string // frames for the cycling characters
+ step atomic.Int64 // current main frame step
+ ellipsisStep atomic.Int64 // current ellipsis frame step
+ ellipsisFrames *csync.Slice[string] // ellipsis animation frames
id int
}
// New creates a new Anim instance with the specified width and label.
-func New(opts Settings) (a Anim) {
+func New(opts Settings) *Anim {
+ a := &Anim{}
// Validate settings.
if opts.Size < 1 {
opts.Size = defaultNumCyclingChars
@@ -142,16 +141,14 @@ func New(opts Settings) (a Anim) {
// Check cache first
cacheKey := settingsHash(opts)
- animCacheMutex.RLock()
- cached, exists := animCacheMap[cacheKey]
- animCacheMutex.RUnlock()
+ cached, exists := animCacheMap.Get(cacheKey)
if exists {
// Use cached values
a.width = cached.width
a.labelWidth = cached.labelWidth
- a.label = cached.label
- a.ellipsisFrames = cached.ellipsisFrames
+ a.label = csync.NewSliceFrom(cached.label)
+ a.ellipsisFrames = csync.NewSliceFrom(cached.ellipsisFrames)
a.initialFrames = cached.initialFrames
a.cyclingFrames = cached.cyclingFrames
} else {
@@ -228,17 +225,23 @@ func New(opts Settings) (a Anim) {
}
// Cache the results
+ labelSlice := make([]string, a.label.Len())
+ for i, v := range a.label.Seq2() {
+ labelSlice[i] = v
+ }
+ ellipsisSlice := make([]string, a.ellipsisFrames.Len())
+ for i, v := range a.ellipsisFrames.Seq2() {
+ ellipsisSlice[i] = v
+ }
cached = &animCache{
initialFrames: a.initialFrames,
cyclingFrames: a.cyclingFrames,
width: a.width,
labelWidth: a.labelWidth,
- label: a.label,
- ellipsisFrames: a.ellipsisFrames,
+ label: labelSlice,
+ ellipsisFrames: ellipsisSlice,
}
- animCacheMutex.Lock()
- animCacheMap[cacheKey] = cached
- animCacheMutex.Unlock()
+ animCacheMap.Set(cacheKey, cached)
}
// Random assign a birth to each character for a stagged entrance effect.
@@ -269,28 +272,30 @@ func (a *Anim) renderLabel(label string) {
if a.labelWidth > 0 {
// Pre-render the label.
labelRunes := []rune(label)
- a.label = make([]string, len(labelRunes))
- for i := range a.label {
- a.label[i] = lipgloss.NewStyle().
+ a.label = csync.NewSlice[string]()
+ for i := range labelRunes {
+ rendered := lipgloss.NewStyle().
Foreground(a.labelColor).
Render(string(labelRunes[i]))
+ a.label.Append(rendered)
}
// Pre-render the ellipsis frames which come after the label.
- a.ellipsisFrames = make([]string, len(ellipsisFrames))
- for i, frame := range ellipsisFrames {
- a.ellipsisFrames[i] = lipgloss.NewStyle().
+ a.ellipsisFrames = csync.NewSlice[string]()
+ for _, frame := range ellipsisFrames {
+ rendered := lipgloss.NewStyle().
Foreground(a.labelColor).
Render(frame)
+ a.ellipsisFrames.Append(rendered)
}
} else {
- a.label = nil
- a.ellipsisFrames = nil
+ a.label = csync.NewSlice[string]()
+ a.ellipsisFrames = csync.NewSlice[string]()
}
}
// Width returns the total width of the animation.
-func (a Anim) Width() (w int) {
+func (a *Anim) Width() (w int) {
w = a.width
if a.labelWidth > 0 {
w += labelGapWidth + a.labelWidth
@@ -308,12 +313,12 @@ func (a Anim) Width() (w int) {
}
// Init starts the animation.
-func (a Anim) Init() tea.Cmd {
+func (a *Anim) Init() tea.Cmd {
return a.Step()
}
// Update processes animation steps (or not).
-func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case StepMsg:
if msg.id != a.id {
@@ -321,19 +326,19 @@ func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, nil
}
- a.step++
- if a.step >= len(a.cyclingFrames) {
- a.step = 0
+ step := a.step.Add(1)
+ if int(step) >= len(a.cyclingFrames) {
+ a.step.Store(0)
}
- if a.initialized && a.labelWidth > 0 {
+ if a.initialized.Load() && a.labelWidth > 0 {
// Manage the ellipsis animation.
- a.ellipsisStep++
- if a.ellipsisStep >= ellipsisAnimSpeed*len(ellipsisFrames) {
- a.ellipsisStep = 0
+ ellipsisStep := a.ellipsisStep.Add(1)
+ if int(ellipsisStep) >= ellipsisAnimSpeed*len(ellipsisFrames) {
+ a.ellipsisStep.Store(0)
}
- } else if !a.initialized && time.Since(a.startTime) >= maxBirthOffset {
- a.initialized = true
+ } else if !a.initialized.Load() && time.Since(a.startTime) >= maxBirthOffset {
+ a.initialized.Store(true)
}
return a, a.Step()
default:
@@ -342,35 +347,41 @@ func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// View renders the current state of the animation.
-func (a Anim) View() string {
+func (a *Anim) View() string {
var b strings.Builder
+ step := int(a.step.Load())
for i := range a.width {
switch {
- case !a.initialized && i < len(a.birthOffsets) && time.Since(a.startTime) < a.birthOffsets[i]:
+ case !a.initialized.Load() && i < len(a.birthOffsets) && time.Since(a.startTime) < a.birthOffsets[i]:
// Birth offset not reached: render initial character.
- b.WriteString(a.initialFrames[a.step][i])
+ b.WriteString(a.initialFrames[step][i])
case i < a.cyclingCharWidth:
// Render a cycling character.
- b.WriteString(a.cyclingFrames[a.step][i])
+ b.WriteString(a.cyclingFrames[step][i])
case i == a.cyclingCharWidth:
// Render label gap.
b.WriteString(labelGap)
case i > a.cyclingCharWidth:
// Label.
- b.WriteString(a.label[i-a.cyclingCharWidth-labelGapWidth])
+ if labelChar, ok := a.label.Get(i - a.cyclingCharWidth - labelGapWidth); ok {
+ b.WriteString(labelChar)
+ }
}
}
// Render animated ellipsis at the end of the label if all characters
// have been initialized.
- if a.initialized && a.labelWidth > 0 {
- b.WriteString(a.ellipsisFrames[a.ellipsisStep/ellipsisAnimSpeed])
+ if a.initialized.Load() && a.labelWidth > 0 {
+ ellipsisStep := int(a.ellipsisStep.Load())
+ if ellipsisFrame, ok := a.ellipsisFrames.Get(ellipsisStep / ellipsisAnimSpeed); ok {
+ b.WriteString(ellipsisFrame)
+ }
}
return b.String()
}
// Step is a command that triggers the next step in the animation.
-func (a Anim) Step() tea.Cmd {
+func (a *Anim) Step() tea.Cmd {
return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg {
return StepMsg{id: a.id}
})
@@ -56,7 +56,7 @@ func (m model) View() tea.View {
return v
}
- if a, ok := m.anim.(anim.Anim); ok {
+ if a, ok := m.anim.(*anim.Anim); ok {
l := lipgloss.NewLayer(a.View()).
Width(a.Width()).
X(m.w/2 - a.Width()/2).
@@ -49,7 +49,7 @@ type messageCmp struct {
// Core message data and state
message message.Message // The underlying message content
spinning bool // Whether to show loading animation
- anim anim.Anim // Animation component for loading states
+ anim *anim.Anim // Animation component for loading states
// Thinking viewport for displaying reasoning content
thinkingViewport viewport.Model
@@ -95,7 +95,7 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.spinning = m.shouldSpin()
if m.spinning {
u, cmd := m.anim.Update(msg)
- m.anim = u.(anim.Anim)
+ m.anim = u.(*anim.Anim)
return m, cmd
}
case tea.KeyPressMsg:
@@ -778,7 +778,7 @@ func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
// shouldSpin determines whether the tool call should show a loading animation.
// Returns true if the tool call is not finished or if the result doesn't match the call ID.
func (m *toolCallCmp) shouldSpin() bool {
- return !m.call.Finished
+ return !m.call.Finished && !m.cancelled
}
// Spinning returns whether the tool call is currently showing a loading animation
@@ -614,6 +614,7 @@ func (m *sidebarCmp) mcpBlockCompact(maxWidth int) string {
}
func (m *sidebarCmp) filesBlock() string {
+ return ""
t := styles.CurrentTheme()
section := t.S().Subtle.Render(
@@ -97,7 +97,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption
f.list = New(items, f.listOptions...).(*list[T])
f.updateKeyMaps()
- f.items = f.list.items.Slice()
+ f.items = slices.Collect(f.list.items.Seq())
if f.inputHidden {
return f
@@ -2,6 +2,7 @@ package list
import (
"regexp"
+ "slices"
"sort"
"strings"
@@ -179,7 +180,7 @@ func (f *filterableGroupList[T]) inputHeight() int {
func (f *filterableGroupList[T]) Filter(query string) tea.Cmd {
var cmds []tea.Cmd
- for _, item := range f.items.Slice() {
+ for _, item := range slices.Collect(f.items.Seq()) {
if i, ok := any(item).(layout.Focusable); ok {
cmds = append(cmds, i.Blur())
}
@@ -1,6 +1,8 @@
package list
import (
+ "slices"
+
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
@@ -89,7 +91,7 @@ func (g *groupedList[T]) convertItems() {
func (g *groupedList[T]) SetGroups(groups []Group[T]) tea.Cmd {
g.groups = groups
g.convertItems()
- return g.SetItems(g.items.Slice())
+ return g.SetItems(slices.Collect(g.items.Seq()))
}
func (g *groupedList[T]) Groups() []Group[T] {
@@ -1,6 +1,7 @@
package list
import (
+ "slices"
"strings"
"github.com/charmbracelet/bubbles/v2/key"
@@ -201,7 +202,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return l, nil
case anim.StepMsg:
var cmds []tea.Cmd
- for _, item := range l.items.Slice() {
+ for _, item := range slices.Collect(l.items.Seq()) {
if i, ok := any(item).(HasAnim); ok && i.Spinning() {
updated, cmd := i.Update(msg)
cmds = append(cmds, cmd)
@@ -300,7 +301,7 @@ func (l *list[T]) viewPosition() (int, int) {
func (l *list[T]) recalculateItemPositions() {
currentContentHeight := 0
- for _, item := range l.items.Slice() {
+ for _, item := range slices.Collect(l.items.Seq()) {
rItem, ok := l.renderedItems.Get(item.ID())
if !ok {
continue
@@ -573,7 +574,7 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
return nil
}
var cmds []tea.Cmd
- for _, item := range l.items.Slice() {
+ for _, item := range slices.Collect(l.items.Seq()) {
if f, ok := any(item).(layout.Focusable); ok {
if item.ID() == l.selectedItem && !f.IsFocused() {
cmds = append(cmds, f.Focus())
@@ -592,7 +593,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
return nil
}
var cmds []tea.Cmd
- for _, item := range l.items.Slice() {
+ for _, item := range slices.Collect(l.items.Seq()) {
if f, ok := any(item).(layout.Focusable); ok {
if item.ID() == l.selectedItem && f.IsFocused() {
cmds = append(cmds, f.Blur())
@@ -667,7 +668,7 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
l.items.Append(item)
l.indexMap = csync.NewMap[string, int]()
- for inx, item := range l.items.Slice() {
+ for inx, item := range slices.Collect(l.items.Seq()) {
l.indexMap.Set(item.ID(), inx)
}
if l.width > 0 && l.height > 0 {
@@ -714,7 +715,7 @@ func (l *list[T]) DeleteItem(id string) tea.Cmd {
}
l.items.Delete(inx)
l.renderedItems.Del(id)
- for inx, item := range l.items.Slice() {
+ for inx, item := range slices.Collect(l.items.Seq()) {
l.indexMap.Set(item.ID(), inx)
}
@@ -779,7 +780,7 @@ func (l *list[T]) IsFocused() bool {
// Items implements List.
func (l *list[T]) Items() []T {
- return l.items.Slice()
+ return slices.Collect(l.items.Seq())
}
func (l *list[T]) incrementOffset(n int) {
@@ -834,7 +835,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
}
l.items.Prepend(item)
l.indexMap = csync.NewMap[string, int]()
- for inx, item := range l.items.Slice() {
+ for inx, item := range slices.Collect(l.items.Seq()) {
l.indexMap.Set(item.ID(), inx)
}
if l.width > 0 && l.height > 0 {
@@ -938,7 +939,7 @@ func (l *list[T]) SelectedItem() *T {
func (l *list[T]) SetItems(items []T) tea.Cmd {
l.items.SetSlice(items)
var cmds []tea.Cmd
- for inx, item := range l.items.Slice() {
+ for inx, item := range slices.Collect(l.items.Seq()) {
if i, ok := any(item).(Indexable); ok {
i.SetIndex(inx)
}
@@ -961,7 +962,7 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
l.selectedItem = selectedItem
l.indexMap = csync.NewMap[string, int]()
l.renderedItems = csync.NewMap[string, renderedItem]()
- for inx, item := range l.items.Slice() {
+ for inx, item := range slices.Collect(l.items.Seq()) {
l.indexMap.Set(item.ID(), inx)
if l.width > 0 && l.height > 0 {
cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -860,7 +860,7 @@ func (p *chatPage) Help() help.KeyMap {
),
key.NewBinding(
key.WithKeys("g", "home"),
- key.WithHelp("g", "hone"),
+ key.WithHelp("g", "home"),
),
key.NewBinding(
key.WithKeys("G", "end"),