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..8a119442aa 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -33,6 +33,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.ttyOutput = f p.previousOutputState, err = term.GetState(f.Fd()) @@ -45,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