From 94eaf252869d6782341c3993d6b6f02964705333 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Sun, 30 Aug 2026 20:06:45 -0400 Subject: [PATCH] text: migrate to tview text package --- go.mod | 2 +- go.sum | 4 +- internal/markdown/renderer.go | 7 +- internal/markdown/renderer_test.go | 4 +- internal/ui/chat/composer.go | 3 +- internal/ui/chat/mentionslist/model.go | 5 +- internal/ui/chat/messages_list.go | 152 +++++++------------------ internal/ui/chat/model.go | 7 +- internal/ui/login/qr/model.go | 33 +++--- 9 files changed, 77 insertions(+), 140 deletions(-) diff --git a/go.mod b/go.mod index 9a96d81..b3bf848 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/andybalholm/brotli v1.2.3 github.com/ayn2op/arikawa/v3 v3.0.0-20260827004545-b90631eb8a21 github.com/ayn2op/ningen/v3 v3.0.1-0.20260827010304-ee3a9ce6c2e4 - github.com/ayn2op/tview v0.0.0-20260829040318-2c40c4a50e56 + github.com/ayn2op/tview v0.0.0-20260831000059-9f61521d46b4 github.com/bogdanfinn/fhttp v0.6.8 github.com/bogdanfinn/tls-client v1.15.1 github.com/gdamore/tcell/v3 v3.4.2 diff --git a/go.sum b/go.sum index ae97536..bbedc70 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260827004545-b90631eb8a21 h1:+6W+QtSmIV8Dr github.com/ayn2op/arikawa/v3 v3.0.0-20260827004545-b90631eb8a21/go.mod h1:C7NGoHHt0MQH/plbA9ly4YUQwG0v+n0XdgXmdEPoRpU= github.com/ayn2op/ningen/v3 v3.0.1-0.20260827010304-ee3a9ce6c2e4 h1:Jl3hKkjANCCGPHTyrxLqy+FVdvdftGY41E5f4/1YrRA= github.com/ayn2op/ningen/v3 v3.0.1-0.20260827010304-ee3a9ce6c2e4/go.mod h1:p+rsTJpVomHc8gyyLY0/Wwzj9qsPKoXgwCg2yIlG04A= -github.com/ayn2op/tview v0.0.0-20260829040318-2c40c4a50e56 h1:0kerJdw1SWXBafEtkRAZbEdPYxWtq+c0N1HnLTcwqsE= -github.com/ayn2op/tview v0.0.0-20260829040318-2c40c4a50e56/go.mod h1:iFzkW7DBltCaeLMR9SvXVUAw/zgKIbgE8JaUU8W8crk= +github.com/ayn2op/tview v0.0.0-20260831000059-9f61521d46b4 h1:mYUivDN9S69PScIxEkQm5ujKuIdhUT522N3FP/FJcMY= +github.com/ayn2op/tview v0.0.0-20260831000059-9f61521d46b4/go.mod h1:iFzkW7DBltCaeLMR9SvXVUAw/zgKIbgE8JaUU8W8crk= github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= diff --git a/internal/markdown/renderer.go b/internal/markdown/renderer.go index e550607..e64d743 100644 --- a/internal/markdown/renderer.go +++ b/internal/markdown/renderer.go @@ -10,6 +10,7 @@ import ( "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/ningen/v3/discordmd" "github.com/ayn2op/tview" + "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" "github.com/yuin/goldmark/ast" ) @@ -27,11 +28,11 @@ func NewRenderer(cfg *config.Config) *Renderer { return &Renderer{cfg: cfg} } -func (r *Renderer) RenderLines(source []byte, node ast.Node, base tcell.Style) []tview.Line { +func (r *Renderer) RenderText(source []byte, node ast.Node, base tcell.Style) text.Text { r.listIx = nil r.listNested = 0 - builder := tview.NewLineBuilder() + builder := new(text.Builder) styleStack := []tcell.Style{base} linkDepth := 0 @@ -144,7 +145,7 @@ func (r *Renderer) RenderLines(source []byte, node ast.Node, base tcell.Style) [ return builder.Finish() } -func (r *Renderer) renderFencedCodeBlock(builder *tview.LineBuilder, source []byte, node *ast.FencedCodeBlock, base tcell.Style) { +func (r *Renderer) renderFencedCodeBlock(builder *text.Builder, source []byte, node *ast.FencedCodeBlock, base tcell.Style) { var code strings.Builder lines := node.Lines() for i := range lines.Len() { diff --git a/internal/markdown/renderer_test.go b/internal/markdown/renderer_test.go index cc49398..c357ae1 100644 --- a/internal/markdown/renderer_test.go +++ b/internal/markdown/renderer_test.go @@ -25,9 +25,9 @@ func TestRendererMasksSpoilers(t *testing.T) { {true, "before [spoiler] after"}, {false, "before secret after"}, } { - lines := NewRenderer(&config.Config{Markdown: config.MarkdownConfig{MaskSpoilers: test.mask}}).RenderLines(source, node, tcell.StyleDefault) + rendered := NewRenderer(&config.Config{Markdown: config.MarkdownConfig{MaskSpoilers: test.mask}}).RenderText(source, node, tcell.StyleDefault) var got strings.Builder - for _, line := range lines { + for _, line := range rendered { for _, segment := range line { got.WriteString(segment.Text) } diff --git a/internal/ui/chat/composer.go b/internal/ui/chat/composer.go index 95d090c..ab9e367 100644 --- a/internal/ui/chat/composer.go +++ b/internal/ui/chat/composer.go @@ -29,6 +29,7 @@ import ( "github.com/ayn2op/tview" "github.com/ayn2op/tview/help" "github.com/ayn2op/tview/keybind" + "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" "github.com/ncruces/zenity" "github.com/sahilm/fuzzy" @@ -75,7 +76,7 @@ func newComposer(cfg *config.Config, chat *Model) *composer { } ui.ConfigureBox(c.Box, &cfg.Theme) c. - SetPlaceholder(tview.NewLine(tview.NewSegment("Select a channel to start chatting", tcell.StyleDefault.Dim(true)))). + SetPlaceholder(text.NewLine(text.NewSegment("Select a channel to start chatting", tcell.StyleDefault.Dim(true)))). SetClipboard( func(s string) { if _, err := clipboard.Write(context.Background(), clipboard.FmtText, []byte(s)); err != nil { diff --git a/internal/ui/chat/mentionslist/model.go b/internal/ui/chat/mentionslist/model.go index 21f7126..474b79f 100644 --- a/internal/ui/chat/mentionslist/model.go +++ b/internal/ui/chat/mentionslist/model.go @@ -5,6 +5,7 @@ import ( "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/tview" "github.com/ayn2op/tview/list" + "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" "github.com/rivo/uniseg" ) @@ -73,13 +74,13 @@ func (m *Model) Rebuild() { } item := m.items[index] style := item.Style - line := tview.NewLine(tview.NewSegment(item.DisplayText, style)) + line := text.NewLine(text.NewSegment(item.DisplayText, style)) return tview.NewTextView(). SetScrollable(false). SetWrap(false). SetWordWrap(false). SetTextStyle(style). - SetLines([]tview.Line{line}) + SetContent(text.Text{line}) }) if len(m.items) == 0 { diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index 141a2a3..f56d406 100644 --- a/internal/ui/chat/messages_list.go +++ b/internal/ui/chat/messages_list.go @@ -31,13 +31,14 @@ import ( "github.com/ayn2op/tview/help" "github.com/ayn2op/tview/keybind" "github.com/ayn2op/tview/list" + "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" "github.com/rivo/uniseg" "github.com/skratchdot/open-golang/open" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" + goldtext "github.com/yuin/goldmark/text" "golang.design/x/clipboard" ) @@ -174,29 +175,29 @@ func (ml *messagesList) buildItem(index int) list.Item { item = tview.NewTextView(). SetWrap(true). SetWordWrap(true). - SetLines(ml.renderMessage(message, ml.cfg.Theme.MessagesList.MessageStyle.Style)) + SetContent(ml.renderMessage(message, ml.cfg.Theme.MessagesList.MessageStyle.Style)) ml.itemByID[message.ID] = item } return item } -func (ml *messagesList) renderMessage(message discord.Message, baseStyle tcell.Style) []tview.Line { - builder := tview.NewLineBuilder() +func (ml *messagesList) renderMessage(message discord.Message, baseStyle tcell.Style) text.Text { + builder := new(text.Builder) ml.writeMessage(builder, message, baseStyle) return builder.Finish() } func (ml *messagesList) buildSeparatorItem(ts discord.Timestamp) *tview.TextView { - builder := tview.NewLineBuilder() + builder := new(text.Builder) ml.drawDateSeparator(builder, ts, ml.cfg.Theme.MessagesList.MessageStyle.Style) return tview.NewTextView(). SetScrollable(false). SetWrap(false). SetWordWrap(false). - SetLines(builder.Finish()) + SetContent(builder.Finish()) } -func (ml *messagesList) drawDateSeparator(builder *tview.LineBuilder, ts discord.Timestamp, baseStyle tcell.Style) { +func (ml *messagesList) drawDateSeparator(builder *text.Builder, ts discord.Timestamp, baseStyle tcell.Style) { date := ts.Time().In(time.Local).Format(ml.cfg.DateSeparator.Format) label := " " + date + " " fillChar := ml.cfg.DateSeparator.Character @@ -304,7 +305,7 @@ func (ml *messagesList) nearestMessageRowIndex(rowIndex int) int { return -1 } -func (ml *messagesList) writeMessage(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) { +func (ml *messagesList) writeMessage(builder *text.Builder, message discord.Message, baseStyle tcell.Style) { if ml.cfg.HideBlockedUsers { isBlocked := ml.chat.state.UserIsBlocked(message.Author.ID) if isBlocked { @@ -335,7 +336,7 @@ func (ml *messagesList) writeMessage(builder *tview.LineBuilder, message discord ml.drawReactions(builder, message.Reactions, baseStyle) } -func (ml *messagesList) drawReactions(builder *tview.LineBuilder, reactions []discord.Reaction, baseStyle tcell.Style) { +func (ml *messagesList) drawReactions(builder *text.Builder, reactions []discord.Reaction, baseStyle tcell.Style) { if len(reactions) == 0 { return } @@ -362,12 +363,12 @@ func (ml *messagesList) formatTimestamp(ts discord.Timestamp) string { return ts.Time().In(time.Local).Format(ml.cfg.Timestamps.Format) } -func (ml *messagesList) drawTimestamps(builder *tview.LineBuilder, ts discord.Timestamp, baseStyle tcell.Style) { +func (ml *messagesList) drawTimestamps(builder *text.Builder, ts discord.Timestamp, baseStyle tcell.Style) { dimStyle := baseStyle.Dim(true) builder.Write(ml.formatTimestamp(ts)+" ", dimStyle) } -func (ml *messagesList) drawAuthor(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) { +func (ml *messagesList) drawAuthor(builder *text.Builder, message discord.Message, baseStyle tcell.Style) { name := message.Author.DisplayOrUsername() foreground := tcell.ColorDefault @@ -407,9 +408,9 @@ func (ml *messagesList) memberForMessage(message discord.Message) *discord.Membe // together with the source bytes it indexes into, so callers can reuse them // instead of re-parsing the same content (see drawEmbeds). root is nil when // markdown rendering is disabled. -func (ml *messagesList) drawContent(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) (ast.Node, []byte) { - lines, root, source := ml.renderContentLines(message, baseStyle) - if ml.cfg.Markdown.Enabled && builder.HasCurrentLine() { +func (ml *messagesList) drawContent(builder *text.Builder, message discord.Message, baseStyle tcell.Style) (ast.Node, []byte) { + content, root, source := ml.renderContent(message, baseStyle) + if ml.cfg.Markdown.Enabled && !builder.LineEmpty() { startsWithCodeBlock := false if root != nil { if first := root.FirstChild(); first != nil { @@ -420,37 +421,37 @@ func (ml *messagesList) drawContent(builder *tview.LineBuilder, message discord. if startsWithCodeBlock { // Keep code blocks visually separate from "timestamp + author". builder.NewLine() - for len(lines) > 0 && len(lines[0]) == 0 { - lines = lines[1:] + for len(content) > 0 && len(content[0]) == 0 { + content = content[1:] } } else { - for len(lines) > 1 && len(lines[0]) == 0 { - lines = lines[1:] + for len(content) > 1 && len(content[0]) == 0 { + content = content[1:] } } } - builder.AppendLines(lines) + builder.WriteText(content) return root, source } -func (ml *messagesList) renderContentLines(message discord.Message, baseStyle tcell.Style) ([]tview.Line, ast.Node, []byte) { - return ml.renderContentLinesWithMarkdown(message, baseStyle, false) +func (ml *messagesList) renderContent(message discord.Message, baseStyle tcell.Style) (text.Text, ast.Node, []byte) { + return ml.renderContentWithMarkdown(message, baseStyle, false) } -func (ml *messagesList) renderContentLinesWithMarkdown(message discord.Message, baseStyle tcell.Style, forceMarkdown bool) ([]tview.Line, ast.Node, []byte) { +func (ml *messagesList) renderContentWithMarkdown(message discord.Message, baseStyle tcell.Style, forceMarkdown bool) (text.Text, ast.Node, []byte) { // Keep one rendering path for both normal messages and embed fragments so we preserve mention/link parsing behavior consistently across both. if forceMarkdown || ml.cfg.Markdown.Enabled { c := []byte(message.Content) root := discordmd.ParseWithMessage(c, *ml.chat.state.Cabinet, &message, false) - return ml.renderer.RenderLines(c, root, baseStyle), root, c + return ml.renderer.RenderText(c, root, baseStyle), root, c } - b := tview.NewLineBuilder() + b := new(text.Builder) b.Write(message.Content, baseStyle) return b.Finish(), nil, nil } -func (ml *messagesList) drawSnapshotContent(builder *tview.LineBuilder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tcell.Style) { +func (ml *messagesList) drawSnapshotContent(builder *text.Builder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tcell.Style) { // Convert discord.MessageSnapshotMessage to discord.Message with common fields. message := discord.Message{ Type: snapshot.Type, @@ -470,7 +471,7 @@ func (ml *messagesList) drawSnapshotContent(builder *tview.LineBuilder, parent d ml.drawContent(builder, message, baseStyle) } -func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) { +func (ml *messagesList) drawDefaultMessage(builder *text.Builder, message discord.Message, baseStyle tcell.Style) { if ml.cfg.Timestamps.Enabled { ml.drawTimestamps(builder, message.Timestamp, baseStyle) } @@ -498,7 +499,7 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d } } -func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style, contentRoot ast.Node, contentSource []byte) { +func (ml *messagesList) drawEmbeds(builder *text.Builder, message discord.Message, baseStyle tcell.Style, contentRoot ast.Node, contentSource []byte) { if len(message.Embeds) == 0 { return } @@ -531,12 +532,12 @@ func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.M continue } - embedContentLines := make([]tview.Line, 0, len(lines)*2) + embedText := make(text.Text, 0, len(lines)*2) barStyle := defaultBarStyle if embed.Color != discord.NullColor && embed.Color != 0 { barStyle = barStyle.Foreground(tcell.NewHexColor(int32(embed.Color))) } - prefix := tview.NewSegment(prefixText, barStyle) + prefix := text.NewSegment(prefixText, barStyle) builder.NewLine() for _, line := range lines { if strings.TrimSpace(line.Text) == "" { @@ -546,98 +547,29 @@ func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.M msg.Content = line.Text lineStyle := lineStyles[line.Kind] // Embed descriptions are always markdown-rendered to match Discord's rich embed semantics, even when message markdown is globally disabled. - rendered, _, _ := ml.renderContentLinesWithMarkdown(msg, lineStyle, line.Kind == embedLineDescription) + rendered, _, _ := ml.renderContentWithMarkdown(msg, lineStyle, line.Kind == embedLineDescription) for _, renderedLine := range rendered { if line.URL != "" { renderedLine = lineWithURL(renderedLine, line.URL) } // Prefix must be applied after wrapping so every visual line keeps the embed bar marker ("▎"), not only the first logical line. - for _, wrapped := range wrapStyledLine(renderedLine, wrapWidth) { - prefixed := make(tview.Line, 0, len(wrapped)+1) + for _, wrapped := range text.Wrap(renderedLine, wrapWidth) { + prefixed := make(text.Line, 0, len(wrapped)+1) prefixed = append(prefixed, prefix) prefixed = append(prefixed, wrapped...) - embedContentLines = append(embedContentLines, prefixed) + embedText = append(embedText, prefixed) } } } - if len(embedContentLines) > 0 { - builder.AppendLines(embedContentLines) + if len(embedText) > 0 { + builder.WriteText(embedText) } } } -func wrapStyledLine(line tview.Line, width int) []tview.Line { - if width <= 0 { - return []tview.Line{line} - } - if len(line) == 0 { - return []tview.Line{line} - } - - lines := make([]tview.Line, 0, 2) - current := make(tview.Line, 0, len(line)) - currentWidth := 0 - - pushSegment := func(text string, style tcell.Style) { - if text == "" { - return - } - if n := len(current); n > 0 && current[n-1].Style == style { - current[n-1].Text += text - return - } - current = append(current, tview.Segment{Text: text, Style: style}) - } - - flush := func() { - lineCopy := make(tview.Line, len(current)) - copy(lineCopy, current) - lines = append(lines, lineCopy) - current = current[:0] - currentWidth = 0 - } - - for _, segment := range line { - state := -1 - rest := segment.Text - for len(rest) > 0 { - cluster, nextRest, boundaries, nextState := uniseg.StepString(rest, state) - state = nextState - rest = nextRest - if cluster == "" { - continue - } - - // Use grapheme width (not rune count) so wrapping stays correct with wide glyphs, emoji, and combining characters. - clusterWidth := graphemeClusterWidth(boundaries) - if currentWidth > 0 && currentWidth+clusterWidth > width { - flush() - } - pushSegment(cluster, segment.Style) - currentWidth += clusterWidth - - if currentWidth >= width { - flush() - } - } - } - - if len(current) > 0 { - flush() - } - if len(lines) == 0 { - return []tview.Line{{}} - } - return lines -} - -func graphemeClusterWidth(boundaries int) int { - return boundaries >> uniseg.ShiftWidth -} - -func lineWithURL(line tview.Line, rawURL string) tview.Line { - out := make(tview.Line, len(line)) +func lineWithURL(line text.Line, rawURL string) text.Line { + out := make(text.Line, len(line)) for i, segment := range line { out[i] = segment out[i].Style = out[i].Style.Url(rawURL) @@ -801,7 +733,7 @@ func isMarkdownEscapable(c byte) bool { } } -func (ml *messagesList) drawForwardedMessage(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) { +func (ml *messagesList) drawForwardedMessage(builder *text.Builder, message discord.Message, baseStyle tcell.Style) { dimStyle := baseStyle.Dim(true) ml.drawTimestamps(builder, message.Timestamp, baseStyle) ml.drawAuthor(builder, message, baseStyle) @@ -810,7 +742,7 @@ func (ml *messagesList) drawForwardedMessage(builder *tview.LineBuilder, message builder.Write(" ("+ml.formatTimestamp(message.MessageSnapshots[0].Message.Timestamp)+") ", dimStyle) } -func (ml *messagesList) drawReplyMessage(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) { +func (ml *messagesList) drawReplyMessage(builder *text.Builder, message discord.Message, baseStyle tcell.Style) { dimStyle := baseStyle.Dim(true) // indicator builder.Write(ml.cfg.Theme.MessagesList.ReplyIndicator+" ", dimStyle) @@ -828,7 +760,7 @@ func (ml *messagesList) drawReplyMessage(builder *tview.LineBuilder, message dis ml.drawDefaultMessage(builder, message, baseStyle) } -func (ml *messagesList) drawPinnedMessage(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) { +func (ml *messagesList) drawPinnedMessage(builder *text.Builder, message discord.Message, baseStyle tcell.Style) { builder.Write(message.Author.DisplayOrUsername(), baseStyle) builder.Write(" pinned a message.", baseStyle) } @@ -1090,7 +1022,7 @@ func extractURLs(content string) []string { node := parser.NewParser( parser.WithBlockParsers(discordmd.BlockParsers()...), parser.WithInlineParsers(discordmd.InlineParserWithLink()...), - ).Parse(text.NewReader(src)) + ).Parse(goldtext.NewReader(src)) return urlsFromAST(node, src) } diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index 485b2dd..f243ab7 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -26,6 +26,7 @@ import ( "github.com/ayn2op/tview/flex" "github.com/ayn2op/tview/keybind" "github.com/ayn2op/tview/layers" + "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" ) @@ -341,13 +342,13 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { hasNoPerm := !isDM && !m.state.HasPermissions(msg.Channel.ID, discord.PermissionSendMessages) m.composer.SetDisabled(hasNoPerm) - text := "Message..." + placeholder := "Message..." if hasNoPerm { - text = "You do not have permission to send messages in this channel." + placeholder = "You do not have permission to send messages in this channel." } else if m.cfg.AutoFocus { m.focusComposer() } - m.composer.SetPlaceholder(tview.NewLine(tview.NewSegment(text, tcell.StyleDefault.Dim(true)))) + m.composer.SetPlaceholder(text.NewLine(text.NewSegment(placeholder, tcell.StyleDefault.Dim(true)))) if msg.Channel.GuildID.IsValid() { return m.messagesList.requestGuildMembers(msg.Channel.GuildID, msg.Messages) } diff --git a/internal/ui/login/qr/model.go b/internal/ui/login/qr/model.go index 2df0983..c4ead3b 100644 --- a/internal/ui/login/qr/model.go +++ b/internal/ui/login/qr/model.go @@ -7,6 +7,7 @@ import ( "github.com/ayn2op/tview" "github.com/ayn2op/tview/tabs" + "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" "github.com/gorilla/websocket" "github.com/skip2/go-qrcode" @@ -137,28 +138,28 @@ func (m *Model) setStatus(status string) { } func (m *Model) render() { - var text strings.Builder + var out strings.Builder if m.qrCode != nil { bitmap := m.qrCode.Bitmap() for y := 0; y < len(bitmap); y += 2 { for x := range bitmap[y] { top := bitmap[y][x] bottom := y+1 < len(bitmap) && bitmap[y+1][x] - text.WriteRune(halfBlock(top, bottom)) + out.WriteRune(halfBlock(top, bottom)) } - text.WriteByte('\n') + out.WriteByte('\n') } } if m.status != "" { - if text.Len() > 0 { - text.WriteByte('\n') + if out.Len() > 0 { + out.WriteByte('\n') } - text.WriteString(m.status) + out.WriteString(m.status) } - builder := tview.NewLineBuilder() - builder.Write(text.String(), tcell.StyleDefault) - m.SetLines(m.centerLines(builder.Finish())) + builder := new(text.Builder) + builder.Write(out.String(), tcell.StyleDefault) + m.SetContent(m.centerText(builder.Finish())) m.TextView.SetRect(m.Rect()) } @@ -166,23 +167,23 @@ func (m *Model) View(screen tcell.Screen) { m.TextView.View(screen) } -func (m *Model) centerLines(lines []tview.Line) []tview.Line { +func (m *Model) centerText(content text.Text) text.Text { _, _, _, height := m.InnerRect() if height == 0 { height = 40 } - padding := (height - len(lines)) / 2 + padding := (height - len(content)) / 2 if padding < 0 { padding = 0 - } else if padding < 1 && height > len(lines) { + } else if padding < 1 && height > len(content) { padding = 1 } if padding == 0 { - return lines + return content } - centered := make([]tview.Line, 0, padding+len(lines)) - centered = append(centered, make([]tview.Line, padding)...) - centered = append(centered, lines...) + centered := make(text.Text, 0, padding+len(content)) + centered = append(centered, make(text.Text, padding)...) + centered = append(centered, content...) return centered }