[GH-ISSUE #1080] TUIs are corrupted when any code in the program prints a newline #786

Closed
opened 2026-03-04 01:07:44 +03:00 by kerem · 2 comments
Owner

Originally created by @xxxserxxx on GitHub (Mar 30, 2025).
Original GitHub issue: https://github.com/rivo/tview/issues/1080

This is related to #1064.

tview display can be corrupted by code printing a newline to either os.Stderr or os.Stdout. Ideally, the tview display would not be affected by code printing output. In particular, this happens if any code, in any dependency, prints a newline; this includes CGO linked libraries, as well as indirect dependencies. It may be impossible to prevent a dependency anywhere in the dependency tree from printing output.

As a work-around for when a dependency is printing to os.Stderr, a program can hijack os.Stderr and os.Stdout and redirect them.

  • The issue happens only when what's printed is a newline (or, possibly, a carriage return -- I haven't tested that)
  • It happens when a newline is printed to either Stdout or Stderr. The sample code demonstrates Stderr.
  • The actual output can not be seen on application exit, but redirecting stderr on the command line can capture the offending message. In my case, the message was written by C code linked in by CGO.

This last point illustrates the importance of handling this in tview.

Paste the code and run it, then scroll the table (list). You'll see the issue. Press 'q' to exit the program. Test the work-around by piping stderr to a file when running, or uncomment the lines 18-20. Note that this work-around is not portable; a portable work-around would need to create a file and redirect Stdin/Stdout to that.

package main

import (
	"os"
	"runtime/debug"

	"fmt"

	"github.com/gdamore/tcell/v2"

	"github.com/rivo/tview"
)

func main() {
	app := tview.NewApplication()
	pages := tview.NewPages()

	// devnull, _ := os.Open("/dev/null")
	// os.Stderr = devnull
        // os.Stdout = devnull

	tv := "github.com/rivo/tview"
	var version string
	bi, ok := debug.ReadBuildInfo()
	if ok {
		for _, d := range bi.Deps {
			if d.Path == tv {
				version = d.Version
			}
		}
	}

	statusLeft := fmt.Sprintf("[::b]%s[::-] v%s", tv, version)
	status := tview.NewTextView().SetText(statusLeft).
		SetTextAlign(tview.AlignLeft).
		SetDynamicColors(true).
		SetScrollable(false)

	infoRight := fmt.Sprintf("[::i]%s[::-]", "rendering demo")
	info := tview.NewTextView().SetText(infoRight).
		SetTextAlign(tview.AlignRight).
		SetDynamicColors(true).
		SetScrollable(false)

	// top bar: status text
	topBarFlex := tview.NewFlex().SetDirection(tview.FlexColumn).
		AddItem(status, 0, 1, false).
		AddItem(info, 30, 0, false)

	box := tview.NewTable().
		SetSelectable(true, false).
		SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorLightGray).Foreground(tcell.ColorBlack))
	box.Box.SetBorder(true).SetTitle(" Cowboy ")
	t := tvc{contents: []string{
		"And as for names, my horse is Dan.",
		"I'm Buster.",
		"Buster Scruggs.",
	}}
	box.SetContent(t)
	box.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		fmt.Fprintf(os.Stderr, "Hey\n")
		return event
	})
	root := tview.NewFlex().SetDirection(tview.FlexColumn).AddItem(box, 0, 1, true)

	pages.AddPage("box", root, true, true)

	rootFlex := tview.NewFlex().
		SetDirection(tview.FlexRow).
		AddItem(topBarFlex, 1, 0, false).
		AddItem(pages, 0, 1, true)

	rootFlex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		if event.Rune() == 'q' {
			app.Stop()
			os.Exit(0)
		}
		return event
	})

	app.SetRoot(rootFlex, true).
		SetFocus(rootFlex).
		EnableMouse(true)

	if err := app.Run(); err != nil {
		panic(err)
	}
}

type tvc struct {
	tview.TableContentReadOnly
	contents []string
}

func (t tvc) GetCell(r, c int) *tview.TableCell {
	return &tview.TableCell{
		Text: t.contents[r],
	}
}
func (t tvc) GetRowCount() int {
	return len(t.contents)
}
func (t tvc) GetColumnCount() int {
	return 1
}
Originally created by @xxxserxxx on GitHub (Mar 30, 2025). Original GitHub issue: https://github.com/rivo/tview/issues/1080 This is related to #1064. tview display can be corrupted by code printing a newline to either `os.Stderr` or `os.Stdout`. Ideally, the tview display would not be affected by code printing output. In particular, this happens if **any** code, in any dependency, prints a newline; this includes CGO linked libraries, as well as indirect dependencies. It may be impossible to prevent a dependency anywhere in the dependency tree from printing output. As a work-around for when a dependency is printing to `os.Stderr`, a program can hijack `os.Stderr` and `os.Stdout` and redirect them. - The issue happens only when what's printed is a newline (or, possibly, a carriage return -- I haven't tested that) - It happens when a newline is printed to **either** Stdout or Stderr. The sample code demonstrates Stderr. - The actual output can not be seen on application exit, but redirecting stderr on the command line can capture the offending message. In my case, the message was written by C code linked in by CGO. This last point illustrates the importance of handling this in tview. Paste the code and run it, then scroll the table (list). You'll see the issue. Press 'q' to exit the program. Test the work-around by piping stderr to a file when running, or uncomment the lines 18-20. Note that this work-around is not portable; a portable work-around would need to create a file and redirect Stdin/Stdout to that. ```go package main import ( "os" "runtime/debug" "fmt" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func main() { app := tview.NewApplication() pages := tview.NewPages() // devnull, _ := os.Open("/dev/null") // os.Stderr = devnull // os.Stdout = devnull tv := "github.com/rivo/tview" var version string bi, ok := debug.ReadBuildInfo() if ok { for _, d := range bi.Deps { if d.Path == tv { version = d.Version } } } statusLeft := fmt.Sprintf("[::b]%s[::-] v%s", tv, version) status := tview.NewTextView().SetText(statusLeft). SetTextAlign(tview.AlignLeft). SetDynamicColors(true). SetScrollable(false) infoRight := fmt.Sprintf("[::i]%s[::-]", "rendering demo") info := tview.NewTextView().SetText(infoRight). SetTextAlign(tview.AlignRight). SetDynamicColors(true). SetScrollable(false) // top bar: status text topBarFlex := tview.NewFlex().SetDirection(tview.FlexColumn). AddItem(status, 0, 1, false). AddItem(info, 30, 0, false) box := tview.NewTable(). SetSelectable(true, false). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorLightGray).Foreground(tcell.ColorBlack)) box.Box.SetBorder(true).SetTitle(" Cowboy ") t := tvc{contents: []string{ "And as for names, my horse is Dan.", "I'm Buster.", "Buster Scruggs.", }} box.SetContent(t) box.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { fmt.Fprintf(os.Stderr, "Hey\n") return event }) root := tview.NewFlex().SetDirection(tview.FlexColumn).AddItem(box, 0, 1, true) pages.AddPage("box", root, true, true) rootFlex := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(topBarFlex, 1, 0, false). AddItem(pages, 0, 1, true) rootFlex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Rune() == 'q' { app.Stop() os.Exit(0) } return event }) app.SetRoot(rootFlex, true). SetFocus(rootFlex). EnableMouse(true) if err := app.Run(); err != nil { panic(err) } } type tvc struct { tview.TableContentReadOnly contents []string } func (t tvc) GetCell(r, c int) *tview.TableCell { return &tview.TableCell{ Text: t.contents[r], } } func (t tvc) GetRowCount() int { return len(t.contents) } func (t tvc) GetColumnCount() int { return 1 } ```
kerem closed this issue 2026-03-04 01:07:44 +03:00
Author
Owner

@xxxserxxx commented on GitHub (Mar 30, 2025):

The work-around of redirecting stdin/stdout within the code apparently does not work when the message is generated in CGO-linked libraries; I assume this is because libraries are initialized and grab the file handles before main() is called. I also tried redirecting the output in an init() function; it also does not work. Piping the output on the command line does work, but this makes the solution something the every user must do.

<!-- gh-comment-id:2764657371 --> @xxxserxxx commented on GitHub (Mar 30, 2025): The work-around of redirecting stdin/stdout within the code apparently does not work when the message is generated in CGO-linked libraries; I assume this is because libraries are initialized and grab the file handles before `main()` is called. I also tried redirecting the output in an `init()` function; it also does not work. Piping the output on the command line _does_ work, but this makes the solution something the every user must do.
Author
Owner

@xxxserxxx commented on GitHub (Apr 9, 2025):

This has been resolved; my test application no longer reproduces the issue.

<!-- gh-comment-id:2789450585 --> @xxxserxxx commented on GitHub (Apr 9, 2025): This has been resolved; my test application no longer reproduces the issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#786
No description provided.