diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-27 06:55:27 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-27 06:55:27 +0200 |
| commit | e2c600f6d80785c0d41853ae1fd6701c2f63db17 (patch) | |
| tree | 7ebf7d7132874ece0801b4161eb9cf8f30dc2c64 | |
| parent | f19a4740992f3fd5f6d2f8b13850c9eb637f6a76 (diff) | |
fix ask description width 84f1af4e-be84-4265-9df2-8f6932059913
| -rw-r--r-- | internal/askcli/command_list.go | 2 | ||||
| -rw-r--r-- | internal/askcli/command_urgency.go | 2 | ||||
| -rw-r--r-- | internal/askcli/formatter.go | 49 | ||||
| -rw-r--r-- | internal/askcli/formatter_test.go | 61 | ||||
| -rw-r--r-- | internal/termprint/columns.go | 8 |
5 files changed, 107 insertions, 15 deletions
diff --git a/internal/askcli/command_list.go b/internal/askcli/command_list.go index c95707e..2e5dfa2 100644 --- a/internal/askcli/command_list.go +++ b/internal/askcli/command_list.go @@ -66,7 +66,7 @@ func (d Dispatcher) handleListWithFilters(ctx context.Context, initialFilters, e fmt.Fprintf(stderr, "error: failed to load task aliases: %v\n", err) return 1, nil } - io.WriteString(stdout, FormatTaskList(tasks, aliases)) + io.WriteString(stdout, FormatTaskListForWidth(tasks, aliases, detectTaskListTerminalWidth(stdout))) } return 0, nil } diff --git a/internal/askcli/command_urgency.go b/internal/askcli/command_urgency.go index 0a6bf65..c1ec24c 100644 --- a/internal/askcli/command_urgency.go +++ b/internal/askcli/command_urgency.go @@ -37,7 +37,7 @@ func (d Dispatcher) handleUrgency(ctx context.Context, stdout, stderr io.Writer) fmt.Fprintf(stderr, "error: failed to load task aliases: %v\n", err) return 1, nil } - io.WriteString(stdout, FormatTaskList(tasks, aliases)) + io.WriteString(stdout, FormatTaskListForWidth(tasks, aliases, detectTaskListTerminalWidth(stdout))) } return 0, nil } diff --git a/internal/askcli/formatter.go b/internal/askcli/formatter.go index 35ac0ba..a181c1a 100644 --- a/internal/askcli/formatter.go +++ b/internal/askcli/formatter.go @@ -5,10 +5,16 @@ import ( "io" "slices" "strings" + + "codeberg.org/snonux/hexai/internal/termprint" ) func FormatTaskList(tasks []TaskExport, aliases map[string]string) string { - widths := taskListWidthsFor(tasks, aliases) + return FormatTaskListForWidth(tasks, aliases, 0) +} + +func FormatTaskListForWidth(tasks []TaskExport, aliases map[string]string, terminalWidth int) string { + widths := taskListWidthsFor(tasks, aliases, terminalWidth) var b strings.Builder writeTaskListHeader(&b, widths) writeTaskListSeparator(&b, widths) @@ -28,7 +34,7 @@ type taskListWidths struct { Description int } -func taskListWidthsFor(tasks []TaskExport, aliases map[string]string) taskListWidths { +func taskListWidthsFor(tasks []TaskExport, aliases map[string]string, terminalWidth int) taskListWidths { widths := taskListWidths{ Urgency: len("Urgency"), Priority: len("Prio"), @@ -38,6 +44,7 @@ func taskListWidthsFor(tasks []TaskExport, aliases map[string]string) taskListWi Tags: len("Tags"), Description: len("Description"), } + longestDescription := widths.Description for _, t := range tasks { widths.Urgency = maxInt(widths.Urgency, len(fmt.Sprintf("%.1f", t.Urgency))) widths.Priority = maxInt(widths.Priority, len(t.Priority)) @@ -45,8 +52,9 @@ func taskListWidthsFor(tasks []TaskExport, aliases map[string]string) taskListWi widths.Status = maxInt(widths.Status, len(t.Status)) widths.Started = maxInt(widths.Started, len(formatTaskStarted(t))) widths.Tags = maxInt(widths.Tags, len(formatTaskTags(t.Tags))) - widths.Description = maxInt(widths.Description, len(formatTaskDescription(t.Description))) + longestDescription = maxInt(longestDescription, len(t.Description)) } + widths.Description = taskListDescriptionWidth(widths, terminalWidth, longestDescription) return widths } @@ -75,7 +83,7 @@ func writeTaskListRow(b *strings.Builder, widths taskListWidths, t TaskExport, a widths.Status, t.Status, widths.Started, formatTaskStarted(t), widths.Tags, formatTaskTags(t.Tags), - widths.Description, formatTaskDescription(t.Description), + widths.Description, formatTaskDescription(t.Description, widths.Description), ) } @@ -86,11 +94,14 @@ func formatTaskTags(tags []string) string { return strings.Join(tags, ",") } -func formatTaskDescription(desc string) string { - if len(desc) > 50 { - return desc[:47] + "..." +func formatTaskDescription(desc string, width int) string { + if width <= 0 || len(desc) <= width { + return desc + } + if width <= 3 { + return desc[:width] } - return desc + return desc[:width-3] + "..." } func formatTaskStarted(t TaskExport) string { @@ -107,6 +118,28 @@ func maxInt(a, b int) int { return b } +func taskListDescriptionWidth(widths taskListWidths, terminalWidth, longestDescription int) int { + if terminalWidth <= 0 { + return longestDescription + } + available := terminalWidth - taskListFixedWidth(widths) + if available < len("Description") { + return len("Description") + } + if available < longestDescription { + return available + } + return longestDescription +} + +func taskListFixedWidth(widths taskListWidths) int { + return widths.Urgency + widths.Priority + widths.ID + widths.Status + widths.Started + widths.Tags + 18 +} + +func detectTaskListTerminalWidth(w io.Writer) int { + return termprint.DetectTerminalWidth(w) +} + func FormatTaskInfo(t TaskExport, alias string, dependencyAliases map[string]string) string { var b strings.Builder fmt.Fprintf(&b, "ID: %s\n", alias) diff --git a/internal/askcli/formatter_test.go b/internal/askcli/formatter_test.go index cd825ad..1f36b6b 100644 --- a/internal/askcli/formatter_test.go +++ b/internal/askcli/formatter_test.go @@ -30,8 +30,8 @@ func TestFormatTaskList(t *testing.T) { if !strings.Contains(lines[2], "0") || strings.Contains(lines[2], "uuid-1") { t.Fatalf("first task line should show alias only: %s", lines[2]) } - if !strings.Contains(lines[3], "...") { - t.Fatalf("long description should be truncated with ...: %s", lines[3]) + if !strings.Contains(lines[3], strings.Repeat("a", 100)) { + t.Fatalf("default formatting should keep the full description when width is unconstrained: %s", lines[3]) } } @@ -62,7 +62,7 @@ func TestFormatTaskList_AlignsHeaderAndSeparator(t *testing.T) { t.Fatalf("FormatTaskList produced %d lines, want 4: %q", len(lines), output) } - widths := taskListWidthsFor(tasks, aliases) + widths := taskListWidthsFor(tasks, aliases, 0) wantHeader := fmt.Sprintf("%-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s", widths.Urgency, "Urgency", widths.Priority, "Prio", @@ -89,6 +89,61 @@ func TestFormatTaskList_FallsBackToUUIDWithoutAlias(t *testing.T) { } } +func TestFormatTaskListForWidth_UsesAvailableTerminalWidthForDescription(t *testing.T) { + tasks := []TaskExport{ + { + UUID: "uuid-1", + Description: strings.Repeat("x", 70), + Status: "pending", + Priority: "H", + Tags: []string{"cli"}, + Urgency: 1.0, + }, + } + + aliases := map[string]string{"uuid-1": "0"} + terminalWidth := 110 + output := FormatTaskListForWidth(tasks, aliases, terminalWidth) + lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") + widths := taskListWidthsFor(tasks, aliases, terminalWidth) + if widths.Description <= 50 { + t.Fatalf("description width = %d, want > 50 for this terminal width", widths.Description) + } + if got := len(lines[0]); got != taskListFixedWidth(widths)+widths.Description { + t.Fatalf("header width = %d, want %d: %q", got, taskListFixedWidth(widths)+widths.Description, lines[0]) + } + renderedDescription := strings.Split(lines[2], " | ")[6] + if renderedDescription != strings.Repeat("x", widths.Description-3)+"..." { + t.Fatalf("description should expand to the available terminal width: %s", lines[2]) + } + if len(renderedDescription) != widths.Description { + t.Fatalf("rendered description width = %d, want %d", len(renderedDescription), widths.Description) + } +} + +func TestFormatTaskListForWidth_TruncatesDescriptionWhenTerminalIsNarrow(t *testing.T) { + tasks := []TaskExport{ + { + UUID: "uuid-1", + Description: "abcdefghijklmnop", + Status: "pending", + Priority: "H", + Tags: []string{"cli"}, + Urgency: 1.0, + }, + } + + aliases := map[string]string{"uuid-1": "0"} + output := FormatTaskListForWidth(tasks, aliases, 40) + lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") + if !strings.Contains(lines[2], "abcdefgh...") { + t.Fatalf("description should truncate to fit a narrow terminal: %s", lines[2]) + } + if strings.Contains(lines[2], "abcdefghijklmnop") { + t.Fatalf("description should not print the full description in a narrow terminal: %s", lines[2]) + } +} + func TestFormatTaskInfo(t *testing.T) { task := TaskExport{ UUID: "test-uuid", diff --git a/internal/termprint/columns.go b/internal/termprint/columns.go index 25cc07e..5950d76 100644 --- a/internal/termprint/columns.go +++ b/internal/termprint/columns.go @@ -36,7 +36,7 @@ func NewColumnPrinter(stdout io.Writer, providers []string, models []string) *Co return nil } - width := detectTerminalWidth(stdout) + width := DetectTerminalWidth(stdout) if width <= 0 { width = 100 } @@ -61,7 +61,7 @@ func NewColumnPrinter(stdout io.Writer, providers []string, models []string) *Co } } -func detectTerminalWidth(w io.Writer) int { +func DetectTerminalWidth(w io.Writer) int { type fder interface{ Fd() uintptr } if f, ok := w.(*os.File); ok { if width, _, err := term.GetSize(int(f.Fd())); err == nil { @@ -76,6 +76,10 @@ func detectTerminalWidth(w io.Writer) int { return 0 } +func detectTerminalWidth(w io.Writer) int { + return DetectTerminalWidth(w) +} + // Writer returns an io.Writer that routes chunks to a single column index. func (cp *ColumnPrinter) Writer(idx int) io.Writer { return columnWriter{printer: cp, index: idx} |
