charmbracelet / charmbracelet/bubbletea

v2: Issue rendering padding/whitespace over ssh connection

Open
#1,577 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
44.9k
Forks
1.3k
PR merge metrics
No merged PRs in 30d

Description

### Describe the bug
When I connect over ssh to a docker container (I'm using [DevPod](https://devpod.sh/) which connects over `ssh` when you run `devpod-cli ssh`) , and run my bubbletea (v2) program, padding/whitespace seems to not be rendered properly sometimes.

It _looks_ like it's related to redrawing logic, as the initial state of the view looks correct.
The very same program works fine outside of the SSH connection.

The exact same program using tea/list/lipgloss v1 works fine.

### Setup
- OS: Ubuntu
- Shell: bash inside docker container, zsh outside.
- Terminal Emulator: ghostty
- Terminal Multiplexer: none

### To Reproduce
Make a simple program with some padding styles, and I see weird rendering behaviour.

### Source Code
Here's a simple list inside of a padded window to really show the issue:

source code and go mod
Source code that is running in the video:

```go
package main

import (
"fmt"
"os"

"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)

type model struct {
width int
list list.Model
}

type item string

func (i item) FilterValue() string {
return string(i)
}

func (i item) Title() string {
return string(i)
}
func (i item) Description() string { return "" }

func initialModel() model {
items := []list.Item{
item("Item 1"),
item("Item 2"),
item("Item 3"),
item("Item 4"),
item("Item 5"),
item("Item 6"),
item("Item 7"),
item("Item 8"),
item("Item 9"),
item("Item 10"),
}
return model{list: list.New(items, list.NewDefaultDelegate(), 20, 20)}
}

func (m model) Init() tea.Cmd {
return nil
}

const minWidth = 40
const maxWidth = 240

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s := newStyles()
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = min(msg.Width, maxWidth) - s.base.GetHorizontalFrameSize()
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Interrupt
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}

func (m model) View() tea.View {
s := newStyles()
windowWidth := max(m.width, minWidth)
body := m.list.View()
window := s.window.Width(windowWidth).Render(body)

return tea.NewView(s.base.Render(window))
}

func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

type styles struct {
base lipgloss.Style
window lipgloss.Style
}

func newStyles() *styles {
highlightColor := lipgloss.Color("#874BFD")

s := new(styles)
s.base = lipgloss.NewStyle().
Padding(1, 4, 0, 1)
s.window = lipgloss.NewStyle().
BorderForeground(highlightColor).
Padding(2, 2).
Align(lipgloss.Left).
Border(lipgloss.NormalBorder())
return s
}
```

and the go.mod file:

```go.mod
module testui

go 1.25.3

require (
charm.land/bubbles/v2 v2.0.0-rc.1
charm.land/bubbletea/v2 v2.0.0-rc.2
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 // indirect
github.com/charmbracelet/x/ansi v0.11.1 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.5.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)
```

### Expected behavior
I'd expect the rendering to behave the same over SSH as it does locally.

### Screenshots
Here's a video of the same program (copied the binary into the container) running in a terminal on the left over SSH and on the right locally in my terminal.

https://github.com/user-attachments/assets/c395e8b5-cd33-4160-8f82-ae9134e23cb8

### Additional context
#### Alt mode
It's less noticeable in alt mode, but still there. Can record a video if wanted.

#### Checking the raw output of my strings
I added debug prints to a file to check what bubble tea was outputting, and the strings that `tea.View` is sent look fine, so that's not the issue as far as I can tell.

#### 2026 mode
I also tried to mess with the 2026 mode settings, as I noticed that it's supposed to be excluded if run in SSH, https://github.com/charmbracelet/bubbletea/blob/v2-exp/tea.go#L948, and this environment variable wasn't set as far as I could tell.
I manually set `SSH_TTY` and added a switch case for the `ModeReportMsg` message and saw that it was no longer sent when I had set `SSH_TTY="true"`. So I don't think that's the issue, but I could be wrong.

#### Reducing FPS
I also thought that perhaps the higher latency of SSH (although it's locally connecting to a Docker container, so probably not noticable) might mess with the optimized rendering, so I tried setting a really low FPS with `WithFPS`. 1 - 5 FPS made no difference.

Contributor guide

Open the contributing guide

Research direction

Reproduce the padded Bubble Tea v2 list from the issue over SSH and locally, then compare the redraw behavior and raw output. Start with tea.go around the mentioned 2026-mode check at line 948 and trace the rendering path; done means padding and whitespace render consistently over SSH and locally without breaking the supplied example.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.