Breite im Terminal begrenzen
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"royalroadupdates/internal/royalroad"
|
"royalroadupdates/internal/royalroad"
|
||||||
)
|
)
|
||||||
@@ -17,17 +18,46 @@ func PrintTable(w io.Writer, stories []royalroad.Story) {
|
|||||||
nameHeader := "Story Name"
|
nameHeader := "Story Name"
|
||||||
updateHeader := "Last Update"
|
updateHeader := "Last Update"
|
||||||
|
|
||||||
maxNameLen := len(nameHeader)
|
// 1. Measure natural widths in runes
|
||||||
|
maxNameLen := utf8.RuneCountInString(nameHeader)
|
||||||
|
maxUpdateLen := utf8.RuneCountInString(updateHeader)
|
||||||
|
|
||||||
for _, s := range stories {
|
for _, s := range stories {
|
||||||
if l := len(s.Name); l > maxNameLen {
|
if l := utf8.RuneCountInString(s.Name); l > maxNameLen {
|
||||||
maxNameLen = l
|
maxNameLen = l
|
||||||
}
|
}
|
||||||
|
if l := utf8.RuneCountInString(Relative(s.DateModified)); l > maxUpdateLen {
|
||||||
|
maxUpdateLen = l
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, "%-*s %s\n", maxNameLen, nameHeader, updateHeader)
|
// 2. Determine final name column width based on terminal availability
|
||||||
fmt.Fprintf(w, "%s %s\n", strings.Repeat("-", maxNameLen), strings.Repeat("-", len(updateHeader)))
|
nameWidth := maxNameLen
|
||||||
|
const minNameWidth = 30
|
||||||
|
const gutter = 2
|
||||||
|
|
||||||
|
termWidth, err := getTerminalWidth()
|
||||||
|
if err == nil {
|
||||||
|
// We have a terminal. check if it's wide enough to support a cap above the min.
|
||||||
|
if termWidth >= minNameWidth+gutter+maxUpdateLen {
|
||||||
|
availableForName := termWidth - gutter - maxUpdateLen
|
||||||
|
if nameWidth > availableForName {
|
||||||
|
nameWidth = availableForName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Render Table
|
||||||
|
fmt.Fprintf(w, "%-*s %s\n", nameWidth, nameHeader, updateHeader)
|
||||||
|
fmt.Fprintf(w, "%s %s\n", strings.Repeat("-", nameWidth), strings.Repeat("-", maxUpdateLen))
|
||||||
|
|
||||||
for _, s := range stories {
|
for _, s := range stories {
|
||||||
fmt.Fprintf(w, "%-*s %s\n", maxNameLen, s.Name, Relative(s.DateModified))
|
displayName := s.Name
|
||||||
|
runeName := []rune(displayName)
|
||||||
|
if len(runeName) > nameWidth {
|
||||||
|
// Truncate to (nameWidth - 3) and add ellipsis
|
||||||
|
displayName = string(runeName[:nameWidth-3]) + "..."
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%-*s %s\n", nameWidth, displayName, Relative(s.DateModified))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
internal/formatter/table_test.go
Normal file
58
internal/formatter/table_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"royalroadupdates/internal/royalroad"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrintTableTruncation(t *testing.T) {
|
||||||
|
stories := []royalroad.Story{
|
||||||
|
{
|
||||||
|
Name: "Short Name",
|
||||||
|
DateModified: time.Now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "This is a very long story name that should definitely be truncated if the column width is restricted",
|
||||||
|
DateModified: time.Now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Unicode Story 🌟 Sparkle",
|
||||||
|
DateModified: time.Now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Natural width (no terminal)", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
PrintTable(&buf, stories)
|
||||||
|
out := buf.String()
|
||||||
|
|
||||||
|
expectedLongName := "This is a very long story name that should definitely be truncated if the column width is restricted"
|
||||||
|
if !strings.Contains(out, expectedLongName) {
|
||||||
|
t.Errorf("Expected output to contain full long name when not truncated, but it didn't. Output:\n%s", out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Check relative time alignment", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
PrintTable(&buf, stories)
|
||||||
|
out := buf.String()
|
||||||
|
|
||||||
|
lines := strings.Split(out, "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
t.Fatalf("Expected at least 2 lines of output, got %d", len(lines))
|
||||||
|
}
|
||||||
|
separatorLine := lines[1]
|
||||||
|
parts := strings.Split(separatorLine, " ")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
t.Fatalf("Separator line does not contain expected gutter: %s", separatorLine)
|
||||||
|
}
|
||||||
|
updateDashes := parts[1]
|
||||||
|
if len(updateDashes) < len("Last Update") {
|
||||||
|
t.Errorf("Update column separator too short: %d < 11", len(updateDashes))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
29
internal/formatter/terminal_linux.go
Normal file
29
internal/formatter/terminal_linux.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type winsize struct {
|
||||||
|
Row, Col uint16
|
||||||
|
Xpixel, Ypixel uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTerminalWidth returns the width of the stdout terminal in columns.
|
||||||
|
// If the output is not a terminal or the width cannot be determined, it returns (0, error).
|
||||||
|
func getTerminalWidth() (int, error) {
|
||||||
|
var ws winsize
|
||||||
|
_, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
uintptr(syscall.Stdout),
|
||||||
|
uintptr(syscall.TIOCGWINSZ),
|
||||||
|
uintptr(unsafe.Pointer(&ws)),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return 0, errno
|
||||||
|
}
|
||||||
|
return int(ws.Col), nil
|
||||||
|
}
|
||||||
11
internal/formatter/terminal_other.go
Normal file
11
internal/formatter/terminal_other.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// getTerminalWidth returns the width of the stdout terminal in columns.
|
||||||
|
// On non-linux platforms, this is not implemented and always returns an error.
|
||||||
|
func getTerminalWidth() (int, error) {
|
||||||
|
return 0, errors.New("not implemented on this platform")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user