textarea.go 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019
  1. package textarea
  2. import (
  3. "crypto/sha256"
  4. "fmt"
  5. "image/color"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "unicode"
  10. "slices"
  11. "github.com/charmbracelet/bubbles/v2/cursor"
  12. "github.com/charmbracelet/bubbles/v2/key"
  13. tea "github.com/charmbracelet/bubbletea/v2"
  14. "github.com/charmbracelet/lipgloss/v2"
  15. "github.com/charmbracelet/x/ansi"
  16. rw "github.com/mattn/go-runewidth"
  17. "github.com/rivo/uniseg"
  18. )
  19. const (
  20. minHeight = 1
  21. defaultHeight = 1
  22. defaultWidth = 40
  23. defaultCharLimit = 0 // no limit
  24. defaultMaxHeight = 99
  25. defaultMaxWidth = 500
  26. // XXX: in v2, make max lines dynamic and default max lines configurable.
  27. maxLines = 10000
  28. )
  29. // Attachment represents a special object within the text, distinct from regular characters.
  30. type Attachment struct {
  31. ID string // A unique identifier for this attachment instance
  32. Display string // e.g., "@filename.txt"
  33. URL string
  34. Filename string
  35. MediaType string
  36. }
  37. // Helper functions for converting between runes and any slices
  38. // runesToInterfaces converts a slice of runes to a slice of interfaces
  39. func runesToInterfaces(runes []rune) []any {
  40. result := make([]any, len(runes))
  41. for i, r := range runes {
  42. result[i] = r
  43. }
  44. return result
  45. }
  46. // interfacesToRunes converts a slice of interfaces to a slice of runes (for display purposes)
  47. func interfacesToRunes(items []any) []rune {
  48. var result []rune
  49. for _, item := range items {
  50. switch val := item.(type) {
  51. case rune:
  52. result = append(result, val)
  53. case *Attachment:
  54. result = append(result, []rune(val.Display)...)
  55. }
  56. }
  57. return result
  58. }
  59. // copyInterfaceSlice creates a copy of an any slice
  60. func copyInterfaceSlice(src []any) []any {
  61. dst := make([]any, len(src))
  62. copy(dst, src)
  63. return dst
  64. }
  65. // interfacesToString converts a slice of interfaces to a string for display
  66. func interfacesToString(items []any) string {
  67. var s strings.Builder
  68. for _, item := range items {
  69. switch val := item.(type) {
  70. case rune:
  71. s.WriteRune(val)
  72. case *Attachment:
  73. s.WriteString(val.Display)
  74. }
  75. }
  76. return s.String()
  77. }
  78. // isAttachmentAtCursor checks if the cursor is positioned on or immediately after an attachment.
  79. // This allows for proper highlighting even when the cursor is technically at the position
  80. // after the attachment object in the underlying slice.
  81. func (m Model) isAttachmentAtCursor() (*Attachment, int, int) {
  82. if m.row >= len(m.value) {
  83. return nil, -1, -1
  84. }
  85. row := m.value[m.row]
  86. col := m.col
  87. if col < 0 || col > len(row) {
  88. return nil, -1, -1
  89. }
  90. // Check if the cursor is at the same index as an attachment.
  91. if col < len(row) {
  92. if att, ok := row[col].(*Attachment); ok {
  93. return att, col, col
  94. }
  95. }
  96. // Check if the cursor is immediately after an attachment. This is a common
  97. // state, for example, after just inserting one.
  98. if col > 0 && col <= len(row) {
  99. if att, ok := row[col-1].(*Attachment); ok {
  100. return att, col - 1, col - 1
  101. }
  102. }
  103. return nil, -1, -1
  104. }
  105. // renderLineWithAttachments renders a line with proper attachment highlighting
  106. func (m Model) renderLineWithAttachments(
  107. items []any,
  108. style lipgloss.Style,
  109. ) string {
  110. var s strings.Builder
  111. currentAttachment, _, _ := m.isAttachmentAtCursor()
  112. for _, item := range items {
  113. switch val := item.(type) {
  114. case rune:
  115. s.WriteString(style.Render(string(val)))
  116. case *Attachment:
  117. // Check if this is the attachment the cursor is currently on
  118. if currentAttachment != nil && currentAttachment.ID == val.ID {
  119. // Cursor is on this attachment, highlight it
  120. s.WriteString(m.Styles.SelectedAttachment.Render(val.Display))
  121. } else {
  122. s.WriteString(m.Styles.Attachment.Render(val.Display))
  123. }
  124. }
  125. }
  126. return s.String()
  127. }
  128. // getRuneAt safely gets a rune at a specific position, returns 0 if not a rune
  129. func getRuneAt(items []any, index int) rune {
  130. if index < 0 || index >= len(items) {
  131. return 0
  132. }
  133. if r, ok := items[index].(rune); ok {
  134. return r
  135. }
  136. return 0
  137. }
  138. // isSpaceAt checks if the item at index is a space rune
  139. func isSpaceAt(items []any, index int) bool {
  140. r := getRuneAt(items, index)
  141. return r != 0 && unicode.IsSpace(r)
  142. }
  143. // setRuneAt safely sets a rune at a specific position if it's a rune
  144. func setRuneAt(items []any, index int, r rune) {
  145. if index >= 0 && index < len(items) {
  146. if _, ok := items[index].(rune); ok {
  147. items[index] = r
  148. }
  149. }
  150. }
  151. // Internal messages for clipboard operations.
  152. type (
  153. pasteMsg string
  154. pasteErrMsg struct{ error }
  155. )
  156. // KeyMap is the key bindings for different actions within the textarea.
  157. type KeyMap struct {
  158. CharacterBackward key.Binding
  159. CharacterForward key.Binding
  160. DeleteAfterCursor key.Binding
  161. DeleteBeforeCursor key.Binding
  162. DeleteCharacterBackward key.Binding
  163. DeleteCharacterForward key.Binding
  164. DeleteWordBackward key.Binding
  165. DeleteWordForward key.Binding
  166. InsertNewline key.Binding
  167. LineEnd key.Binding
  168. LineNext key.Binding
  169. LinePrevious key.Binding
  170. LineStart key.Binding
  171. Paste key.Binding
  172. WordBackward key.Binding
  173. WordForward key.Binding
  174. InputBegin key.Binding
  175. InputEnd key.Binding
  176. UppercaseWordForward key.Binding
  177. LowercaseWordForward key.Binding
  178. CapitalizeWordForward key.Binding
  179. TransposeCharacterBackward key.Binding
  180. }
  181. // DefaultKeyMap returns the default set of key bindings for navigating and acting
  182. // upon the textarea.
  183. func DefaultKeyMap() KeyMap {
  184. return KeyMap{
  185. CharacterForward: key.NewBinding(
  186. key.WithKeys("right", "ctrl+f"),
  187. key.WithHelp("right", "character forward"),
  188. ),
  189. CharacterBackward: key.NewBinding(
  190. key.WithKeys("left", "ctrl+b"),
  191. key.WithHelp("left", "character backward"),
  192. ),
  193. WordForward: key.NewBinding(
  194. key.WithKeys("alt+right", "alt+f"),
  195. key.WithHelp("alt+right", "word forward"),
  196. ),
  197. WordBackward: key.NewBinding(
  198. key.WithKeys("alt+left", "alt+b"),
  199. key.WithHelp("alt+left", "word backward"),
  200. ),
  201. LineNext: key.NewBinding(
  202. key.WithKeys("down", "ctrl+n"),
  203. key.WithHelp("down", "next line"),
  204. ),
  205. LinePrevious: key.NewBinding(
  206. key.WithKeys("up", "ctrl+p"),
  207. key.WithHelp("up", "previous line"),
  208. ),
  209. DeleteWordBackward: key.NewBinding(
  210. key.WithKeys("alt+backspace", "ctrl+w"),
  211. key.WithHelp("alt+backspace", "delete word backward"),
  212. ),
  213. DeleteWordForward: key.NewBinding(
  214. key.WithKeys("alt+delete", "alt+d"),
  215. key.WithHelp("alt+delete", "delete word forward"),
  216. ),
  217. DeleteAfterCursor: key.NewBinding(
  218. key.WithKeys("ctrl+k"),
  219. key.WithHelp("ctrl+k", "delete after cursor"),
  220. ),
  221. DeleteBeforeCursor: key.NewBinding(
  222. key.WithKeys("ctrl+u"),
  223. key.WithHelp("ctrl+u", "delete before cursor"),
  224. ),
  225. InsertNewline: key.NewBinding(
  226. key.WithKeys("enter", "ctrl+m"),
  227. key.WithHelp("enter", "insert newline"),
  228. ),
  229. DeleteCharacterBackward: key.NewBinding(
  230. key.WithKeys("backspace", "ctrl+h"),
  231. key.WithHelp("backspace", "delete character backward"),
  232. ),
  233. DeleteCharacterForward: key.NewBinding(
  234. key.WithKeys("delete", "ctrl+d"),
  235. key.WithHelp("delete", "delete character forward"),
  236. ),
  237. LineStart: key.NewBinding(
  238. key.WithKeys("home", "ctrl+a"),
  239. key.WithHelp("home", "line start"),
  240. ),
  241. LineEnd: key.NewBinding(
  242. key.WithKeys("end", "ctrl+e"),
  243. key.WithHelp("end", "line end"),
  244. ),
  245. Paste: key.NewBinding(
  246. key.WithKeys("ctrl+v"),
  247. key.WithHelp("ctrl+v", "paste"),
  248. ),
  249. InputBegin: key.NewBinding(
  250. key.WithKeys("alt+<", "ctrl+home"),
  251. key.WithHelp("alt+<", "input begin"),
  252. ),
  253. InputEnd: key.NewBinding(
  254. key.WithKeys("alt+>", "ctrl+end"),
  255. key.WithHelp("alt+>", "input end"),
  256. ),
  257. CapitalizeWordForward: key.NewBinding(
  258. key.WithKeys("alt+c"),
  259. key.WithHelp("alt+c", "capitalize word forward"),
  260. ),
  261. LowercaseWordForward: key.NewBinding(
  262. key.WithKeys("alt+l"),
  263. key.WithHelp("alt+l", "lowercase word forward"),
  264. ),
  265. UppercaseWordForward: key.NewBinding(
  266. key.WithKeys("alt+u"),
  267. key.WithHelp("alt+u", "uppercase word forward"),
  268. ),
  269. TransposeCharacterBackward: key.NewBinding(
  270. key.WithKeys("ctrl+t"),
  271. key.WithHelp("ctrl+t", "transpose character backward"),
  272. ),
  273. }
  274. }
  275. // LineInfo is a helper for keeping track of line information regarding
  276. // soft-wrapped lines.
  277. type LineInfo struct {
  278. // Width is the number of columns in the line.
  279. Width int
  280. // CharWidth is the number of characters in the line to account for
  281. // double-width runes.
  282. CharWidth int
  283. // Height is the number of rows in the line.
  284. Height int
  285. // StartColumn is the index of the first column of the line.
  286. StartColumn int
  287. // ColumnOffset is the number of columns that the cursor is offset from the
  288. // start of the line.
  289. ColumnOffset int
  290. // RowOffset is the number of rows that the cursor is offset from the start
  291. // of the line.
  292. RowOffset int
  293. // CharOffset is the number of characters that the cursor is offset
  294. // from the start of the line. This will generally be equivalent to
  295. // ColumnOffset, but will be different there are double-width runes before
  296. // the cursor.
  297. CharOffset int
  298. }
  299. // CursorStyle is the style for real and virtual cursors.
  300. type CursorStyle struct {
  301. // Style styles the cursor block.
  302. //
  303. // For real cursors, the foreground color set here will be used as the
  304. // cursor color.
  305. Color color.Color
  306. // Shape is the cursor shape. The following shapes are available:
  307. //
  308. // - tea.CursorBlock
  309. // - tea.CursorUnderline
  310. // - tea.CursorBar
  311. //
  312. // This is only used for real cursors.
  313. Shape tea.CursorShape
  314. // CursorBlink determines whether or not the cursor should blink.
  315. Blink bool
  316. // BlinkSpeed is the speed at which the virtual cursor blinks. This has no
  317. // effect on real cursors as well as no effect if the cursor is set not to
  318. // [CursorBlink].
  319. //
  320. // By default, the blink speed is set to about 500ms.
  321. BlinkSpeed time.Duration
  322. }
  323. // Styles are the styles for the textarea, separated into focused and blurred
  324. // states. The appropriate styles will be chosen based on the focus state of
  325. // the textarea.
  326. type Styles struct {
  327. Focused StyleState
  328. Blurred StyleState
  329. Cursor CursorStyle
  330. Attachment lipgloss.Style
  331. SelectedAttachment lipgloss.Style
  332. }
  333. // StyleState that will be applied to the text area.
  334. //
  335. // StyleState can be applied to focused and unfocused states to change the styles
  336. // depending on the focus state.
  337. //
  338. // For an introduction to styling with Lip Gloss see:
  339. // https://github.com/charmbracelet/lipgloss
  340. type StyleState struct {
  341. Base lipgloss.Style
  342. Text lipgloss.Style
  343. LineNumber lipgloss.Style
  344. CursorLineNumber lipgloss.Style
  345. CursorLine lipgloss.Style
  346. EndOfBuffer lipgloss.Style
  347. Placeholder lipgloss.Style
  348. Prompt lipgloss.Style
  349. }
  350. func (s StyleState) computedCursorLine() lipgloss.Style {
  351. return s.CursorLine.Inherit(s.Base).Inline(true)
  352. }
  353. func (s StyleState) computedCursorLineNumber() lipgloss.Style {
  354. return s.CursorLineNumber.
  355. Inherit(s.CursorLine).
  356. Inherit(s.Base).
  357. Inline(true)
  358. }
  359. func (s StyleState) computedEndOfBuffer() lipgloss.Style {
  360. return s.EndOfBuffer.Inherit(s.Base).Inline(true)
  361. }
  362. func (s StyleState) computedLineNumber() lipgloss.Style {
  363. return s.LineNumber.Inherit(s.Base).Inline(true)
  364. }
  365. func (s StyleState) computedPlaceholder() lipgloss.Style {
  366. return s.Placeholder.Inherit(s.Base).Inline(true)
  367. }
  368. func (s StyleState) computedPrompt() lipgloss.Style {
  369. return s.Prompt.Inherit(s.Base).Inline(true)
  370. }
  371. func (s StyleState) computedText() lipgloss.Style {
  372. return s.Text.Inherit(s.Base).Inline(true)
  373. }
  374. // line is the input to the text wrapping function. This is stored in a struct
  375. // so that it can be hashed and memoized.
  376. type line struct {
  377. content []any // Contains runes and *Attachment
  378. width int
  379. }
  380. // Hash returns a hash of the line.
  381. func (w line) Hash() string {
  382. var s strings.Builder
  383. for _, item := range w.content {
  384. switch v := item.(type) {
  385. case rune:
  386. s.WriteRune(v)
  387. case *Attachment:
  388. s.WriteString(v.ID)
  389. }
  390. }
  391. v := fmt.Sprintf("%s:%d", s.String(), w.width)
  392. return fmt.Sprintf("%x", sha256.Sum256([]byte(v)))
  393. }
  394. // Model is the Bubble Tea model for this text area element.
  395. type Model struct {
  396. Err error
  397. // General settings.
  398. cache *MemoCache[line, [][]any]
  399. // Prompt is printed at the beginning of each line.
  400. //
  401. // When changing the value of Prompt after the model has been
  402. // initialized, ensure that SetWidth() gets called afterwards.
  403. //
  404. // See also [SetPromptFunc] for a dynamic prompt.
  405. Prompt string
  406. // Placeholder is the text displayed when the user
  407. // hasn't entered anything yet.
  408. Placeholder string
  409. // ShowLineNumbers, if enabled, causes line numbers to be printed
  410. // after the prompt.
  411. ShowLineNumbers bool
  412. // EndOfBufferCharacter is displayed at the end of the input.
  413. EndOfBufferCharacter rune
  414. // KeyMap encodes the keybindings recognized by the widget.
  415. KeyMap KeyMap
  416. // Styling. FocusedStyle and BlurredStyle are used to style the textarea in
  417. // focused and blurred states.
  418. Styles Styles
  419. // virtualCursor manages the virtual cursor.
  420. virtualCursor cursor.Model
  421. // VirtualCursor determines whether or not to use the virtual cursor. If
  422. // set to false, use [Model.Cursor] to return a real cursor for rendering.
  423. VirtualCursor bool
  424. // CharLimit is the maximum number of characters this input element will
  425. // accept. If 0 or less, there's no limit.
  426. CharLimit int
  427. // MaxHeight is the maximum height of the text area in rows. If 0 or less,
  428. // there's no limit.
  429. MaxHeight int
  430. // MaxWidth is the maximum width of the text area in columns. If 0 or less,
  431. // there's no limit.
  432. MaxWidth int
  433. // If promptFunc is set, it replaces Prompt as a generator for
  434. // prompt strings at the beginning of each line.
  435. promptFunc func(line int) string
  436. // promptWidth is the width of the prompt.
  437. promptWidth int
  438. // width is the maximum number of characters that can be displayed at once.
  439. // If 0 or less this setting is ignored.
  440. width int
  441. // height is the maximum number of lines that can be displayed at once. It
  442. // essentially treats the text field like a vertically scrolling viewport
  443. // if there are more lines than the permitted height.
  444. height int
  445. // Underlying text value. Contains either rune or *Attachment types.
  446. value [][]any
  447. // focus indicates whether user input focus should be on this input
  448. // component. When false, ignore keyboard input and hide the cursor.
  449. focus bool
  450. // Cursor column (slice index).
  451. col int
  452. // Cursor row.
  453. row int
  454. // Last character offset, used to maintain state when the cursor is moved
  455. // vertically such that we can maintain the same navigating position.
  456. lastCharOffset int
  457. // rune sanitizer for input.
  458. rsan Sanitizer
  459. }
  460. // New creates a new model with default settings.
  461. func New() Model {
  462. cur := cursor.New()
  463. styles := DefaultDarkStyles()
  464. m := Model{
  465. CharLimit: defaultCharLimit,
  466. MaxHeight: defaultMaxHeight,
  467. MaxWidth: defaultMaxWidth,
  468. Prompt: lipgloss.ThickBorder().Left + " ",
  469. Styles: styles,
  470. cache: NewMemoCache[line, [][]any](maxLines),
  471. EndOfBufferCharacter: ' ',
  472. ShowLineNumbers: true,
  473. VirtualCursor: true,
  474. virtualCursor: cur,
  475. KeyMap: DefaultKeyMap(),
  476. value: make([][]any, minHeight, maxLines),
  477. focus: false,
  478. col: 0,
  479. row: 0,
  480. }
  481. m.SetWidth(defaultWidth)
  482. m.SetHeight(defaultHeight)
  483. return m
  484. }
  485. // DefaultStyles returns the default styles for focused and blurred states for
  486. // the textarea.
  487. func DefaultStyles(isDark bool) Styles {
  488. lightDark := lipgloss.LightDark(isDark)
  489. var s Styles
  490. s.Focused = StyleState{
  491. Base: lipgloss.NewStyle(),
  492. CursorLine: lipgloss.NewStyle().
  493. Background(lightDark(lipgloss.Color("255"), lipgloss.Color("0"))),
  494. CursorLineNumber: lipgloss.NewStyle().
  495. Foreground(lightDark(lipgloss.Color("240"), lipgloss.Color("240"))),
  496. EndOfBuffer: lipgloss.NewStyle().
  497. Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))),
  498. LineNumber: lipgloss.NewStyle().
  499. Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))),
  500. Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
  501. Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
  502. Text: lipgloss.NewStyle(),
  503. }
  504. s.Blurred = StyleState{
  505. Base: lipgloss.NewStyle(),
  506. CursorLine: lipgloss.NewStyle().
  507. Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))),
  508. CursorLineNumber: lipgloss.NewStyle().
  509. Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))),
  510. EndOfBuffer: lipgloss.NewStyle().
  511. Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))),
  512. LineNumber: lipgloss.NewStyle().
  513. Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))),
  514. Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
  515. Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
  516. Text: lipgloss.NewStyle().
  517. Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))),
  518. }
  519. s.Attachment = lipgloss.NewStyle().
  520. Background(lipgloss.Color("11")).
  521. Foreground(lipgloss.Color("0"))
  522. s.SelectedAttachment = lipgloss.NewStyle().
  523. Background(lipgloss.Color("11")).
  524. Foreground(lipgloss.Color("0"))
  525. s.Cursor = CursorStyle{
  526. Color: lipgloss.Color("7"),
  527. Shape: tea.CursorBlock,
  528. Blink: true,
  529. }
  530. return s
  531. }
  532. // DefaultLightStyles returns the default styles for a light background.
  533. func DefaultLightStyles() Styles {
  534. return DefaultStyles(false)
  535. }
  536. // DefaultDarkStyles returns the default styles for a dark background.
  537. func DefaultDarkStyles() Styles {
  538. return DefaultStyles(true)
  539. }
  540. // updateVirtualCursorStyle sets styling on the virtual cursor based on the
  541. // textarea's style settings.
  542. func (m *Model) updateVirtualCursorStyle() {
  543. if !m.VirtualCursor {
  544. m.virtualCursor.SetMode(cursor.CursorHide)
  545. return
  546. }
  547. m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.Styles.Cursor.Color)
  548. // By default, the blink speed of the cursor is set to a default
  549. // internally.
  550. if m.Styles.Cursor.Blink {
  551. if m.Styles.Cursor.BlinkSpeed > 0 {
  552. m.virtualCursor.BlinkSpeed = m.Styles.Cursor.BlinkSpeed
  553. }
  554. m.virtualCursor.SetMode(cursor.CursorBlink)
  555. return
  556. }
  557. m.virtualCursor.SetMode(cursor.CursorStatic)
  558. }
  559. // SetValue sets the value of the text input.
  560. func (m *Model) SetValue(s string) {
  561. m.Reset()
  562. m.InsertString(s)
  563. }
  564. // InsertString inserts a string at the cursor position.
  565. func (m *Model) InsertString(s string) {
  566. m.InsertRunesFromUserInput([]rune(s))
  567. }
  568. // InsertRune inserts a rune at the cursor position.
  569. func (m *Model) InsertRune(r rune) {
  570. m.InsertRunesFromUserInput([]rune{r})
  571. }
  572. // InsertAttachment inserts an attachment at the cursor position.
  573. func (m *Model) InsertAttachment(att *Attachment) {
  574. if m.CharLimit > 0 {
  575. availSpace := m.CharLimit - m.Length()
  576. // If the char limit's been reached, cancel.
  577. if availSpace <= 0 {
  578. return
  579. }
  580. }
  581. // Insert the attachment at the current cursor position
  582. m.value[m.row] = append(
  583. m.value[m.row][:m.col],
  584. append([]any{att}, m.value[m.row][m.col:]...)...)
  585. m.col++
  586. m.SetCursorColumn(m.col)
  587. }
  588. // ReplaceRange replaces text from startCol to endCol on the current row with the given string.
  589. // This preserves attachments outside the replaced range.
  590. func (m *Model) ReplaceRange(startCol, endCol int, replacement string) {
  591. if m.row >= len(m.value) || startCol < 0 || endCol < startCol {
  592. return
  593. }
  594. // Ensure bounds are within the current row
  595. rowLen := len(m.value[m.row])
  596. startCol = max(0, min(startCol, rowLen))
  597. endCol = max(startCol, min(endCol, rowLen))
  598. // Create new row content: before + replacement + after
  599. before := m.value[m.row][:startCol]
  600. after := m.value[m.row][endCol:]
  601. replacementRunes := runesToInterfaces([]rune(replacement))
  602. // Combine the parts
  603. newRow := make([]any, 0, len(before)+len(replacementRunes)+len(after))
  604. newRow = append(newRow, before...)
  605. newRow = append(newRow, replacementRunes...)
  606. newRow = append(newRow, after...)
  607. m.value[m.row] = newRow
  608. // Position cursor at end of replacement
  609. m.col = startCol + len(replacementRunes)
  610. m.SetCursorColumn(m.col)
  611. }
  612. // CurrentRowLength returns the length of the current row.
  613. func (m *Model) CurrentRowLength() int {
  614. if m.row >= len(m.value) {
  615. return 0
  616. }
  617. return len(m.value[m.row])
  618. }
  619. // GetAttachments returns all attachments in the textarea.
  620. func (m Model) GetAttachments() []*Attachment {
  621. var attachments []*Attachment
  622. for _, row := range m.value {
  623. for _, item := range row {
  624. if att, ok := item.(*Attachment); ok {
  625. attachments = append(attachments, att)
  626. }
  627. }
  628. }
  629. return attachments
  630. }
  631. // InsertRunesFromUserInput inserts runes at the current cursor position.
  632. func (m *Model) InsertRunesFromUserInput(runes []rune) {
  633. // Clean up any special characters in the input provided by the
  634. // clipboard. This avoids bugs due to e.g. tab characters and
  635. // whatnot.
  636. runes = m.san().Sanitize(runes)
  637. if m.CharLimit > 0 {
  638. availSpace := m.CharLimit - m.Length()
  639. // If the char limit's been reached, cancel.
  640. if availSpace <= 0 {
  641. return
  642. }
  643. // If there's not enough space to paste the whole thing cut the pasted
  644. // runes down so they'll fit.
  645. if availSpace < len(runes) {
  646. runes = runes[:availSpace]
  647. }
  648. }
  649. // Split the input into lines.
  650. var lines [][]rune
  651. lstart := 0
  652. for i := range runes {
  653. if runes[i] == '\n' {
  654. // Queue a line to become a new row in the text area below.
  655. // Beware to clamp the max capacity of the slice, to ensure no
  656. // data from different rows get overwritten when later edits
  657. // will modify this line.
  658. lines = append(lines, runes[lstart:i:i])
  659. lstart = i + 1
  660. }
  661. }
  662. if lstart <= len(runes) {
  663. // The last line did not end with a newline character.
  664. // Take it now.
  665. lines = append(lines, runes[lstart:])
  666. }
  667. // Obey the maximum line limit.
  668. if maxLines > 0 && len(m.value)+len(lines)-1 > maxLines {
  669. allowedHeight := max(0, maxLines-len(m.value)+1)
  670. lines = lines[:allowedHeight]
  671. }
  672. if len(lines) == 0 {
  673. // Nothing left to insert.
  674. return
  675. }
  676. // Save the remainder of the original line at the current
  677. // cursor position.
  678. tail := copyInterfaceSlice(m.value[m.row][m.col:])
  679. // Paste the first line at the current cursor position.
  680. m.value[m.row] = append(m.value[m.row][:m.col], runesToInterfaces(lines[0])...)
  681. m.col += len(lines[0])
  682. if numExtraLines := len(lines) - 1; numExtraLines > 0 {
  683. // Add the new lines.
  684. // We try to reuse the slice if there's already space.
  685. var newGrid [][]any
  686. if cap(m.value) >= len(m.value)+numExtraLines {
  687. // Can reuse the extra space.
  688. newGrid = m.value[:len(m.value)+numExtraLines]
  689. } else {
  690. // No space left; need a new slice.
  691. newGrid = make([][]any, len(m.value)+numExtraLines)
  692. copy(newGrid, m.value[:m.row+1])
  693. }
  694. // Add all the rows that were after the cursor in the original
  695. // grid at the end of the new grid.
  696. copy(newGrid[m.row+1+numExtraLines:], m.value[m.row+1:])
  697. m.value = newGrid
  698. // Insert all the new lines in the middle.
  699. for _, l := range lines[1:] {
  700. m.row++
  701. m.value[m.row] = runesToInterfaces(l)
  702. m.col = len(l)
  703. }
  704. }
  705. // Finally add the tail at the end of the last line inserted.
  706. m.value[m.row] = append(m.value[m.row], tail...)
  707. m.SetCursorColumn(m.col)
  708. }
  709. // Value returns the value of the text input.
  710. func (m Model) Value() string {
  711. if m.value == nil {
  712. return ""
  713. }
  714. var v strings.Builder
  715. for _, l := range m.value {
  716. for _, item := range l {
  717. switch val := item.(type) {
  718. case rune:
  719. v.WriteRune(val)
  720. case *Attachment:
  721. v.WriteString(val.Display)
  722. }
  723. }
  724. v.WriteByte('\n')
  725. }
  726. return strings.TrimSuffix(v.String(), "\n")
  727. }
  728. // Length returns the number of characters currently in the text input.
  729. func (m *Model) Length() int {
  730. var l int
  731. for _, row := range m.value {
  732. for _, item := range row {
  733. switch val := item.(type) {
  734. case rune:
  735. l += rw.RuneWidth(val)
  736. case *Attachment:
  737. l += uniseg.StringWidth(val.Display)
  738. }
  739. }
  740. }
  741. // We add len(m.value) to include the newline characters.
  742. return l + len(m.value) - 1
  743. }
  744. // LineCount returns the number of lines that are currently in the text input.
  745. func (m *Model) LineCount() int {
  746. return m.ContentHeight()
  747. }
  748. // Line returns the line position.
  749. func (m Model) Line() int {
  750. return m.row
  751. }
  752. // CursorColumn returns the cursor's column position (slice index).
  753. func (m Model) CursorColumn() int {
  754. return m.col
  755. }
  756. // LastRuneIndex returns the index of the last occurrence of a rune on the current line,
  757. // searching backwards from the current cursor position.
  758. // Returns -1 if the rune is not found before the cursor.
  759. func (m Model) LastRuneIndex(r rune) int {
  760. if m.row >= len(m.value) {
  761. return -1
  762. }
  763. // Iterate backwards from just before the cursor position
  764. for i := m.col - 1; i >= 0; i-- {
  765. if i < len(m.value[m.row]) {
  766. if item, ok := m.value[m.row][i].(rune); ok && item == r {
  767. return i
  768. }
  769. }
  770. }
  771. return -1
  772. }
  773. func (m *Model) Newline() {
  774. if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
  775. return
  776. }
  777. m.col = clamp(m.col, 0, len(m.value[m.row]))
  778. m.splitLine(m.row, m.col)
  779. }
  780. // mapVisualOffsetToSliceIndex converts a visual column offset to a slice index.
  781. // This is used to maintain the cursor's horizontal position when moving vertically.
  782. func (m *Model) mapVisualOffsetToSliceIndex(row int, charOffset int) int {
  783. if row < 0 || row >= len(m.value) {
  784. return 0
  785. }
  786. offset := 0
  787. // Find the slice index that corresponds to the visual offset.
  788. for i, item := range m.value[row] {
  789. var itemWidth int
  790. switch v := item.(type) {
  791. case rune:
  792. itemWidth = rw.RuneWidth(v)
  793. case *Attachment:
  794. itemWidth = uniseg.StringWidth(v.Display)
  795. }
  796. // If the target offset falls within the current item, this is our index.
  797. if offset+itemWidth > charOffset {
  798. // Decide whether to stick with the previous index or move to the current
  799. // one based on which is closer to the target offset.
  800. if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
  801. return i + 1
  802. }
  803. return i
  804. }
  805. offset += itemWidth
  806. }
  807. return len(m.value[row])
  808. }
  809. // CursorDown moves the cursor down by one line.
  810. // Returns whether or not the cursor blink should be reset.
  811. func (m *Model) CursorDown() {
  812. li := m.LineInfo()
  813. charOffset := max(m.lastCharOffset, li.CharOffset)
  814. m.lastCharOffset = charOffset
  815. if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 {
  816. // Move to the next model line
  817. m.row++
  818. m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
  819. } else if li.RowOffset+1 < li.Height {
  820. // Move to the next wrapped line within the same model line
  821. startOfNextWrappedLine := li.StartColumn + li.Width
  822. m.col = startOfNextWrappedLine + m.mapVisualOffsetToSliceIndex(m.row, charOffset)
  823. }
  824. m.SetCursorColumn(m.col)
  825. }
  826. // CursorUp moves the cursor up by one line.
  827. func (m *Model) CursorUp() {
  828. li := m.LineInfo()
  829. charOffset := max(m.lastCharOffset, li.CharOffset)
  830. m.lastCharOffset = charOffset
  831. if li.RowOffset <= 0 && m.row > 0 {
  832. // Move to the previous model line
  833. m.row--
  834. m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
  835. } else if li.RowOffset > 0 {
  836. // Move to the previous wrapped line within the same model line
  837. // To do this, we need to find the start of the previous wrapped line.
  838. prevLineInfo := m.LineInfo()
  839. // prevLineStart := 0
  840. if prevLineInfo.RowOffset > 0 {
  841. // This is complex, so we'll approximate by moving to the start of the current wrapped line
  842. // and then letting characterLeft handle it. A more precise calculation would
  843. // require re-wrapping to find the previous line's start.
  844. // For now, a simpler approach:
  845. m.col = li.StartColumn - 1
  846. }
  847. m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
  848. }
  849. m.SetCursorColumn(m.col)
  850. }
  851. // SetCursorColumn moves the cursor to the given position. If the position is
  852. // out of bounds the cursor will be moved to the start or end accordingly.
  853. func (m *Model) SetCursorColumn(col int) {
  854. m.col = clamp(col, 0, len(m.value[m.row]))
  855. // Any time that we move the cursor horizontally we need to reset the last
  856. // offset so that the horizontal position when navigating is adjusted.
  857. m.lastCharOffset = 0
  858. }
  859. // CursorStart moves the cursor to the start of the input field.
  860. func (m *Model) CursorStart() {
  861. m.SetCursorColumn(0)
  862. }
  863. // CursorEnd moves the cursor to the end of the input field.
  864. func (m *Model) CursorEnd() {
  865. m.SetCursorColumn(len(m.value[m.row]))
  866. }
  867. // Focused returns the focus state on the model.
  868. func (m Model) Focused() bool {
  869. return m.focus
  870. }
  871. // activeStyle returns the appropriate set of styles to use depending on
  872. // whether the textarea is focused or blurred.
  873. func (m Model) activeStyle() *StyleState {
  874. if m.focus {
  875. return &m.Styles.Focused
  876. }
  877. return &m.Styles.Blurred
  878. }
  879. // Focus sets the focus state on the model. When the model is in focus it can
  880. // receive keyboard input and the cursor will be hidden.
  881. func (m *Model) Focus() tea.Cmd {
  882. m.focus = true
  883. return m.virtualCursor.Focus()
  884. }
  885. // Blur removes the focus state on the model. When the model is blurred it can
  886. // not receive keyboard input and the cursor will be hidden.
  887. func (m *Model) Blur() {
  888. m.focus = false
  889. m.virtualCursor.Blur()
  890. }
  891. // Reset sets the input to its default state with no input.
  892. func (m *Model) Reset() {
  893. m.value = make([][]any, minHeight, maxLines)
  894. m.col = 0
  895. m.row = 0
  896. m.SetCursorColumn(0)
  897. }
  898. // san initializes or retrieves the rune sanitizer.
  899. func (m *Model) san() Sanitizer {
  900. if m.rsan == nil {
  901. // Textinput has all its input on a single line so collapse
  902. // newlines/tabs to single spaces.
  903. m.rsan = NewSanitizer()
  904. }
  905. return m.rsan
  906. }
  907. // deleteBeforeCursor deletes all text before the cursor. Returns whether or
  908. // not the cursor blink should be reset.
  909. func (m *Model) deleteBeforeCursor() {
  910. m.value[m.row] = m.value[m.row][m.col:]
  911. m.SetCursorColumn(0)
  912. }
  913. // deleteAfterCursor deletes all text after the cursor. Returns whether or not
  914. // the cursor blink should be reset. If input is masked delete everything after
  915. // the cursor so as not to reveal word breaks in the masked input.
  916. func (m *Model) deleteAfterCursor() {
  917. m.value[m.row] = m.value[m.row][:m.col]
  918. m.SetCursorColumn(len(m.value[m.row]))
  919. }
  920. // transposeLeft exchanges the runes at the cursor and immediately
  921. // before. No-op if the cursor is at the beginning of the line. If
  922. // the cursor is not at the end of the line yet, moves the cursor to
  923. // the right.
  924. func (m *Model) transposeLeft() {
  925. if m.col == 0 || len(m.value[m.row]) < 2 {
  926. return
  927. }
  928. if m.col >= len(m.value[m.row]) {
  929. m.SetCursorColumn(m.col - 1)
  930. }
  931. m.value[m.row][m.col-1], m.value[m.row][m.col] = m.value[m.row][m.col], m.value[m.row][m.col-1]
  932. if m.col < len(m.value[m.row]) {
  933. m.SetCursorColumn(m.col + 1)
  934. }
  935. }
  936. // deleteWordLeft deletes the word left to the cursor. Returns whether or not
  937. // the cursor blink should be reset.
  938. func (m *Model) deleteWordLeft() {
  939. if m.col == 0 || len(m.value[m.row]) == 0 {
  940. return
  941. }
  942. // Linter note: it's critical that we acquire the initial cursor position
  943. // here prior to altering it via SetCursor() below. As such, moving this
  944. // call into the corresponding if clause does not apply here.
  945. oldCol := m.col //nolint:ifshort
  946. m.SetCursorColumn(m.col - 1)
  947. for isSpaceAt(m.value[m.row], m.col) {
  948. if m.col <= 0 {
  949. break
  950. }
  951. // ignore series of whitespace before cursor
  952. m.SetCursorColumn(m.col - 1)
  953. }
  954. for m.col > 0 {
  955. if !isSpaceAt(m.value[m.row], m.col) {
  956. m.SetCursorColumn(m.col - 1)
  957. } else {
  958. if m.col > 0 {
  959. // keep the previous space
  960. m.SetCursorColumn(m.col + 1)
  961. }
  962. break
  963. }
  964. }
  965. if oldCol > len(m.value[m.row]) {
  966. m.value[m.row] = m.value[m.row][:m.col]
  967. } else {
  968. m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][oldCol:]...)
  969. }
  970. }
  971. // deleteWordRight deletes the word right to the cursor.
  972. func (m *Model) deleteWordRight() {
  973. if m.col >= len(m.value[m.row]) || len(m.value[m.row]) == 0 {
  974. return
  975. }
  976. oldCol := m.col
  977. for m.col < len(m.value[m.row]) && isSpaceAt(m.value[m.row], m.col) {
  978. // ignore series of whitespace after cursor
  979. m.SetCursorColumn(m.col + 1)
  980. }
  981. for m.col < len(m.value[m.row]) {
  982. if !isSpaceAt(m.value[m.row], m.col) {
  983. m.SetCursorColumn(m.col + 1)
  984. } else {
  985. break
  986. }
  987. }
  988. if m.col > len(m.value[m.row]) {
  989. m.value[m.row] = m.value[m.row][:oldCol]
  990. } else {
  991. m.value[m.row] = append(m.value[m.row][:oldCol], m.value[m.row][m.col:]...)
  992. }
  993. m.SetCursorColumn(oldCol)
  994. }
  995. // characterRight moves the cursor one character to the right.
  996. func (m *Model) characterRight() {
  997. if m.col < len(m.value[m.row]) {
  998. m.SetCursorColumn(m.col + 1)
  999. } else {
  1000. if m.row < len(m.value)-1 {
  1001. m.row++
  1002. m.CursorStart()
  1003. }
  1004. }
  1005. }
  1006. // characterLeft moves the cursor one character to the left.
  1007. // If insideLine is set, the cursor is moved to the last
  1008. // character in the previous line, instead of one past that.
  1009. func (m *Model) characterLeft(insideLine bool) {
  1010. if m.col == 0 && m.row != 0 {
  1011. m.row--
  1012. m.CursorEnd()
  1013. if !insideLine {
  1014. return
  1015. }
  1016. }
  1017. if m.col > 0 {
  1018. m.SetCursorColumn(m.col - 1)
  1019. }
  1020. }
  1021. // wordLeft moves the cursor one word to the left. Returns whether or not the
  1022. // cursor blink should be reset. If input is masked, move input to the start
  1023. // so as not to reveal word breaks in the masked input.
  1024. func (m *Model) wordLeft() {
  1025. for {
  1026. m.characterLeft(true /* insideLine */)
  1027. if m.col < len(m.value[m.row]) && !isSpaceAt(m.value[m.row], m.col) {
  1028. break
  1029. }
  1030. }
  1031. for m.col > 0 {
  1032. if isSpaceAt(m.value[m.row], m.col-1) {
  1033. break
  1034. }
  1035. m.SetCursorColumn(m.col - 1)
  1036. }
  1037. }
  1038. // wordRight moves the cursor one word to the right. Returns whether or not the
  1039. // cursor blink should be reset. If the input is masked, move input to the end
  1040. // so as not to reveal word breaks in the masked input.
  1041. func (m *Model) wordRight() {
  1042. m.doWordRight(func(int, int) { /* nothing */ })
  1043. }
  1044. func (m *Model) doWordRight(fn func(charIdx int, pos int)) {
  1045. // Skip spaces forward.
  1046. for m.col >= len(m.value[m.row]) || isSpaceAt(m.value[m.row], m.col) {
  1047. if m.row == len(m.value)-1 && m.col == len(m.value[m.row]) {
  1048. // End of text.
  1049. break
  1050. }
  1051. m.characterRight()
  1052. }
  1053. charIdx := 0
  1054. for m.col < len(m.value[m.row]) {
  1055. if isSpaceAt(m.value[m.row], m.col) {
  1056. break
  1057. }
  1058. fn(charIdx, m.col)
  1059. m.SetCursorColumn(m.col + 1)
  1060. charIdx++
  1061. }
  1062. }
  1063. // uppercaseRight changes the word to the right to uppercase.
  1064. func (m *Model) uppercaseRight() {
  1065. m.doWordRight(func(_ int, i int) {
  1066. if r, ok := m.value[m.row][i].(rune); ok {
  1067. m.value[m.row][i] = unicode.ToUpper(r)
  1068. }
  1069. })
  1070. }
  1071. // lowercaseRight changes the word to the right to lowercase.
  1072. func (m *Model) lowercaseRight() {
  1073. m.doWordRight(func(_ int, i int) {
  1074. if r, ok := m.value[m.row][i].(rune); ok {
  1075. m.value[m.row][i] = unicode.ToLower(r)
  1076. }
  1077. })
  1078. }
  1079. // capitalizeRight changes the word to the right to title case.
  1080. func (m *Model) capitalizeRight() {
  1081. m.doWordRight(func(charIdx int, i int) {
  1082. if charIdx == 0 {
  1083. if r, ok := m.value[m.row][i].(rune); ok {
  1084. m.value[m.row][i] = unicode.ToTitle(r)
  1085. }
  1086. }
  1087. })
  1088. }
  1089. // LineInfo returns the number of characters from the start of the
  1090. // (soft-wrapped) line and the (soft-wrapped) line width.
  1091. func (m Model) LineInfo() LineInfo {
  1092. grid := m.memoizedWrap(m.value[m.row], m.width)
  1093. // Find out which line we are currently on. This can be determined by the
  1094. // m.col and counting the number of runes that we need to skip.
  1095. var counter int
  1096. for i, line := range grid {
  1097. start := counter
  1098. end := counter + len(line)
  1099. if m.col >= start && m.col <= end {
  1100. // This is the wrapped line the cursor is on.
  1101. // Special case: if the cursor is at the end of a wrapped line,
  1102. // and there's another wrapped line after it, the cursor should
  1103. // be considered at the beginning of the next line.
  1104. if m.col == end && i < len(grid)-1 {
  1105. nextLine := grid[i+1]
  1106. return LineInfo{
  1107. CharOffset: 0,
  1108. ColumnOffset: 0,
  1109. Height: len(grid),
  1110. RowOffset: i + 1,
  1111. StartColumn: end,
  1112. Width: len(nextLine),
  1113. CharWidth: uniseg.StringWidth(interfacesToString(nextLine)),
  1114. }
  1115. }
  1116. return LineInfo{
  1117. CharOffset: uniseg.StringWidth(interfacesToString(line[:max(0, m.col-start)])),
  1118. ColumnOffset: m.col - start,
  1119. Height: len(grid),
  1120. RowOffset: i,
  1121. StartColumn: start,
  1122. Width: len(line),
  1123. CharWidth: uniseg.StringWidth(interfacesToString(line)),
  1124. }
  1125. }
  1126. counter = end
  1127. }
  1128. return LineInfo{}
  1129. }
  1130. // Width returns the width of the textarea.
  1131. func (m Model) Width() int {
  1132. return m.width
  1133. }
  1134. // moveToBegin moves the cursor to the beginning of the input.
  1135. func (m *Model) moveToBegin() {
  1136. m.row = 0
  1137. m.SetCursorColumn(0)
  1138. }
  1139. // moveToEnd moves the cursor to the end of the input.
  1140. func (m *Model) moveToEnd() {
  1141. m.row = len(m.value) - 1
  1142. m.SetCursorColumn(len(m.value[m.row]))
  1143. }
  1144. // SetWidth sets the width of the textarea to fit exactly within the given width.
  1145. // This means that the textarea will account for the width of the prompt and
  1146. // whether or not line numbers are being shown.
  1147. //
  1148. // Ensure that SetWidth is called after setting the Prompt and ShowLineNumbers,
  1149. // It is important that the width of the textarea be exactly the given width
  1150. // and no more.
  1151. func (m *Model) SetWidth(w int) {
  1152. // Update prompt width only if there is no prompt function as
  1153. // [SetPromptFunc] updates the prompt width when it is called.
  1154. if m.promptFunc == nil {
  1155. // XXX: Do we even need this or can we calculate the prompt width
  1156. // at render time?
  1157. m.promptWidth = uniseg.StringWidth(m.Prompt)
  1158. }
  1159. // Add base style borders and padding to reserved outer width.
  1160. reservedOuter := m.activeStyle().Base.GetHorizontalFrameSize()
  1161. // Add prompt width to reserved inner width.
  1162. reservedInner := m.promptWidth
  1163. // Add line number width to reserved inner width.
  1164. if m.ShowLineNumbers {
  1165. // XXX: this was originally documented as needing "1 cell" but was,
  1166. // in practice, effectively hardcoded to 2 cells. We can, and should,
  1167. // reduce this to one gap and update the tests accordingly.
  1168. const gap = 2
  1169. // Number of digits plus 1 cell for the margin.
  1170. reservedInner += numDigits(m.MaxHeight) + gap
  1171. }
  1172. // Input width must be at least one more than the reserved inner and outer
  1173. // width. This gives us a minimum input width of 1.
  1174. minWidth := reservedInner + reservedOuter + 1
  1175. inputWidth := max(w, minWidth)
  1176. // Input width must be no more than maximum width.
  1177. if m.MaxWidth > 0 {
  1178. inputWidth = min(inputWidth, m.MaxWidth)
  1179. }
  1180. // Since the width of the viewport and input area is dependent on the width of
  1181. // borders, prompt and line numbers, we need to calculate it by subtracting
  1182. // the reserved width from them.
  1183. m.width = inputWidth - reservedOuter - reservedInner
  1184. }
  1185. // SetPromptFunc supersedes the Prompt field and sets a dynamic prompt instead.
  1186. //
  1187. // If the function returns a prompt that is shorter than the specified
  1188. // promptWidth, it will be padded to the left. If it returns a prompt that is
  1189. // longer, display artifacts may occur; the caller is responsible for computing
  1190. // an adequate promptWidth.
  1191. func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIndex int) string) {
  1192. m.promptFunc = fn
  1193. m.promptWidth = promptWidth
  1194. }
  1195. // Height returns the current height of the textarea.
  1196. func (m Model) Height() int {
  1197. return m.height
  1198. }
  1199. // ContentHeight returns the actual height needed to display all content
  1200. // including wrapped lines.
  1201. func (m Model) ContentHeight() int {
  1202. totalLines := 0
  1203. for _, line := range m.value {
  1204. wrappedLines := m.memoizedWrap(line, m.width)
  1205. totalLines += len(wrappedLines)
  1206. }
  1207. // Ensure at least one line is shown
  1208. if totalLines == 0 {
  1209. totalLines = 1
  1210. }
  1211. return totalLines
  1212. }
  1213. // SetHeight sets the height of the textarea.
  1214. func (m *Model) SetHeight(h int) {
  1215. // Calculate the actual content height
  1216. contentHeight := m.ContentHeight()
  1217. // Use the content height as the actual height
  1218. if m.MaxHeight > 0 {
  1219. m.height = clamp(contentHeight, minHeight, m.MaxHeight)
  1220. } else {
  1221. m.height = max(contentHeight, minHeight)
  1222. }
  1223. }
  1224. // Update is the Bubble Tea update loop.
  1225. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
  1226. if !m.focus {
  1227. m.virtualCursor.Blur()
  1228. return m, nil
  1229. }
  1230. // Used to determine if the cursor should blink.
  1231. oldRow, oldCol := m.cursorLineNumber(), m.col
  1232. var cmds []tea.Cmd
  1233. if m.row >= len(m.value) {
  1234. m.value = append(m.value, make([]any, 0))
  1235. }
  1236. if m.value[m.row] == nil {
  1237. m.value[m.row] = make([]any, 0)
  1238. }
  1239. if m.MaxHeight > 0 && m.MaxHeight != m.cache.Capacity() {
  1240. m.cache = NewMemoCache[line, [][]any](m.MaxHeight)
  1241. }
  1242. switch msg := msg.(type) {
  1243. case tea.KeyPressMsg:
  1244. switch {
  1245. case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
  1246. m.col = clamp(m.col, 0, len(m.value[m.row]))
  1247. if m.col >= len(m.value[m.row]) {
  1248. m.mergeLineBelow(m.row)
  1249. break
  1250. }
  1251. m.deleteAfterCursor()
  1252. case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
  1253. m.col = clamp(m.col, 0, len(m.value[m.row]))
  1254. if m.col <= 0 {
  1255. m.mergeLineAbove(m.row)
  1256. break
  1257. }
  1258. m.deleteBeforeCursor()
  1259. case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
  1260. m.col = clamp(m.col, 0, len(m.value[m.row]))
  1261. if m.col <= 0 {
  1262. m.mergeLineAbove(m.row)
  1263. break
  1264. }
  1265. if len(m.value[m.row]) > 0 && m.col > 0 {
  1266. m.value[m.row] = slices.Delete(m.value[m.row], m.col-1, m.col)
  1267. m.SetCursorColumn(m.col - 1)
  1268. }
  1269. case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
  1270. if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
  1271. m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
  1272. }
  1273. if m.col >= len(m.value[m.row]) {
  1274. m.mergeLineBelow(m.row)
  1275. break
  1276. }
  1277. case key.Matches(msg, m.KeyMap.DeleteWordBackward):
  1278. if m.col <= 0 {
  1279. m.mergeLineAbove(m.row)
  1280. break
  1281. }
  1282. m.deleteWordLeft()
  1283. case key.Matches(msg, m.KeyMap.DeleteWordForward):
  1284. m.col = clamp(m.col, 0, len(m.value[m.row]))
  1285. if m.col >= len(m.value[m.row]) {
  1286. m.mergeLineBelow(m.row)
  1287. break
  1288. }
  1289. m.deleteWordRight()
  1290. case key.Matches(msg, m.KeyMap.InsertNewline):
  1291. m.Newline()
  1292. case key.Matches(msg, m.KeyMap.LineEnd):
  1293. m.CursorEnd()
  1294. case key.Matches(msg, m.KeyMap.LineStart):
  1295. m.CursorStart()
  1296. case key.Matches(msg, m.KeyMap.CharacterForward):
  1297. m.characterRight()
  1298. case key.Matches(msg, m.KeyMap.LineNext):
  1299. m.CursorDown()
  1300. case key.Matches(msg, m.KeyMap.WordForward):
  1301. m.wordRight()
  1302. case key.Matches(msg, m.KeyMap.CharacterBackward):
  1303. m.characterLeft(false /* insideLine */)
  1304. case key.Matches(msg, m.KeyMap.LinePrevious):
  1305. m.CursorUp()
  1306. case key.Matches(msg, m.KeyMap.WordBackward):
  1307. m.wordLeft()
  1308. case key.Matches(msg, m.KeyMap.InputBegin):
  1309. m.moveToBegin()
  1310. case key.Matches(msg, m.KeyMap.InputEnd):
  1311. m.moveToEnd()
  1312. case key.Matches(msg, m.KeyMap.LowercaseWordForward):
  1313. m.lowercaseRight()
  1314. case key.Matches(msg, m.KeyMap.UppercaseWordForward):
  1315. m.uppercaseRight()
  1316. case key.Matches(msg, m.KeyMap.CapitalizeWordForward):
  1317. m.capitalizeRight()
  1318. case key.Matches(msg, m.KeyMap.TransposeCharacterBackward):
  1319. m.transposeLeft()
  1320. default:
  1321. m.InsertRunesFromUserInput([]rune(msg.Text))
  1322. }
  1323. case pasteMsg:
  1324. m.InsertRunesFromUserInput([]rune(msg))
  1325. case pasteErrMsg:
  1326. m.Err = msg
  1327. }
  1328. var cmd tea.Cmd
  1329. newRow, newCol := m.cursorLineNumber(), m.col
  1330. m.virtualCursor, cmd = m.virtualCursor.Update(msg)
  1331. if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
  1332. m.virtualCursor.Blink = false
  1333. cmd = m.virtualCursor.BlinkCmd()
  1334. }
  1335. cmds = append(cmds, cmd)
  1336. return m, tea.Batch(cmds...)
  1337. }
  1338. // View renders the text area in its current state.
  1339. func (m Model) View() string {
  1340. m.updateVirtualCursorStyle()
  1341. if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
  1342. return m.placeholderView()
  1343. }
  1344. m.virtualCursor.TextStyle = m.activeStyle().computedCursorLine()
  1345. var (
  1346. s strings.Builder
  1347. style lipgloss.Style
  1348. newLines int
  1349. widestLineNumber int
  1350. lineInfo = m.LineInfo()
  1351. styles = m.activeStyle()
  1352. )
  1353. displayLine := 0
  1354. for l, line := range m.value {
  1355. wrappedLines := m.memoizedWrap(line, m.width)
  1356. if m.row == l {
  1357. style = styles.computedCursorLine()
  1358. } else {
  1359. style = styles.computedText()
  1360. }
  1361. for wl, wrappedLine := range wrappedLines {
  1362. prompt := m.promptView(displayLine)
  1363. prompt = styles.computedPrompt().Render(prompt)
  1364. s.WriteString(style.Render(prompt))
  1365. displayLine++
  1366. var ln string
  1367. if m.ShowLineNumbers {
  1368. if wl == 0 { // normal line
  1369. isCursorLine := m.row == l
  1370. s.WriteString(m.lineNumberView(l+1, isCursorLine))
  1371. } else { // soft wrapped line
  1372. isCursorLine := m.row == l
  1373. s.WriteString(m.lineNumberView(-1, isCursorLine))
  1374. }
  1375. }
  1376. // Note the widest line number for padding purposes later.
  1377. lnw := uniseg.StringWidth(ln)
  1378. if lnw > widestLineNumber {
  1379. widestLineNumber = lnw
  1380. }
  1381. wrappedLineStr := interfacesToString(wrappedLine)
  1382. strwidth := uniseg.StringWidth(wrappedLineStr)
  1383. padding := m.width - strwidth
  1384. // If the trailing space causes the line to be wider than the
  1385. // width, we should not draw it to the screen since it will result
  1386. // in an extra space at the end of the line which can look off when
  1387. // the cursor line is showing.
  1388. if strwidth > m.width {
  1389. // The character causing the line to be wider than the width is
  1390. // guaranteed to be a space since any other character would
  1391. // have been wrapped.
  1392. wrappedLineStr = strings.TrimSuffix(wrappedLineStr, " ")
  1393. padding -= m.width - strwidth
  1394. }
  1395. if m.row == l && lineInfo.RowOffset == wl {
  1396. // Render the part of the line before the cursor
  1397. s.WriteString(
  1398. m.renderLineWithAttachments(
  1399. wrappedLine[:lineInfo.ColumnOffset],
  1400. style,
  1401. ),
  1402. )
  1403. if m.col >= len(line) && lineInfo.CharOffset >= m.width {
  1404. m.virtualCursor.SetChar(" ")
  1405. s.WriteString(m.virtualCursor.View())
  1406. } else if lineInfo.ColumnOffset < len(wrappedLine) {
  1407. // Render the item under the cursor
  1408. item := wrappedLine[lineInfo.ColumnOffset]
  1409. if att, ok := item.(*Attachment); ok {
  1410. // Item at cursor is an attachment. Render it with the selection style.
  1411. // This becomes the "cursor" visually.
  1412. s.WriteString(m.Styles.SelectedAttachment.Render(att.Display))
  1413. } else {
  1414. // Item at cursor is a rune. Render it with the virtual cursor.
  1415. m.virtualCursor.SetChar(string(item.(rune)))
  1416. s.WriteString(style.Render(m.virtualCursor.View()))
  1417. }
  1418. // Render the part of the line after the cursor
  1419. s.WriteString(m.renderLineWithAttachments(wrappedLine[lineInfo.ColumnOffset+1:], style))
  1420. } else {
  1421. // Cursor is at the end of the line
  1422. m.virtualCursor.SetChar(" ")
  1423. s.WriteString(style.Render(m.virtualCursor.View()))
  1424. }
  1425. } else {
  1426. s.WriteString(m.renderLineWithAttachments(wrappedLine, style))
  1427. }
  1428. s.WriteString(style.Render(strings.Repeat(" ", max(0, padding))))
  1429. s.WriteRune('\n')
  1430. newLines++
  1431. }
  1432. }
  1433. // Remove the trailing newline from the last line
  1434. result := s.String()
  1435. if len(result) > 0 && result[len(result)-1] == '\n' {
  1436. result = result[:len(result)-1]
  1437. }
  1438. return styles.Base.Render(result)
  1439. }
  1440. // promptView renders a single line of the prompt.
  1441. func (m Model) promptView(displayLine int) (prompt string) {
  1442. prompt = m.Prompt
  1443. if m.promptFunc == nil {
  1444. return prompt
  1445. }
  1446. prompt = m.promptFunc(displayLine)
  1447. width := lipgloss.Width(prompt)
  1448. if width < m.promptWidth {
  1449. prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
  1450. }
  1451. return m.activeStyle().computedPrompt().Render(prompt)
  1452. }
  1453. // lineNumberView renders the line number.
  1454. //
  1455. // If the argument is less than 0, a space styled as a line number is returned
  1456. // instead. Such cases are used for soft-wrapped lines.
  1457. //
  1458. // The second argument indicates whether this line number is for a 'cursorline'
  1459. // line number.
  1460. func (m Model) lineNumberView(n int, isCursorLine bool) (str string) {
  1461. if !m.ShowLineNumbers {
  1462. return ""
  1463. }
  1464. if n <= 0 {
  1465. str = " "
  1466. } else {
  1467. str = strconv.Itoa(n)
  1468. }
  1469. // XXX: is textStyle really necessary here?
  1470. textStyle := m.activeStyle().computedText()
  1471. lineNumberStyle := m.activeStyle().computedLineNumber()
  1472. if isCursorLine {
  1473. textStyle = m.activeStyle().computedCursorLine()
  1474. lineNumberStyle = m.activeStyle().computedCursorLineNumber()
  1475. }
  1476. // Format line number dynamically based on the maximum number of lines.
  1477. digits := len(strconv.Itoa(m.MaxHeight))
  1478. str = fmt.Sprintf(" %*v ", digits, str)
  1479. return textStyle.Render(lineNumberStyle.Render(str))
  1480. }
  1481. // placeholderView returns the prompt and placeholder, if any.
  1482. func (m Model) placeholderView() string {
  1483. var (
  1484. s strings.Builder
  1485. p = m.Placeholder
  1486. styles = m.activeStyle()
  1487. )
  1488. // word wrap lines
  1489. pwordwrap := ansi.Wordwrap(p, m.width, "")
  1490. // hard wrap lines (handles lines that could not be word wrapped)
  1491. pwrap := ansi.Hardwrap(pwordwrap, m.width, true)
  1492. // split string by new lines
  1493. plines := strings.Split(strings.TrimSpace(pwrap), "\n")
  1494. // Only render the actual placeholder lines, not padded to m.height
  1495. maxLines := max(len(plines), 1) // At least show one line for cursor
  1496. for i := range maxLines {
  1497. isLineNumber := len(plines) > i
  1498. lineStyle := styles.computedPlaceholder()
  1499. if len(plines) > i {
  1500. lineStyle = styles.computedCursorLine()
  1501. }
  1502. // render prompt
  1503. prompt := m.promptView(i)
  1504. prompt = styles.computedPrompt().Render(prompt)
  1505. s.WriteString(lineStyle.Render(prompt))
  1506. // when show line numbers enabled:
  1507. // - render line number for only the cursor line
  1508. // - indent other placeholder lines
  1509. // this is consistent with vim with line numbers enabled
  1510. if m.ShowLineNumbers {
  1511. var ln int
  1512. switch {
  1513. case i == 0:
  1514. ln = i + 1
  1515. fallthrough
  1516. case len(plines) > i:
  1517. s.WriteString(m.lineNumberView(ln, isLineNumber))
  1518. default:
  1519. }
  1520. }
  1521. switch {
  1522. // first line
  1523. case i == 0:
  1524. // first character of first line as cursor with character
  1525. m.virtualCursor.TextStyle = styles.computedPlaceholder()
  1526. m.virtualCursor.SetChar(string(plines[0][0]))
  1527. s.WriteString(lineStyle.Render(m.virtualCursor.View()))
  1528. // the rest of the first line
  1529. placeholderTail := plines[0][1:]
  1530. gap := strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0])))
  1531. renderedPlaceholder := styles.computedPlaceholder().Render(placeholderTail + gap)
  1532. s.WriteString(lineStyle.Render(renderedPlaceholder))
  1533. // remaining lines
  1534. case len(plines) > i:
  1535. // current line placeholder text
  1536. if len(plines) > i {
  1537. placeholderLine := plines[i]
  1538. gap := strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[i])))
  1539. s.WriteString(lineStyle.Render(placeholderLine + gap))
  1540. }
  1541. default:
  1542. // end of line buffer character
  1543. eob := styles.computedEndOfBuffer().Render(string(m.EndOfBufferCharacter))
  1544. s.WriteString(eob)
  1545. }
  1546. // terminate with new line (except for last line)
  1547. if i < maxLines-1 {
  1548. s.WriteRune('\n')
  1549. }
  1550. }
  1551. return styles.Base.Render(s.String())
  1552. }
  1553. // Blink returns the blink command for the virtual cursor.
  1554. func Blink() tea.Msg {
  1555. return cursor.Blink()
  1556. }
  1557. // Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea
  1558. // program. This requires that [Model.VirtualCursor] is set to false.
  1559. //
  1560. // Note that you will almost certainly also need to adjust the offset cursor
  1561. // position per the textarea's per the textarea's position in the terminal.
  1562. //
  1563. // Example:
  1564. //
  1565. // // In your top-level View function:
  1566. // f := tea.NewFrame(m.textarea.View())
  1567. // f.Cursor = m.textarea.Cursor()
  1568. // f.Cursor.Position.X += offsetX
  1569. // f.Cursor.Position.Y += offsetY
  1570. func (m Model) Cursor() *tea.Cursor {
  1571. if m.VirtualCursor {
  1572. return nil
  1573. }
  1574. lineInfo := m.LineInfo()
  1575. w := lipgloss.Width
  1576. baseStyle := m.activeStyle().Base
  1577. xOffset := lineInfo.CharOffset +
  1578. w(m.promptView(0)) +
  1579. w(m.lineNumberView(0, false)) +
  1580. baseStyle.GetMarginLeft() +
  1581. baseStyle.GetPaddingLeft() +
  1582. baseStyle.GetBorderLeftSize()
  1583. yOffset := m.cursorLineNumber() -
  1584. baseStyle.GetMarginTop() +
  1585. baseStyle.GetPaddingTop() +
  1586. baseStyle.GetBorderTopSize()
  1587. c := tea.NewCursor(xOffset, yOffset)
  1588. c.Blink = m.Styles.Cursor.Blink
  1589. c.Color = m.Styles.Cursor.Color
  1590. c.Shape = m.Styles.Cursor.Shape
  1591. return c
  1592. }
  1593. func (m Model) memoizedWrap(content []any, width int) [][]any {
  1594. input := line{content: content, width: width}
  1595. if v, ok := m.cache.Get(input); ok {
  1596. return v
  1597. }
  1598. v := wrapInterfaces(content, width)
  1599. m.cache.Set(input, v)
  1600. return v
  1601. }
  1602. // cursorLineNumber returns the line number that the cursor is on.
  1603. // This accounts for soft wrapped lines.
  1604. func (m Model) cursorLineNumber() int {
  1605. line := 0
  1606. for i := range m.row {
  1607. // Calculate the number of lines that the current line will be split
  1608. // into.
  1609. line += len(m.memoizedWrap(m.value[i], m.width))
  1610. }
  1611. line += m.LineInfo().RowOffset
  1612. return line
  1613. }
  1614. // mergeLineBelow merges the current line the cursor is on with the line below.
  1615. func (m *Model) mergeLineBelow(row int) {
  1616. if row >= len(m.value)-1 {
  1617. return
  1618. }
  1619. // To perform a merge, we will need to combine the two lines and then
  1620. m.value[row] = append(m.value[row], m.value[row+1]...)
  1621. // Shift all lines up by one
  1622. for i := row + 1; i < len(m.value)-1; i++ {
  1623. m.value[i] = m.value[i+1]
  1624. }
  1625. // And, remove the last line
  1626. if len(m.value) > 0 {
  1627. m.value = m.value[:len(m.value)-1]
  1628. }
  1629. }
  1630. // mergeLineAbove merges the current line the cursor is on with the line above.
  1631. func (m *Model) mergeLineAbove(row int) {
  1632. if row <= 0 {
  1633. return
  1634. }
  1635. m.col = len(m.value[row-1])
  1636. m.row = m.row - 1
  1637. // To perform a merge, we will need to combine the two lines and then
  1638. m.value[row-1] = append(m.value[row-1], m.value[row]...)
  1639. // Shift all lines up by one
  1640. for i := row; i < len(m.value)-1; i++ {
  1641. m.value[i] = m.value[i+1]
  1642. }
  1643. // And, remove the last line
  1644. if len(m.value) > 0 {
  1645. m.value = m.value[:len(m.value)-1]
  1646. }
  1647. }
  1648. func (m *Model) splitLine(row, col int) {
  1649. // To perform a split, take the current line and keep the content before
  1650. // the cursor, take the content after the cursor and make it the content of
  1651. // the line underneath, and shift the remaining lines down by one
  1652. head, tailSrc := m.value[row][:col], m.value[row][col:]
  1653. tail := copyInterfaceSlice(tailSrc)
  1654. m.value = append(m.value[:row+1], m.value[row:]...)
  1655. m.value[row] = head
  1656. m.value[row+1] = tail
  1657. m.col = 0
  1658. m.row++
  1659. }
  1660. func wrapInterfaces(content []any, width int) [][]any {
  1661. if width <= 0 {
  1662. return [][]any{content}
  1663. }
  1664. var (
  1665. lines = [][]any{{}}
  1666. word = []any{}
  1667. wordW int
  1668. lineW int
  1669. spaceW int
  1670. inSpaces bool
  1671. )
  1672. for _, item := range content {
  1673. itemW := 0
  1674. isSpace := false
  1675. if r, ok := item.(rune); ok {
  1676. if unicode.IsSpace(r) {
  1677. isSpace = true
  1678. }
  1679. itemW = rw.RuneWidth(r)
  1680. } else if att, ok := item.(*Attachment); ok {
  1681. itemW = uniseg.StringWidth(att.Display)
  1682. }
  1683. if isSpace {
  1684. if !inSpaces {
  1685. // End of a word
  1686. if lineW > 0 && lineW+wordW > width {
  1687. lines = append(lines, word)
  1688. lineW = wordW
  1689. } else {
  1690. lines[len(lines)-1] = append(lines[len(lines)-1], word...)
  1691. lineW += wordW
  1692. }
  1693. word = nil
  1694. wordW = 0
  1695. }
  1696. inSpaces = true
  1697. spaceW += itemW
  1698. } else {
  1699. if inSpaces {
  1700. // End of spaces
  1701. if lineW > 0 && lineW+spaceW > width {
  1702. lines = append(lines, []any{})
  1703. lineW = 0
  1704. } else {
  1705. lineW += spaceW
  1706. }
  1707. // Add spaces to current line
  1708. for i := 0; i < spaceW; i++ {
  1709. lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
  1710. }
  1711. spaceW = 0
  1712. }
  1713. inSpaces = false
  1714. word = append(word, item)
  1715. wordW += itemW
  1716. }
  1717. }
  1718. // Handle any remaining word/spaces
  1719. if wordW > 0 {
  1720. if lineW > 0 && lineW+wordW > width {
  1721. lines = append(lines, word)
  1722. } else {
  1723. lines[len(lines)-1] = append(lines[len(lines)-1], word...)
  1724. }
  1725. }
  1726. if spaceW > 0 {
  1727. if lineW > 0 && lineW+spaceW > width {
  1728. lines = append(lines, []any{})
  1729. }
  1730. for i := 0; i < spaceW; i++ {
  1731. lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
  1732. }
  1733. }
  1734. return lines
  1735. }
  1736. func repeatSpaces(n int) []rune {
  1737. return []rune(strings.Repeat(string(' '), n))
  1738. }
  1739. // numDigits returns the number of digits in an integer.
  1740. func numDigits(n int) int {
  1741. if n == 0 {
  1742. return 1
  1743. }
  1744. count := 0
  1745. num := abs(n)
  1746. for num > 0 {
  1747. count++
  1748. num /= 10
  1749. }
  1750. return count
  1751. }
  1752. func clamp(v, low, high int) int {
  1753. if high < low {
  1754. low, high = high, low
  1755. }
  1756. return min(high, max(low, v))
  1757. }
  1758. func abs(n int) int {
  1759. if n < 0 {
  1760. return -n
  1761. }
  1762. return n
  1763. }