Prototype of other parser.

This commit is contained in:
Daniel Schmidt 2023-05-23 20:55:59 +02:00
parent 032bbda893
commit 7491df1479
3 changed files with 895 additions and 0 deletions

335
ansi/parse.go Normal file
View File

@ -0,0 +1,335 @@
package ansi
import (
"github.com/muesli/termenv"
)
type SequenceType int
type SGRType int
const (
SequenceTypeNone SequenceType = iota
SequenceTypeCursorUp
SequenceTypeCursorDown
SequenceTypeCursorForward
SequenceTypeCursorBack
SequenceTypeCursorNextLine
SequenceTypeCursorPreviousLine
SequenceTypeCursorHorizontal
SequenceTypeCursorPosition
SequenceTypeEraseDisplay
SequenceTypeEraseLine
SequenceTypeScrollUp
SequenceTypeScrollDown
SequenceTypeSaveCursorPosition
SequenceTypeSGR
SequenceTypeShowCursor
SequenceTypeHideCursor
SequenceTypeUnimplemented
)
const (
SGRTypeReset SGRType = 0
SGRTypeBold SGRType = 1
SGRTypeUnsetBold SGRType = 22
SGRTypeItalic SGRType = 3
SGRTypeUnsetItalic SGRType = 23
SGRTypeUnderline SGRType = 4
SGRTypeUnsetUnderline SGRType = 24
SGRTypeFgColor SGRType = 38
SGRTypeBgColor SGRType = 48
SGRTypeFgDefaultColor SGRType = 39
SGRTypeBgDefaultColor SGRType = 49
)
const (
// States for the internal state machine
stateStart = 0
stateParam = 1
stateInter = 2
)
var (
csi = []rune(termenv.CSI)
)
type Cursor struct {
SeqType SequenceType
runes *[]rune
start, end int
}
func (c *Cursor) Len() int {
diff := c.end - c.start
if diff < 0 {
return 0
}
return diff
}
func (c *Cursor) View() []rune {
if c.Len() == 0 {
return nil
}
return (*c.runes)[c.start:c.end]
}
func (c *Cursor) Arg1() byte {
if c.Len() == 0 {
return 0
}
id, _ := parseNum(c.View())
return byte(id)
}
func (c *Cursor) Arg2() (byte, byte) {
if c.Len() == 0 {
return 0, 0
}
a1, off := parseNum(c.View())
a2, _ := parseNum(c.View()[off+1:])
return byte(a1), byte(a2)
}
func (c *Cursor) VisitSGR(fn func(t SGRType, params []byte)) {
if c.SeqType != SequenceTypeSGR {
return
}
// No parameters at all in ESC[m acts like a 0 reset code
if c.Len() == 0 {
fn(SGRTypeReset, []byte{})
return
}
params := make([]byte, 0, 3)
// We have parameters, so we parse them
view := c.View()
for i := 0; i < len(view); i++ {
if id, off := parseNum(view[i:]); off > 0 {
i += off - 1
t := SGRType(id)
switch t {
case SGRTypeFgColor, SGRTypeBgColor:
if ok, trueColor, r, g, b, off := parseColor(view[i+2:]); ok {
i += off + 2
if trueColor {
params = append(params[:0], byte(r), byte(g), byte(b))
} else {
params = append(params[:0], byte(r))
}
fn(t, params)
}
default:
fn(t, nil)
}
}
}
}
func (c *Cursor) reset(start, end int) {
c.SeqType = SequenceTypeNone
c.start = start
c.end = end
}
func Parse(runes []rune, fn func(cursor *Cursor)) {
state := stateStart
cursor := &Cursor{SeqType: SequenceTypeNone, runes: &runes, start: 0, end: 0}
for i := 0; i < len(runes); i++ {
switch state {
// Start state where we don't know what the sequence is, or if it even is one.
case stateStart:
if isCSI(runes[i:]) {
// If we have some runes left we visit them
if cursor.Len() > 0 {
cursor.end = i
fn(cursor)
}
cursor.start = i + 2
cursor.end = cursor.start
i += 1
// We found the CSI start, so we go to the param state
state = stateParam
} else {
cursor.SeqType = SequenceTypeNone
cursor.end = i + 1
}
// In the param state we look for the end of the param sequence.
case stateParam:
if isParam(runes[i]) {
cursor.end = i
} else if isInter(runes[i]) {
state = stateInter
i -= 1
} else if isFinal(runes[i]) {
state = stateStart
cursor.end = i
cursor.SeqType = finalToType(runes[i], cursor)
fn(cursor)
cursor.reset(i+1, i+1)
} else {
state = stateStart
cursor.reset(i, i)
}
// In the inter state we look for the end of the intermediate sequence.
case stateInter:
if isInter(runes[i]) {
cursor.end = i
} else if isFinal(runes[i]) {
state = stateStart
cursor.end = i
cursor.SeqType = finalToType(runes[i], cursor)
fn(cursor)
cursor.reset(i+1, i+1)
} else {
state = stateStart
cursor.reset(i, i)
}
}
}
if cursor.Len() > 0 {
fn(cursor)
}
}
func parseColor(rune []rune) (bool, bool, byte, byte, byte, int) {
// Check if it can be a color sequence
if len(rune) < 2 || !(rune[0] == '5' || rune[0] == '2') || rune[1] != ';' {
return false, false, 0, 0, 0, 2
}
// 5;n case
if rune[0] == '5' {
// If n is not there the color id is 0
if len(rune) == 2 {
return true, false, 0, 0, 0, len(rune)
}
if num, off := parseNum(rune[2:]); off > 0 {
return true, false, byte(num), 0, 0, 2 + off
}
return false, false, 0, 0, 0, 2
}
rgb := make([]byte, 3)
comp := 0
stop := 0
// 2;r;g;b case
for i := 2; i < len(rune); i++ {
if v, off := parseNum(rune[i:]); off > 0 {
i += off - 1
rgb[comp] = byte(v)
comp++
}
stop = i
if comp == 3 {
break
}
}
return true, true, rgb[0], rgb[1], rgb[2], stop
}
func isCSI(rune []rune) bool {
if len(rune) == 1 {
return false
}
return rune[0] == csi[0] && rune[1] == csi[1]
}
func isParam(r rune) bool {
return r >= 0x30 && r <= 0x3F
}
func isInter(r rune) bool {
return r >= 0x20 && r <= 0x2F
}
func isFinal(r rune) bool {
return r >= 0x40 && r <= 0x7E
}
func isNum(r rune) bool {
return r >= 0x30 && r <= 0x39
}
func parseNum(rune []rune) (int, int) {
if len(rune) == 0 {
return 0, 0
}
var num int
var digits int
for _, r := range rune {
if isNum(r) {
num = num*10 + int(r-'0')
digits++
} else {
break
}
}
return num, digits
}
func finalToType(lastRune rune, cursor *Cursor) SequenceType {
switch lastRune {
case 'A':
return SequenceTypeCursorUp
case 'B':
return SequenceTypeCursorDown
case 'C':
return SequenceTypeCursorForward
case 'D':
return SequenceTypeCursorBack
case 'E':
return SequenceTypeCursorNextLine
case 'F':
return SequenceTypeCursorPreviousLine
case 'G':
return SequenceTypeCursorHorizontal
case 'H':
return SequenceTypeCursorPosition
case 'J':
return SequenceTypeEraseDisplay
case 'K':
return SequenceTypeEraseLine
case 'S':
return SequenceTypeScrollUp
case 'T':
return SequenceTypeScrollDown
case 's':
return SequenceTypeSaveCursorPosition
case 'm':
return SequenceTypeSGR
case 'h':
view := cursor.View()
if cursor.Len() >= 3 && view[len(view)-3] == '?' && view[len(view)-2] == '2' && view[len(view)-1] == '5' {
return SequenceTypeShowCursor
}
return SequenceTypeUnimplemented
case 'l':
view := cursor.View()
if cursor.Len() >= 3 && view[len(view)-3] == '?' && view[len(view)-2] == '2' && view[len(view)-1] == '5' {
return SequenceTypeHideCursor
}
return SequenceTypeUnimplemented
}
return SequenceTypeNone
}

436
ansi/parse_test.go Normal file
View File

@ -0,0 +1,436 @@
package ansi
import (
"bytes"
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/assert"
"math/rand"
"strings"
"testing"
)
func TestParseNum(t *testing.T) {
for i := 0; i < 1000; i++ {
num := i
if i > 255 {
num = rand.Intn(1000)
}
s := fmt.Sprintf("%d;", num)
pnum, off := parseNum([]rune(s))
assert.Equal(t, num, pnum)
assert.Equal(t, len(s)-1, off)
}
}
func randomString(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func removeAnsiReset(s string) string {
return strings.Replace(s, "\x1b[0m", "", 1)
}
func TestParse(t *testing.T) {
lipgloss.SetColorProfile(termenv.TrueColor)
type SGRArgs struct {
Bold bool
Italic bool
Underline bool
Arg1 byte
Arg2 byte
Arg3 byte
}
type Args struct {
Arg1 byte
Arg2 byte
SGR SGRArgs
}
type SequenceTester struct {
Args int
Gen func() (string, Args)
Check func(cursor *Cursor, args Args)
}
type SelectedTester struct {
Name string
Args Args
}
sequenceTests := map[string]SequenceTester{
"CursorUp": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorUpSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorUp, cursor.SeqType, "CursorUp")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorUp")
},
},
"CursorDown": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorDownSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorDown, cursor.SeqType, "CursorDown")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorDown")
},
},
"CursorForward": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorForwardSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorForward, cursor.SeqType, "CursorForward")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorForward")
},
},
"CursorBack": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorBackSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorBack, cursor.SeqType, "CursorBack")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorBack")
},
},
"CursorNextLine": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorNextLineSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorNextLine, cursor.SeqType, "CursorNextLine")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorNextLine")
},
},
"CursorPreviousLine": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorPreviousLineSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorPreviousLine, cursor.SeqType, "CursorPreviousLine")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorPreviousLine")
},
},
"CursorHorizontalAbsolute": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorHorizontalSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorHorizontal, cursor.SeqType, "CursorHorizontalAbsolute")
assert.Equal(t, args.Arg1, cursor.Arg1(), "CursorHorizontalAbsolute")
},
},
"CursorPosition": {
Args: 2,
Gen: func() (string, Args) {
arg1 := byte(rand.Intn(255))
arg2 := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, arg1, arg2), Args{
Arg1: arg1,
Arg2: arg2,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeCursorPosition, cursor.SeqType, "CursorPosition")
arg1, arg2 := cursor.Arg2()
assert.Equal(t, args.Arg1, arg1, "CursorPosition")
assert.Equal(t, args.Arg2, arg2, "CursorPosition")
},
},
"CursorShow": {
Args: 0,
Gen: func() (string, Args) {
return fmt.Sprintf(termenv.CSI + termenv.ShowCursorSeq), Args{
Arg1: 0,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeShowCursor, cursor.SeqType, "CursorShow")
},
},
"CursorHide": {
Args: 0,
Gen: func() (string, Args) {
return fmt.Sprintf(termenv.CSI + termenv.HideCursorSeq), Args{
Arg1: 0,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeHideCursor, cursor.SeqType, "CursorHide")
},
},
"EraseDisplay": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(3))
return fmt.Sprintf(termenv.CSI+termenv.EraseDisplaySeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeEraseDisplay, cursor.SeqType, "EraseDisplay")
assert.Equal(t, args.Arg1, cursor.Arg1(), "EraseDisplay")
},
},
"EraseLine": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(3))
return fmt.Sprintf(termenv.CSI+termenv.EraseLineSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeEraseLine, cursor.SeqType, "EraseLine")
assert.Equal(t, args.Arg1, cursor.Arg1(), "EraseLine")
},
},
"ScrollUp": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.ScrollUpSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeScrollUp, cursor.SeqType, "ScrollUp")
assert.Equal(t, args.Arg1, cursor.Arg1(), "ScrollUp")
},
},
"ScrollDown": {
Args: 1,
Gen: func() (string, Args) {
arg := byte(rand.Intn(255))
return fmt.Sprintf(termenv.CSI+termenv.ScrollDownSeq, arg), Args{
Arg1: arg,
Arg2: 0,
}
},
Check: func(cursor *Cursor, args Args) {
assert.Equal(t, SequenceTypeScrollDown, cursor.SeqType, "ScrollDown")
},
},
"SGR": {
Args: 0,
Gen: func() (string, Args) {
bold := rand.Intn(2) == 1
italic := rand.Intn(2) == 1
underline := rand.Intn(2) == 1
if rand.Intn(2) == 1 {
color := byte(rand.Intn(255))
return removeAnsiReset(lipgloss.NewStyle().Bold(bold).Italic(italic).Underline(underline).Foreground(lipgloss.Color(fmt.Sprint(color))).Render("X")), Args{
Arg1: 0,
Arg2: 0,
SGR: SGRArgs{
Bold: bold,
Italic: italic,
Underline: underline,
Arg1: color,
},
}
}
r := byte(rand.Intn(255))
g := byte(rand.Intn(255))
b := byte(rand.Intn(255))
return removeAnsiReset(lipgloss.NewStyle().Bold(bold).Italic(italic).Underline(underline).Foreground(lipgloss.Color(fmt.Sprintf("#%02X%02X%02X", r, g, b))).Render("X")), Args{
Arg1: 0,
Arg2: 0,
SGR: SGRArgs{
Bold: bold,
Italic: italic,
Underline: underline,
Arg1: r,
Arg2: g,
Arg3: b,
},
}
},
Check: func(cursor *Cursor, args Args) {
if !assert.Equal(t, SequenceTypeSGR, cursor.SeqType, "SGR") {
return
}
cursor.VisitSGR(func(code SGRType, params []byte) {
switch code {
case SGRTypeBold:
assert.Equal(t, true, args.SGR.Bold, "SGR")
case SGRTypeItalic:
assert.Equal(t, true, args.SGR.Italic, "SGR")
case SGRTypeUnderline:
assert.Equal(t, true, args.SGR.Underline, "SGR")
case SGRTypeFgColor:
if len(params) == 1 {
assert.Equal(t, args.SGR.Arg1, params[0], "SGR")
} else {
// Termenv seems to change colors slightly
assert.InDelta(t, args.SGR.Arg1, params[0], 2, "SGR")
assert.InDelta(t, args.SGR.Arg2, params[1], 2, "SGR")
assert.InDelta(t, args.SGR.Arg3, params[2], 2, "SGR")
}
}
})
},
},
}
var possibleTests []string
var selected []SelectedTester
var testString string
// Try everything at least once
for name, test := range sequenceTests {
possibleTests = append(possibleTests, name)
str, expectedArgs := test.Gen()
testString += str
selected = append(selected, SelectedTester{
Name: name,
Args: expectedArgs,
})
}
// Randomly generate 100 extra ones
for i := 0; i < 1000; i++ {
selectedName := possibleTests[rand.Intn(len(possibleTests))]
test := sequenceTests[selectedName]
str, expectedArgs := test.Gen()
testString += str
selected = append(selected, SelectedTester{
Name: selectedName,
Args: expectedArgs,
})
if rand.Intn(2) == 0 {
testString += randomString(1 + rand.Intn(25))
}
}
var i int
Parse([]rune(testString), func(cursor *Cursor) {
// If its a non-sequence, skip it
if cursor.SeqType == SequenceTypeNone {
return
}
sequenceTests[selected[i].Name].Check(cursor, selected[i].Args)
i++
})
}
func TestParseColor(t *testing.T) {
testVal1 := []rune("2;255;0;255")
ok, trueColor, r, g, b, off := parseColor(testVal1)
assert.Equal(t, true, ok)
assert.Equal(t, true, trueColor)
assert.Equal(t, byte(255), r)
assert.Equal(t, byte(0), g)
assert.Equal(t, byte(255), b)
assert.Equal(t, len(testVal1), off)
testVal2 := []rune("5;23")
ok, trueColor, r, g, b, off = parseColor(testVal2)
assert.Equal(t, true, ok)
assert.Equal(t, false, trueColor)
assert.Equal(t, byte(23), r)
assert.Equal(t, byte(0), g)
assert.Equal(t, byte(0), b)
assert.Equal(t, len(testVal2), off)
testVal3 := []rune("5;")
ok, trueColor, r, g, b, off = parseColor(testVal3)
assert.Equal(t, true, ok)
assert.Equal(t, false, trueColor)
assert.Equal(t, byte(0), r)
assert.Equal(t, byte(0), g)
assert.Equal(t, byte(0), b)
assert.Equal(t, len(testVal3), off)
testVal4 := []rune("2;1;;1")
ok, trueColor, r, g, b, off = parseColor(testVal4)
assert.Equal(t, true, ok)
assert.Equal(t, true, trueColor)
assert.Equal(t, byte(1), r)
assert.Equal(t, byte(0), g)
assert.Equal(t, byte(1), b)
assert.Equal(t, len(testVal4), off)
}
func BenchmarkParse(b *testing.B) {
var testString string
testString += "AbcDefG"
testString += fmt.Sprintf(termenv.CSI+termenv.EraseDisplaySeq, 20)
testString += "HELLO WORLD"
testString += fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, 1, 29)
testString += fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, 1, 2)
testString += "HELLO WORLD"
testString += termenv.CSI + termenv.ShowCursorSeq
testString += fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, 1, 29)
testString += fmt.Sprintf(termenv.CSI+termenv.CursorBackSeq, 5)
buf := &bytes.Buffer{}
lip := lipgloss.NewRenderer(buf, termenv.WithProfile(termenv.TrueColor))
testString += lip.NewStyle().Bold(true).Foreground(lipgloss.Color("#ff00ff")).Render("Hello World") + "asdasdasdasdasd" + lip.NewStyle().Bold(true).Foreground(lipgloss.Color("10")).Render("Hello World") + lip.NewStyle().Italic(true).Background(lipgloss.Color("#ff00ff")).Render("Hello World")
b.ResetTimer()
for i := 0; i < b.N; i++ {
Parse([]rune(testString), func(cursor *Cursor) {})
}
}

124
examples/chat/main.go Normal file
View File

@ -0,0 +1,124 @@
package main
// A simple program demonstrating the text area component from the Bubbles
// component library.
import (
"fmt"
"github.com/BigJk/crt"
bubbleadapter "github.com/BigJk/crt/bubbletea"
"image/color"
"strings"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
Width = 1000
Height = 600
)
func main() {
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 16.0)
if err != nil {
panic(err)
}
win, _, err := bubbleadapter.Window(Width, Height, fonts, initialModel(), color.Black, tea.WithAltScreen())
if err != nil {
panic(err)
}
if err := win.Run("Simple"); err != nil {
panic(err)
}
}
type (
errMsg error
)
type model struct {
viewport viewport.Model
messages []string
textarea textarea.Model
senderStyle lipgloss.Style
err error
}
func initialModel() model {
ta := textarea.New()
ta.Placeholder = "Send a message..."
ta.Focus()
ta.Prompt = "┃ "
ta.CharLimit = 280
ta.SetWidth(30)
ta.SetHeight(3)
// Remove cursor line styling
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
ta.ShowLineNumbers = false
vp := viewport.New(30, 5)
vp.SetContent(`Welcome to the chat room!
Type a message and press Enter to send.`)
ta.KeyMap.InsertNewline.SetEnabled(false)
return model{
textarea: ta,
messages: []string{},
viewport: vp,
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
err: nil,
}
}
func (m model) Init() tea.Cmd {
return textarea.Blink
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
tiCmd tea.Cmd
vpCmd tea.Cmd
)
m.textarea, tiCmd = m.textarea.Update(msg)
m.viewport, vpCmd = m.viewport.Update(msg)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
fmt.Println(m.textarea.Value())
return m, tea.Quit
case tea.KeyEnter:
m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value())
m.viewport.SetContent(strings.Join(m.messages, "\n"))
m.textarea.Reset()
m.viewport.GotoBottom()
}
// We handle errors just like any other message
case errMsg:
m.err = msg
return m, nil
}
return m, tea.Batch(tiCmd, vpCmd)
}
func (m model) View() string {
return fmt.Sprintf(
"%s\n\n%s",
m.viewport.View(),
m.textarea.View(),
) + "\n\n"
}