From ec374d7d0621daa56d7b5fa717eb2a9b515e27b6 Mon Sep 17 00:00:00 2001 From: "Heiko Schlittermann (HS12-RIPE)" Date: Sat, 30 May 2026 15:06:21 +0200 Subject: [PATCH] fix: use /dev/tty for width autodetect; default config width: 0 --- config_cmd.go | 4 ++-- glow_test.go | 37 +++++++++++++++++++++++++++++++++++++ main.go | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/config_cmd.go b/config_cmd.go index 390a1db..2bf5ffd 100644 --- a/config_cmd.go +++ b/config_cmd.go @@ -19,8 +19,8 @@ style: "auto" mouse: false # use pager to display markdown pager: false -# word-wrap at width -width: 80 +# word-wrap at width (0 = autodetect terminal width) +width: 0 # show all files, including hidden and ignored. all: false ` diff --git a/glow_test.go b/glow_test.go index 8743be2..46f7772 100644 --- a/glow_test.go +++ b/glow_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "testing" ) @@ -39,3 +40,39 @@ func TestGlowFlags(t *testing.T) { } } } + +// TestTerminalWidthFromFdRejectsPipe ensures that a pipe (the canonical +// non-tty fd type) is reported as "not a terminal" rather than e.g. +// returning a stale or zero width as success. This is the regression we +// care about for `glow x.md | less`: the pager's pipe must not be +// mistaken for a terminal. +func TestTerminalWidthFromFdRejectsPipe(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe: %v", err) + } + t.Cleanup(func() { + _ = r.Close() + _ = w.Close() + }) + + for name, fd := range map[string]uintptr{"read end": r.Fd(), "write end": w.Fd()} { + if gotW, ok := terminalWidthFromFd(fd); ok { + t.Errorf("%s: expected (_, false), got (%d, true)", name, gotW) + } + } +} + +// TestTerminalWidthFromFdRejectsRegularFile guards the other common +// non-tty case: stdout redirected to a file (`glow x.md > out`). +func TestTerminalWidthFromFdRejectsRegularFile(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "glow-width-*") + if err != nil { + t.Fatalf("CreateTemp: %v", err) + } + t.Cleanup(func() { _ = f.Close() }) + + if gotW, ok := terminalWidthFromFd(f.Fd()); ok { + t.Errorf("regular file: expected (_, false), got (%d, true)", gotW) + } +} diff --git a/main.go b/main.go index b31ca15..4f5ab25 100644 --- a/main.go +++ b/main.go @@ -191,12 +191,17 @@ func validateOptions(cmd *cobra.Command) error { style = "notty" } - // Detect terminal width + // Detect terminal width. Prefer /dev/tty so that `glow 120 { @@ -210,6 +215,27 @@ func validateOptions(cmd *cobra.Command) error { return nil } +// terminalWidthFromTTY reads the controlling terminal's width via +// /dev/tty, so pipes and stdin redirection don't fool the autodetector. +func terminalWidthFromTTY() (int, bool) { + tty, err := os.Open("/dev/tty") + if err != nil { + return 0, false + } + defer tty.Close() //nolint:errcheck + return terminalWidthFromFd(tty.Fd()) +} + +// terminalWidthFromFd returns the terminal width for the given fd, or +// (0, false) when fd is not a terminal or the ioctl fails. +func terminalWidthFromFd(fd uintptr) (int, bool) { + w, _, err := term.GetSize(int(fd)) //nolint:gosec + if err != nil || w <= 0 { + return 0, false + } + return w, true +} + func stdinIsPipe() (bool, error) { stat, err := os.Stdin.Stat() if err != nil {