diff --git a/main.go b/main.go index a494a8b..d21079d 100644 --- a/main.go +++ b/main.go @@ -145,7 +145,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.listHeight < 3 { m.listHeight = 3 } - return m, nil + // Clear so a shrink doesn't leave wider stale rows behind. + return m, tea.ClearScreen case tea.MouseMsg: // Determine if mouse is in preview area (below list + separator) @@ -260,8 +261,8 @@ func (m model) View() string { var b strings.Builder - // Table width: 2 + 16 + 2 + 22 + 2 + 34 + 2 + 5 + 2 + 4 + 2 + 6 = 99 - tableWidth := 99 + // The list spans the full terminal width; TOPIC flexes to fill it. + tableWidth := m.width // Title line with help right-aligned title := fmt.Sprintf("ccs · claude code search · %s", version) @@ -309,7 +310,8 @@ func (m model) View() string { previewHeight := m.height - listHeight - 6 // 6 for title + search + blank + header + borders // Column headers - b.WriteString(fmt.Sprintf(" \033[90m%-16s %-22s %-34s %5s %4s %6s\033[0m\n", "DATE", "PROJECT", "TOPIC", "MSGS", "HITS", "SIZE")) + b.WriteString(fmt.Sprintf(" \033[90m%-*s %-*s %-*s %*s %*s %*s\033[0m\n", + colDate, "DATE", colProject, "PROJECT", m.topicColWidth(), "TOPIC", colMsgs, "MSGS", colHits, "HITS", colSize, "SIZE")) b.WriteString(strings.Repeat("─", m.width)) b.WriteString("\n") @@ -351,16 +353,35 @@ func (m model) View() string { return b.String() } +// Fixed list column widths. TOPIC is the flex column - it absorbs the rest of +// the terminal width (see topicColWidth). +const ( + colDate = 16 + colProject = 22 + colMsgs = 5 + colHits = 4 + colSize = 6 + colGap = 2 // spaces between columns + listIndent = 2 // leading " " / "> " on each row + numGaps = 5 +) + +// topicColWidth flexes the TOPIC column to fill the terminal width. +func (m model) topicColWidth() int { + used := listIndent + colDate + colProject + colMsgs + colHits + colSize + numGaps*colGap + if w := m.width - used; w > 10 { + return w + } + return 10 +} + func (m model) formatListItem(item listItem, selected bool) string { ts := formatTimestamp(item.conv.LastTimestamp) project := item.conv.Cwd if idx := strings.LastIndex(project, "/"); idx >= 0 { project = project[idx+1:] } - // Truncate project name to fit column - if len(project) > 22 { - project = project[:19] + "..." - } + project = truncate(project, colProject) // Mark only user-set custom titles. Claude auto-generates an ai-title for // almost every session, so marking any title would flag nearly every row; @@ -371,7 +392,8 @@ func (m model) formatListItem(item listItem, selected bool) string { if item.conv.IsCustomTitle { topic = "✎ " + topic } - topic = truncate(topic, 34) + tw := m.topicColWidth() + topic = truncate(topic, tw) // Message count msgs := len(item.conv.Messages) @@ -392,10 +414,11 @@ func (m model) formatListItem(item listItem, selected bool) string { // Format: date | project | topic | msgs | hits | size (aligned columns) if selected { - return fmt.Sprintf("%-16s %-22s %-34s %5d %4d %6s", ts, project, topic, msgs, hits, size) + return fmt.Sprintf("%-*s %-*s %-*s %*d %*d %*s", + colDate, ts, colProject, project, tw, topic, colMsgs, msgs, colHits, hits, colSize, size) } - return fmt.Sprintf("\033[90m%-16s\033[0m \033[1;33m%-22s\033[0m %-34s %5d \033[36m%4d\033[0m \033[35m%6s\033[0m", - ts, project, topic, msgs, hits, size) + return fmt.Sprintf("\033[90m%-*s\033[0m \033[1;33m%-*s\033[0m %-*s %*d \033[36m%*d\033[0m \033[35m%*s\033[0m", + colDate, ts, colProject, project, tw, topic, colMsgs, msgs, colHits, hits, colSize, size) } // buildPreviewLines builds the scrollable message lines of a conversation diff --git a/main_test.go b/main_test.go index 4606a33..e8d3e12 100644 --- a/main_test.go +++ b/main_test.go @@ -784,6 +784,7 @@ func TestFormatListItemNamedSessionMarker(t *testing.T) { Messages: []Message{{Role: "user", Text: "just a first message"}}, }} m := initialModel([]listItem{custom, aiTitled, fallback}, "", nil) + m.width = 120 // give TOPIC room so the title isn't truncated if got := m.formatListItem(custom, false); !strings.Contains(got, "✎ Refactor auth flow") { t.Errorf("user-set custom title should show the marker, got %q", got) @@ -796,6 +797,38 @@ func TestFormatListItemNamedSessionMarker(t *testing.T) { } } +func TestTopicColWidthFlexes(t *testing.T) { + fixed := listIndent + colDate + colProject + colMsgs + colHits + colSize + numGaps*colGap + for _, w := range []int{80, 120, 200} { + m := model{width: w} + if got, want := m.topicColWidth(), w-fixed; got != want { + t.Errorf("topicColWidth(width=%d) = %d, want %d", w, got, want) + } + } + // Tiny terminal clamps to a minimum rather than going negative. + if got := (model{width: 20}).topicColWidth(); got != 10 { + t.Errorf("topicColWidth(width=20) = %d, want 10 (min)", got) + } +} + +func TestFormatListItemFillsWidth(t *testing.T) { + item := listItem{conv: Conversation{ + SessionID: "s", + LastTimestamp: "2024-01-15T10:30:00Z", + Size: 1 << 20, + Messages: []Message{{Role: "user", Text: "hi"}}, + }} + for _, w := range []int{80, 120, 200} { + m := initialModel([]listItem{item}, "", nil) + m.width = w + // Selected row has no ANSI codes; its width + the 2-char row prefix + // added by View should fill the terminal exactly. + if got := len(m.formatListItem(item, true)); got != w-listIndent { + t.Errorf("width=%d: row length = %d, want %d", w, got, w-listIndent) + } + } +} + func TestUpdateKeyboardNavigation(t *testing.T) { items := []listItem{ {conv: Conversation{SessionID: "test-1"}, searchText: "first"},