charmbracelet / charmbracelet/bubbles

textarea: Bottom-anchored textarea shifts upward after multiline submit

Open
#993 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
8.9k
Forks
457
Avg merge
1d 18h
Merged PRs (30d)
5

Description

# **Description**
When a bottom-aligned dynamic-height `bubbles/v2/textarea` collapses from `N>1` lines to `N=1` after submit/reset, it jumps upward and leaves `N-1` blank lines below it. Single-line submits do not show this bug: the `textarea` stays anchored to the bottom row.

## **Setup**
Please complete the following information along with version numbers, if applicable.
- CachyOS (rolling)
- Bash 5.3.9
- Alacritty 0.17.0
- en_US.UTF-8

### **To Reproduce**

#### Steps

1. Run the repro Go command attached below, e.g., `go run .`.
2. Resize the terminal so it is fairly short, or submit a few entries until the `textarea` is resting on the bottom row.
3. Confirm the non-buggy case: enter a single line (`N=1`) and press Ctrl+s (do **NOT** insert a newline with Enter).
- Observe: The printed entry appears above, and the empty `textarea` remains anchored to the bottom:

```
foo
┃ 1
```

4. To demonstrate the issue, enter `N` lines (`N>1`) using Enter for newlines, then press Ctrl+S.
- Observe the difference: after the multiline submit (`N=3`), the `textarea` resets to one line but moves upward, leaving `N-1=2` blank rows below it instead of staying on the bottom row (if you submit `N=4` lines, there will be `N-1=3` blank lines underneath; or if `N=50`, then `N-1=49` blank lines; etc.):

```
foo
bar
baz
┃ 1

```

##### Controls

- `Enter`: insert newline
- `Ctrl+S`: submit
- `Esc` or `Ctrl+C`: quit

### **Source Code**

- `go.mod`

```gomod
module textarea-bottom-bug

go 1.26.1

require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.6
)
```

- `main.go`

```go
package main

import (
"fmt"
"os"

"charm.land/bubbles/v2/textarea"
tea "charm.land/bubbletea/v2"
)

type model struct {
ta textarea.Model
}

func newModel() model {
ta := textarea.New()
ta.DynamicHeight = true
ta.MinHeight = 1
ta.MaxHeight = 8
ta.Focus()
return model{ta: ta}
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.ta.SetWidth(msg.Width)
return m, nil

case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "esc":
return m, tea.Quit

case "ctrl+s":
entry := m.ta.Value()
m.ta.Reset()
return m, tea.Batch(
tea.Printf("%s", entry),
m.ta.Focus(),
)
}
}

var cmd tea.Cmd
m.ta, cmd = m.ta.Update(msg)
return m, cmd
}

func (m model) View() tea.View {
v := tea.NewView(m.ta.View())
if c := m.ta.Cursor(); c != nil {
v.Cursor = c
}
return v
}

func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```

**Expected behavior**
I expected the `textarea` to remain anchored to the bottom of the screen in both cases: `N=1` and `N>1`.

**Screenshots**

- Single line (`N=1`)

> Image

- Multiline (`N=3`)

> Image

**Additional context**
N/A

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.