Skip to content

Commit 99edb3b

Browse files
authored
fix: don't pad after last column (#53)
We were outputting space after the last column if the terminal wasn't wide enough to display the last column in the specified format, which caused unsightly blank lines in the output fix it by adding an `isLast` bool to the column output functions
1 parent 2968357 commit 99edb3b

3 files changed

Lines changed: 158 additions & 43 deletions

File tree

columns.go

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func calculateColumnWidths(files []*File, columns []Column, rctx *RenderContext)
213213
}
214214

215215
// renderStatus renders the status column
216-
func renderStatus(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
216+
func renderStatus(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
217217
if maxWidth > 0 {
218218
var statusStr string
219219
if rctx.NerdFont {
@@ -227,21 +227,24 @@ func renderStatus(out io.Writer, file *File, maxWidth int, rctx *RenderContext)
227227
must(fmt.Fprintf(out, " "))
228228
}
229229
must(fmt.Fprintf(out, "%s", statusStr))
230+
// No right padding needed for status since it's right-aligned
230231
}
231232
}
232233

233234
// renderDiff renders the diff graph column
234-
func renderDiff(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
235+
func renderDiff(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
235236
if maxWidth > 0 {
236237
must(fmt.Fprintf(out, "%s", file.diffStat))
237-
for i := 0; i < maxWidth-width(file.diffStat); i++ {
238-
must(fmt.Fprintf(out, " "))
238+
if !isLast {
239+
for i := 0; i < maxWidth-width(file.diffStat); i++ {
240+
must(fmt.Fprintf(out, " "))
241+
}
239242
}
240243
}
241244
}
242245

243246
// renderFilename renders the filename column with colors and hyperlinks
244-
func renderFilename(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
247+
func renderFilename(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
245248
if file.isDeleted {
246249
must(fmt.Fprintf(out, "%s%s", RED, STRIKEOUT))
247250
} else if file.isDir {
@@ -258,9 +261,11 @@ func renderFilename(out io.Writer, file *File, maxWidth int, rctx *RenderContext
258261
must(fmt.Fprintf(out, "%s", link(fileURL, file.Name())))
259262
}
260263

261-
// pad spaces to the right up to maxWidth
262-
for i := 0; i < maxWidth-width(file.Name()); i++ {
263-
must(fmt.Fprintf(out, " "))
264+
// pad spaces to the right up to maxWidth (unless this is the last column)
265+
if !isLast {
266+
for i := 0; i < maxWidth-width(file.Name()); i++ {
267+
must(fmt.Fprintf(out, " "))
268+
}
264269
}
265270

266271
// reset color for dir/exe but not deleted (strikethrough continues)
@@ -270,7 +275,7 @@ func renderFilename(out io.Writer, file *File, maxWidth int, rctx *RenderContext
270275
}
271276

272277
// renderShorthash renders the short commit hash
273-
func renderShorthash(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
278+
func renderShorthash(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
274279
if maxWidth > 0 {
275280
shortHash := file.shortHash
276281
shortHashWidth := min(width(shortHash), maxWidth)
@@ -288,15 +293,17 @@ func renderShorthash(out io.Writer, file *File, maxWidth int, rctx *RenderContex
288293
must(fmt.Fprintf(out, "%s%s%s", color, shortHash[:min(len(shortHash), maxWidth)], RESET))
289294
}
290295

291-
// pad spaces to the right up to maxWidth
292-
for i := 0; i < maxWidth-shortHashWidth; i++ {
293-
must(fmt.Fprintf(out, " "))
296+
// pad spaces to the right up to maxWidth (unless this is the last column)
297+
if !isLast {
298+
for i := 0; i < maxWidth-shortHashWidth; i++ {
299+
must(fmt.Fprintf(out, " "))
300+
}
294301
}
295302
}
296303
}
297304

298305
// renderHash renders the full commit hash
299-
func renderHash(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
306+
func renderHash(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
300307
if maxWidth > 0 {
301308
var color string
302309
if rctx.MonoHash {
@@ -311,24 +318,28 @@ func renderHash(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
311318
must(fmt.Fprintf(out, "%s%s%s", color, file.hash, RESET))
312319
}
313320

314-
// pad spaces to the right up to maxWidth
315-
for i := 0; i < maxWidth-width(file.hash); i++ {
316-
must(fmt.Fprintf(out, " "))
321+
// pad spaces to the right up to maxWidth (unless this is the last column)
322+
if !isLast {
323+
for i := 0; i < maxWidth-width(file.hash); i++ {
324+
must(fmt.Fprintf(out, " "))
325+
}
317326
}
318327
}
319328
}
320329

321330
// renderDate renders the date column
322-
func renderDate(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
331+
func renderDate(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
323332
must(fmt.Fprintf(out, "%s", file.lastModified))
324-
// pad spaces to the right up to maxWidth
325-
for i := 0; i < maxWidth-width(file.lastModified); i++ {
326-
must(fmt.Fprintf(out, " "))
333+
// pad spaces to the right up to maxWidth (unless this is the last column)
334+
if !isLast {
335+
for i := 0; i < maxWidth-width(file.lastModified); i++ {
336+
must(fmt.Fprintf(out, " "))
337+
}
327338
}
328339
}
329340

330341
// renderAuthor renders the author column with hyperlinks
331-
func renderAuthor(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
342+
func renderAuthor(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
332343
authorWidth := min(width(file.author), maxWidth)
333344

334345
// Truncate author string if needed, being careful with multi-byte characters
@@ -349,50 +360,56 @@ func renderAuthor(out io.Writer, file *File, maxWidth int, rctx *RenderContext)
349360
}
350361
}
351362

352-
// pad spaces to the right up to maxWidth
353-
for i := 0; i < maxWidth-width(truncatedAuthor); i++ {
354-
must(fmt.Fprintf(out, " "))
363+
// pad spaces to the right up to maxWidth (unless this is the last column)
364+
if !isLast {
365+
for i := 0; i < maxWidth-width(truncatedAuthor); i++ {
366+
must(fmt.Fprintf(out, " "))
367+
}
355368
}
356369
}
357370

358371
// renderEmail renders the author email column
359-
func renderEmail(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
372+
func renderEmail(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
360373
// Calculate how many characters we can display
361374
emailWidth := min(width(file.authorEmail), maxWidth)
362375

363376
// Truncate email string if needed
364377
truncatedEmail := truncateToWidth(file.authorEmail, emailWidth)
365378
must(fmt.Fprintf(out, "%s", truncatedEmail))
366379

367-
// pad spaces to the right up to maxWidth
368-
for i := 0; i < maxWidth-width(truncatedEmail); i++ {
369-
must(fmt.Fprintf(out, " "))
380+
// pad spaces to the right up to maxWidth (unless this is the last column)
381+
if !isLast {
382+
for i := 0; i < maxWidth-width(truncatedEmail); i++ {
383+
must(fmt.Fprintf(out, " "))
384+
}
370385
}
371386
}
372387

373388
// renderNumstat renders the numeric diffstat
374-
func renderNumstat(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
389+
func renderNumstat(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
375390
if file.diffSum != nil {
376391
numstat := fmt.Sprintf("+%d/-%d", file.diffSum.plus, file.diffSum.minus)
377392
numstatWidth := min(width(numstat), maxWidth)
378393

379394
truncatedNumstat := truncateToWidth(numstat, numstatWidth)
380395
must(fmt.Fprintf(out, "%s", truncatedNumstat))
381396

382-
// pad spaces to the right up to maxWidth
383-
for i := 0; i < maxWidth-width(truncatedNumstat); i++ {
384-
must(fmt.Fprintf(out, " "))
397+
// pad spaces to the right up to maxWidth (unless this is the last column)
398+
if !isLast {
399+
for i := 0; i < maxWidth-width(truncatedNumstat); i++ {
400+
must(fmt.Fprintf(out, " "))
401+
}
385402
}
386-
} else {
387-
// pad spaces if no diffSum
403+
} else if !isLast {
404+
// pad spaces if no diffSum (unless this is the last column)
388405
for range maxWidth {
389406
must(fmt.Fprintf(out, " "))
390407
}
391408
}
392409
}
393410

394411
// renderCommitMessage renders the commit message with issue linkification
395-
func renderCommitMessage(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
412+
func renderCommitMessage(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
396413
// Calculate how many characters we can display
397414
messageWidth := min(width(file.message), maxWidth)
398415

@@ -411,14 +428,16 @@ func renderCommitMessage(out io.Writer, file *File, maxWidth int, rctx *RenderCo
411428
must(fmt.Fprintf(out, "%s", truncatedMessage))
412429
}
413430

414-
// pad spaces to the right up to maxWidth
415-
for i := 0; i < maxWidth-width(truncatedMessage); i++ {
416-
must(fmt.Fprintf(out, " "))
431+
// pad spaces to the right up to maxWidth (unless this is the last column)
432+
if !isLast {
433+
for i := 0; i < maxWidth-width(truncatedMessage); i++ {
434+
must(fmt.Fprintf(out, " "))
435+
}
417436
}
418437
}
419438

420439
// getColumnRenderer returns the renderer function for a column
421-
func getColumnRenderer(col Column) func(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {
440+
func getColumnRenderer(col Column) func(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {
422441
switch col {
423442
case ColStatus:
424443
return renderStatus
@@ -441,6 +460,6 @@ func getColumnRenderer(col Column) func(out io.Writer, file *File, maxWidth int,
441460
case ColCommitMessage:
442461
return renderCommitMessage
443462
default:
444-
return func(out io.Writer, file *File, maxWidth int, rctx *RenderContext) {}
463+
return func(out io.Writer, file *File, maxWidth int, rctx *RenderContext, isLast bool) {}
445464
}
446465
}

columns_test.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"fmt"
56
"strings"
67
"testing"
78
)
@@ -48,7 +49,7 @@ func TestRenderDiffMaxWidth(t *testing.T) {
4849
var buf bytes.Buffer
4950
rctx := &RenderContext{}
5051

51-
renderDiff(&buf, file, tt.maxWidth, rctx)
52+
renderDiff(&buf, file, tt.maxWidth, rctx, false)
5253

5354
output := buf.String()
5455
// Strip ANSI codes to measure actual visible width
@@ -332,7 +333,7 @@ func TestRenderStatusNerdFont(t *testing.T) {
332333
var buf bytes.Buffer
333334
rctx := &RenderContext{NerdFont: tt.nerdFont}
334335

335-
renderStatus(&buf, file, tt.maxWidth, rctx)
336+
renderStatus(&buf, file, tt.maxWidth, rctx, false)
336337

337338
output := buf.String()
338339
visibleOutput := stripANSI(output)
@@ -412,6 +413,88 @@ func TestCalculateColumnWidthsNerdFont(t *testing.T) {
412413
}
413414
}
414415

416+
// TestTerminalWidthBug reproduces the issue where lines become blank when the
417+
// terminal width falls exactly at the end of the author name
418+
func TestTerminalWidthBug(t *testing.T) {
419+
// Create a file with Christina Ahrens Roberts as the author
420+
files := []*File{
421+
{
422+
entry: &mockDirEntry{name: "test.py"},
423+
status: " M",
424+
diffStat: "++--",
425+
shortHash: "7863cc2b54",
426+
hash: "7863cc2b54abc123def456",
427+
lastModified: "2026-03-26",
428+
author: "Christina Ahrens Roberts",
429+
message: "Update tests",
430+
},
431+
}
432+
433+
columns := AllColumns()
434+
rctx := &RenderContext{
435+
Dir: "/test",
436+
}
437+
438+
// Calculate what the normal column widths would be
439+
colWidths := calculateColumnWidths(files, columns, rctx)
440+
441+
// Calculate the cumulative widths up to and including the author column
442+
cumulativeWidth := 0
443+
for i, col := range columns {
444+
if i > 0 {
445+
cumulativeWidth += 1 // space between columns
446+
}
447+
cumulativeWidth += colWidths[col]
448+
449+
if col == ColAuthor {
450+
t.Logf("Width up to end of author column: %d", cumulativeWidth)
451+
break
452+
}
453+
}
454+
455+
// Test with terminal widths around the author column boundary
456+
testWidths := []int{
457+
cumulativeWidth - 5, // Middle of author name
458+
cumulativeWidth - 1, // Just before end of author name
459+
cumulativeWidth, // Exactly at end of author name
460+
cumulativeWidth + 1, // One character after author name
461+
}
462+
463+
for _, maxWidth := range testWidths {
464+
t.Run(fmt.Sprintf("width_%d", maxWidth), func(t *testing.T) {
465+
var buf bytes.Buffer
466+
showColumns(&buf, maxWidth, files, rctx, columns)
467+
468+
output := buf.String()
469+
lines := strings.Split(output, "\n")
470+
471+
for i, line := range lines {
472+
if line == "" {
473+
continue
474+
}
475+
476+
// Strip ANSI codes to measure visible width
477+
visibleLine := stripANSI(line)
478+
actualWidth := len(visibleLine)
479+
480+
t.Logf("Terminal width %d: Line %d visible width = %d", maxWidth, i+1, actualWidth)
481+
t.Logf(" Raw: %q", line)
482+
t.Logf(" Visible: %q", visibleLine)
483+
484+
// The line should not be empty (all whitespace)
485+
if strings.TrimSpace(visibleLine) == "" {
486+
t.Errorf("Line %d is blank at terminal width %d", i+1, maxWidth)
487+
}
488+
489+
// The line should not exceed maxWidth
490+
if actualWidth > maxWidth {
491+
t.Errorf("Line %d exceeds maxWidth: got %d chars, want <= %d", i+1, actualWidth, maxWidth)
492+
}
493+
}
494+
})
495+
}
496+
}
497+
415498
// TestCalculateColumnWidthsWithDiacritics tests that diacritics are handled correctly
416499
func TestCalculateColumnWidthsWithDiacritics(t *testing.T) {
417500
files := []*File{

main.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,10 +892,23 @@ func showColumns(out io.Writer, maxWidth int, files []*File, rctx *RenderContext
892892
availableWidth := maxWidth - lineWidth
893893
colWidth := min(colWidths[col], availableWidth)
894894

895+
// Check if there's room for another column after this one.
896+
// If not, we shouldn't pad this column to avoid trailing spaces.
897+
isLastColumn := (i == len(columns)-1) || (lineWidth+colWidth >= maxWidth)
898+
if !isLastColumn && i+1 < len(columns) {
899+
// Check if next column would fit (need space separator + min 1 char)
900+
isLastColumn = (lineWidth+colWidth+1 >= maxWidth)
901+
}
902+
895903
// Render the column
896904
renderer := getColumnRenderer(col)
897-
renderer(out, file, colWidth, rctx)
905+
renderer(out, file, colWidth, rctx, isLastColumn)
898906
lineWidth += colWidth
907+
908+
// If this was the last column that fits, stop rendering
909+
if isLastColumn {
910+
break
911+
}
899912
}
900913

901914
// Reset any remaining formatting (like strikethrough for deleted files)

0 commit comments

Comments
 (0)