wrapRows mirrors bubbles/textarea.wrap()'s row count so the prompt's auto-grow stays in lock step with what the textarea renders: word-boundary aware with a hard-wrap fallback for over-wide words, plus the trailing cursor-anchor row when content exactly fills the width. Adapted from charmbracelet/bu
(s string, width int)
| 188 | // cursor-anchor row when content exactly fills the width. |
| 189 | // Adapted from charmbracelet/bubbles v0.20 textarea. |
| 190 | // |
| 191 | // Caveat: width is summed per-rune (runewidth) rather than per grapheme cluster |
| 192 | // like bubbles' uniseg, so ASCII and CJK match exactly, but a multi-rune cluster |
| 193 | // (a ZWJ-family emoji, a keycap) can over-count, harmlessly over-growing the |
| 194 | // prompt by a row on emoji-heavy input. Not worth pulling in uniseg for that. |
| 195 | func wrapRows(s string, width int) int { |
| 196 | if width <= 0 { |
| 197 | return 1 |
| 198 | } |
| 199 | row := 0 |
| 200 | var lineW, wordW, spaces, charW int |
| 201 | var hadWord bool |
| 202 | |
| 203 | for _, r := range s { |
| 204 | charW = 0 |
| 205 | if unicode.IsSpace(r) { |
| 206 | spaces++ |
| 207 | } else { |
| 208 | charW = runewidth.RuneWidth(r) |
| 209 | wordW += charW |
| 210 | hadWord = true |
| 211 | } |
| 212 | |
| 213 | switch { |
| 214 | case spaces > 0: |
| 215 | if lineW+wordW+spaces > width { |
| 216 | row++ |
| 217 | lineW = wordW + spaces |
| 218 | } else { |
| 219 | lineW += wordW + spaces |
| 220 | } |
| 221 | wordW, spaces = 0, 0 |
| 222 | hadWord = false |
| 223 | case hadWord && wordW+charW > width: |
| 224 | // Space-less word grew past the width; matches bubbles' |
| 225 | // StringWidth(word)+lastCharLen check. |
| 226 | if lineW > 0 { |
| 227 | row++ |
| 228 | } |
| 229 | lineW = wordW |
| 230 | wordW = 0 |
| 231 | hadWord = false |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | if lineW+wordW+spaces >= width { |
| 236 | row++ |
| 237 | } |
no outgoing calls