charmbracelet/bubbletea · error

error getting terminal state: %w

Error message

error getting terminal state: %w

What it means

On Windows, after input setup, initInput() saves the output screen buffer state with term.GetState(p.ttyOutput.Fd()) so it can be restored when the program exits. This error means that snapshot failed — the output handle is not a readable console screen buffer at that moment. Program.Run() aborts because Bubble Tea cannot guarantee it can restore the user's terminal afterwards.

Source

Thrown at tty_windows.go:40

		}

		// Enable VT input
		var mode uint32
		if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.Fd()), &mode); err != nil {
			return fmt.Errorf("error getting console mode: %w", err)
		}

		if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
			return fmt.Errorf("error setting console mode: %w", err)
		}
	}

	// Save output screen buffer state and enable VT processing.
	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 {
			return fmt.Errorf("error getting terminal state: %w", err)
		}

		var mode uint32
		if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil {
			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)
		}

		//nolint:godox
		// TODO: check if we can optimize cursor movements on Windows.
		p.checkOptimizedMovements(p.previousOutputState)
	}

View on GitHub (pinned to 351d2159f8)

Solutions

  1. Ensure the output handle stays a live console screen buffer for the whole run — do not close the ConPTY until Program.Run() returns.
  2. If output is genuinely redirected, pass the actual writer via tea.WithOutput(w) (or nil) instead of letting the program inherit a half-valid handle.
  3. Run from a normal interactive console session to confirm the code path works before blaming configuration.
  4. For headless runs, tea.WithoutRenderer() skips this initialization entirely.

Example fix

// before
f, _ := os.OpenFile("out.log", os.O_WRONLY, 0o644)
p := tea.NewProgram(model{}, tea.WithOutput(f)) // later state save fails on dead handle

// after: keep exactly one stable output target for the program lifetime
p := tea.NewProgram(model{}, tea.WithOutput(os.Stdout))
Defensive patterns

Strategy: validation

Validate before calling

if f, ok := os.Stdout.(term.File); ok && !term.IsTerminal(f.Fd()) {
    // stdout cannot be snapshotted/restored; run headless
    p := tea.NewProgram(model, tea.WithoutRenderer(), tea.WithOutput(nil))
}

Type guard

func isConsoleScreenBuffer(f term.File) bool {
    var mode uint32
    return windows.GetConsoleMode(windows.Handle(f.Fd()), &mode) == nil
}

Try / catch

if _, err := p.Run(); err != nil {
    if strings.Contains(err.Error(), "error getting terminal state") {
        log.Fatal("stdout is not a live console screen buffer; keep the output handle stable or run with tea.WithoutRenderer()")
    }
    log.Fatalf("run: %v", err)
}

Prevention

When it happens

Trigger: Program output implements term.File and passes term.IsTerminal(), but GetState's internal GetConsoleMode on the screen buffer fails: the output handle was closed/replaced between the IsTerminal check and the call, or the handle points to a non-console sink that was misdetected.

Common situations: A harness or parent process swapping/closing the ConPTY output handle during startup, piping stdout while a wrapper pty still makes it look like a terminal, and services started with redirected-but-then-detached output.

Related errors


AI-assisted analysis of charmbracelet/bubbletea@351d2159f8 (2026-08-15). Data as JSON: /api/errors/cc0906c57fc5cb0c. Report an issue: GitHub.