charmbracelet / charmbracelet/bubbles
textarea: Bottom-anchored textarea shifts upward after multiline submit
- 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`)
>
- Multiline (`N=3`)
>
**Additional context**
N/A
Contributor guide
Assessment
This issue has not been assessed yet.