From d55cf56d98ed4522b83228ac709463699c74a8ae Mon Sep 17 00:00:00 2001 From: robinsamuel Date: Thu, 4 Dec 2025 23:59:51 +0100 Subject: [PATCH 1/2] Add WithoutOutput option for input-only mode Add new WithoutOutput() option that disables visual output while preserving raw mode for proper keyboard input handling. This allows arrow keys and other special keys to work without requiring Enter, while standard output functions like println() and logging libraries work correctly. This solves a common issue where users want Bubbletea's input handling but need to use standard output for logging or debugging without the renderer interfering with cursor positioning. Changes: - Add WithoutOutput() option in options.go - Add disableOutput flag to Program struct - Use term.MakeRawOutput() when disableOutput is enabled - Add counter example demonstrating the usage - Update go.mod with local x/term replace directive for development The implementation uses the new term.MakeRawOutput() function which sets raw mode for input while preserving output processing flags (OPOST/ONLCR), ensuring that newlines are automatically converted to CR+LF as expected by standard output functions. --- examples/counter/main.go | 48 ++++++++++++++++++++++++++++++++++++++++ go.mod | 2 ++ options.go | 17 ++++++++++++++ tea.go | 7 +++++- tty.go | 2 +- tty_unix.go | 14 ++++++++++-- tty_windows.go | 3 ++- 7 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 examples/counter/main.go diff --git a/examples/counter/main.go b/examples/counter/main.go new file mode 100644 index 0000000000..f06d247588 --- /dev/null +++ b/examples/counter/main.go @@ -0,0 +1,48 @@ +package main + +import ( + tea "charm.land/bubbletea/v2" +) + +// This example demonstrates using Bubbletea without visual output. +// By using WithoutOutput(), the renderer is disabled but raw mode is still +// enabled for proper keyboard input handling (arrow keys work without Enter). +// Output processing is preserved so that println and logging libraries work +// correctly without needing manual "\r\n" handling. +// +// This is useful for non-TUI applications that still need Bubbletea's +// event handling and state management but want to use standard output +// functions like println or logging libraries. +func main() { + p := tea.NewProgram(model(5), tea.WithoutOutput()) + if _, err := p.Run(); err != nil { + panic(err) + } +} + +type model int + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "up": + m++ + println(m) + case "down": + m-- + println(m) + } + } + return m, nil +} + +func (m model) View() tea.View { + return tea.NewView("") +} diff --git a/go.mod b/go.mod index 0d5cf293aa..6fdd66d512 100644 --- a/go.mod +++ b/go.mod @@ -29,3 +29,5 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.18.0 // indirect ) + +replace github.com/charmbracelet/x/term => ../x/term diff --git a/options.go b/options.go index dfd8cda4f8..b6e4d0543e 100644 --- a/options.go +++ b/options.go @@ -101,6 +101,23 @@ func WithoutRenderer() ProgramOption { } } +// WithoutOutput disables visual output from the renderer while still enabling +// raw mode for proper keyboard input handling. This is useful when you want +// keyboard events (like arrow keys) to work properly, but you want to use +// standard output (println, log libraries, etc.) without interference. +// +// Unlike WithoutRenderer, this option preserves output processing (ONLCR flag) +// so that newlines are automatically converted to carriage return + newline, +// making standard output functions work correctly. +// +// This is ideal for applications that need Bubble Tea's input handling but +// want to output using standard logging libraries or print statements. +func WithoutOutput() ProgramOption { + return func(p *Program) { + p.disableOutput = true + } +} + // WithFilter supplies an event filter that will be invoked before Bubble Tea // processes a tea.Msg. The event filter can return any tea.Msg which will then // get handled by Bubble Tea instead of the original event. If the event filter diff --git a/tea.go b/tea.go index 4c6102f858..27211beb50 100644 --- a/tea.go +++ b/tea.go @@ -465,6 +465,11 @@ type Program struct { // UI but still want to take advantage of Bubble Tea's architecture. disableRenderer bool + // disableOutput enables raw mode for input handling but preserves output + // processing (ONLCR) so that standard output functions like println work + // correctly. The renderer is disabled in this mode. + disableOutput bool + // handlers is a list of channels that need to be waited on before the // program can exit. handlers channelHandlers @@ -1020,7 +1025,7 @@ func (p *Program) Run() (returnModel Model, returnErr error) { resizeMsg := WindowSizeMsg{Width: p.width, Height: p.height} if p.renderer == nil { - if p.disableRenderer { + if p.disableRenderer || p.disableOutput { p.renderer = &nilRenderer{} } else { // If no renderer is set use the cursed one. diff --git a/tty.go b/tty.go index 12b86493b3..8946432807 100644 --- a/tty.go +++ b/tty.go @@ -22,7 +22,7 @@ func (p *Program) suspend() { } func (p *Program) initTerminal() error { - if p.disableRenderer { + if p.disableRenderer && !p.disableOutput { return nil } return p.initInput() diff --git a/tty_unix.go b/tty_unix.go index bf1b49a185..d7cafb309f 100644 --- a/tty_unix.go +++ b/tty_unix.go @@ -16,7 +16,15 @@ func (p *Program) initInput() (err error) { // Check if input is a terminal if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) { p.ttyInput = f - p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd()) + + if p.disableOutput { + // Use raw mode that preserves output processing + p.previousTtyInputState, err = term.MakeRawOutput(p.ttyInput.Fd()) + } else { + // Use standard raw mode + p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd()) + } + if err != nil { return fmt.Errorf("error entering raw mode: %w", err) } @@ -24,7 +32,9 @@ func (p *Program) initInput() (err error) { // OPTIM: We can use hard tabs and backspaces to optimize cursor // movements. This is based on termios settings support and whether // they exist and enabled. - p.checkOptimizedMovements(p.previousTtyInputState) + if !p.disableOutput { + p.checkOptimizedMovements(p.previousTtyInputState) + } } if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { diff --git a/tty_windows.go b/tty_windows.go index 27b31182a6..69649e8424 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -33,7 +33,8 @@ func (p *Program) initInput() (err error) { } // Save output screen buffer state and enable VT processing. - if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { + // Skip output modifications in disableOutput mode to preserve normal output behavior. + if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) && !p.disableOutput { p.ttyOutput = f p.previousOutputState, err = term.GetState(f.Fd()) if err != nil { From 2e2b5184794d22decd52cc7fba6137f8cebdfe45 Mon Sep 17 00:00:00 2001 From: Robin Jung Date: Tue, 23 Dec 2025 15:04:14 +0100 Subject: [PATCH 2/2] fix: windows tty with disable output --- tty_windows.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tty_windows.go b/tty_windows.go index 69649e8424..8a119442aa 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -34,7 +34,7 @@ func (p *Program) initInput() (err error) { // Save output screen buffer state and enable VT processing. // Skip output modifications in disableOutput mode to preserve normal output behavior. - if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) && !p.disableOutput { + if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { p.ttyOutput = f p.previousOutputState, err = term.GetState(f.Fd()) if err != nil { @@ -46,10 +46,17 @@ func (p *Program) initInput() (err error) { return fmt.Errorf("error getting console mode: %w", err) } - if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), - mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING| - windows.DISABLE_NEWLINE_AUTO_RETURN); err != nil { - return fmt.Errorf("error setting console mode: %w", err) + if p.disableOutput { + if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), + mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { + return fmt.Errorf("error setting console mode: %w", err) + } + } else { + if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), + mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING| + windows.DISABLE_NEWLINE_AUTO_RETURN); err != nil { + return fmt.Errorf("error setting console mode: %w", err) + } } //nolint:godox