summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-27 06:55:27 +0200
committerPaul Buetow <paul@buetow.org>2026-03-27 06:55:27 +0200
commite2c600f6d80785c0d41853ae1fd6701c2f63db17 (patch)
tree7ebf7d7132874ece0801b4161eb9cf8f30dc2c64
parentf19a4740992f3fd5f6d2f8b13850c9eb637f6a76 (diff)
fix ask description width 84f1af4e-be84-4265-9df2-8f6932059913
-rw-r--r--internal/askcli/command_list.go2
-rw-r--r--internal/askcli/command_urgency.go2
-rw-r--r--internal/askcli/formatter.go49
-rw-r--r--internal/askcli/formatter_test.go61
-rw-r--r--internal/termprint/columns.go8
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}