refactor: move to the tview text widget and go-uucode
This commit is contained in:
parent
03d1ef13d2
commit
805621bc2e
17 changed files with 190 additions and 160 deletions
8
go.mod
8
go.mod
|
|
@ -16,7 +16,7 @@ require (
|
|||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/ayn2op/arikawa/v3 v3.0.0-20261016043052-54a68a54e42e
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426
|
||||
github.com/ayn2op/tview v0.0.0-20261015233535-ab884c65f4fe
|
||||
github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f
|
||||
github.com/bogdanfinn/fhttp v0.6.9
|
||||
github.com/bogdanfinn/tls-client v1.16.0
|
||||
github.com/gdamore/tcell/v3 v3.6.0
|
||||
|
|
@ -24,7 +24,7 @@ require (
|
|||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-shellwords v1.0.16
|
||||
github.com/ncruces/zenity v0.10.15
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/rockorager/go-uucode v1.2.2
|
||||
github.com/sahilm/fuzzy v0.1.3
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
|
|
@ -50,7 +50,7 @@ require (
|
|||
github.com/danieljoos/wincred v1.2.3 // indirect
|
||||
github.com/dchest/jsmin v1.0.0 // indirect
|
||||
github.com/disgoorg/godave v0.3.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.9.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.9.1 // indirect
|
||||
github.com/ebitengine/purego v0.11.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.4 // indirect
|
||||
|
|
@ -62,7 +62,7 @@ require (
|
|||
github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7 // indirect
|
||||
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
|
||||
github.com/thomas-vilte/mls-go v1.6.0 // indirect
|
||||
github.com/tphakala/simd v1.12.0 // indirect
|
||||
github.com/tphakala/simd v1.13.0 // indirect
|
||||
github.com/twmb/murmur3 v1.2.1 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.design/x/x11 v0.2.0 // indirect
|
||||
|
|
|
|||
16
go.sum
16
go.sum
|
|
@ -16,8 +16,8 @@ github.com/ayn2op/fhttp v0.6.10-0.20261016015330-2d5e34c6b935 h1:oVxkY1/nVsVQ8Cn
|
|||
github.com/ayn2op/fhttp v0.6.10-0.20261016015330-2d5e34c6b935/go.mod h1:7phoj3f/q54WuiTnCTlAhEoaVcdlruKkSzCSD47LzHk=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426 h1:0ndRWpAiSSgR0z9b7K+pwMlCHEk3VzQeS5ls+zWtbr0=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426/go.mod h1:4S2B9frwrjELeAldoqPgasA/VfnOySgRlektwYLuVks=
|
||||
github.com/ayn2op/tview v0.0.0-20261015233535-ab884c65f4fe h1:XBuBkzSmoomT6agxYt2d5+1WNF4T+AFuI52lZsrD8/I=
|
||||
github.com/ayn2op/tview v0.0.0-20261015233535-ab884c65f4fe/go.mod h1:3aWH6S5uOqOkZv7JFePEVHukTj9hPb6Lbynm59Sks2E=
|
||||
github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f h1:f3Kiqss18xrNTzyB5MfzVTGfrX1tUwMPENmku8BdNGg=
|
||||
github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f/go.mod h1:kn8Up2Tk2vhDUtW6H9nqAvRk9hDVyCzk18CCAo2qkjA=
|
||||
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=
|
||||
|
|
@ -42,8 +42,8 @@ github.com/dchest/jsmin v1.0.0 h1:Y2hWXmGZiRxtl+VcTksyucgTlYxnhPzTozCwx9gy9zI=
|
|||
github.com/dchest/jsmin v1.0.0/go.mod h1:AVBIund7Mr7lKXT70hKT2YgL3XEXUaUk5iw9DZ8b0Uc=
|
||||
github.com/disgoorg/godave v0.3.0 h1:37F3ZuiMd8/EXeoCtTeE8iP2eT2nWsfEa4/ITwoKfe4=
|
||||
github.com/disgoorg/godave v0.3.0/go.mod h1:OreAC3hpabr39bMVA+jwOVDq1EUPXH5A0XUBiZaDI1Y=
|
||||
github.com/dlclark/regexp2/v2 v2.9.0 h1:kKj+BlRuUhZzJk8B3yOBxWKxDFnx2Ilq4e7KNdq/Mxk=
|
||||
github.com/dlclark/regexp2/v2 v2.9.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dlclark/regexp2/v2 v2.9.1 h1:q6bZW/NN9VodqMjYXpuic2lZxdHPfFTzGS+MJxIJmKQ=
|
||||
github.com/dlclark/regexp2/v2 v2.9.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
|
||||
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
|
|
@ -76,8 +76,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
|||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7 h1:m1yKMZwDSXkT5o2MKhd6ihdzb2dYb6eElNE04xjOSEY=
|
||||
github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rockorager/go-uucode v1.2.2 h1:bqVVrZtdwb8v4OB5BIw/aSAKXvGMbOjQwDTHsRC7XhU=
|
||||
github.com/rockorager/go-uucode v1.2.2/go.mod h1:0BZXIGRvWIHt1ruBeViNGcuKyQ3+BRHQbgyXkqRCz38=
|
||||
github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU=
|
||||
github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
|
|
@ -96,8 +96,8 @@ github.com/thomas-vilte/mls-go v1.6.0 h1:HGxrRe0kRfEAXu7MtTemNzqGmC/758rfGkarhYG
|
|||
github.com/thomas-vilte/mls-go v1.6.0/go.mod h1:Cxl407ecAW94wwYLOycpQUp0UyfBk6dNmvs1hkw2yLg=
|
||||
github.com/tphakala/go-opus v1.2.1 h1:j3DlcDf+9VTUzOEZ6kODoZoLMb/MTN2+ft24VuO8FaQ=
|
||||
github.com/tphakala/go-opus v1.2.1/go.mod h1:CzYWGAXjMJHIqu/aTowxgmut2D0+OFJCjQ9+YJHDJV0=
|
||||
github.com/tphakala/simd v1.12.0 h1:qjCO4f/ega4+2znt8bp+lb6R8a7bSri+/KpH+cGHOrA=
|
||||
github.com/tphakala/simd v1.12.0/go.mod h1:eO53GkWg40mRbr2tzs0Xb1IMZQ5U8cMR+Aa1RlXUinE=
|
||||
github.com/tphakala/simd v1.13.0 h1:toN0PH/OIyPlaSaaQQ9Z/IwjVFrrAq2iJsi0Lbtlm2Q=
|
||||
github.com/tphakala/simd v1.13.0/go.mod h1:eO53GkWg40mRbr2tzs0Xb1IMZQ5U8cMR+Aa1RlXUinE=
|
||||
github.com/twmb/murmur3 v1.2.1 h1:TJzZGygOc1VJ7jiBpy8m7KAAzm67zy1jvkUxxEjJp+g=
|
||||
github.com/twmb/murmur3 v1.2.1/go.mod h1:LVqs/avIy0flspVtmBd4VSbogDepFl6Wkj0i0ToccHE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
|
|
|
|||
46
internal/markdown/builder.go
Normal file
46
internal/markdown/builder.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/text"
|
||||
)
|
||||
|
||||
// Builder builds the spans of a text.
|
||||
type Builder struct {
|
||||
spans []text.Span
|
||||
}
|
||||
|
||||
// Write appends value in style.
|
||||
func (b *Builder) Write(value string, style tview.Style) {
|
||||
if value != "" {
|
||||
b.spans = append(b.spans, text.Span{Text: value, Style: style})
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSpans appends spans.
|
||||
func (b *Builder) WriteSpans(spans ...text.Span) {
|
||||
b.spans = append(b.spans, spans...)
|
||||
}
|
||||
|
||||
// LineEmpty reports whether nothing was written since the last line break.
|
||||
func (b *Builder) LineEmpty() bool {
|
||||
return len(b.spans) == 0 || strings.HasSuffix(b.spans[len(b.spans)-1].Text, "\n")
|
||||
}
|
||||
|
||||
// NewLine starts a new line.
|
||||
func (b *Builder) NewLine() {
|
||||
b.Write("\n", tview.Style{})
|
||||
}
|
||||
|
||||
// Finish returns the spans, without a line break at the end, which would add an empty line.
|
||||
func (b *Builder) Finish() []text.Span {
|
||||
if n := len(b.spans); n > 0 {
|
||||
last := &b.spans[n-1]
|
||||
if last.Text = strings.TrimSuffix(last.Text, "\n"); last.Text == "" {
|
||||
b.spans = b.spans[:n-1]
|
||||
}
|
||||
}
|
||||
return b.spans
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
md "github.com/ayn2op/arikawa/v3/markdown"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/richtext"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/yuin/goldmark/v2/ast"
|
||||
)
|
||||
|
|
@ -25,8 +25,8 @@ func NewRenderer(cfg *config.Config) *Renderer {
|
|||
return &Renderer{cfg: cfg}
|
||||
}
|
||||
|
||||
func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style) richtext.Text {
|
||||
builder := new(richtext.Builder)
|
||||
func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style) []text.Span {
|
||||
builder := new(Builder)
|
||||
styleStack := []tview.Style{base}
|
||||
linkDepth := 0
|
||||
listDepth := 0
|
||||
|
|
@ -112,7 +112,7 @@ func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style) ri
|
|||
return builder.Finish()
|
||||
}
|
||||
|
||||
func (r *Renderer) renderCodeBlock(builder *richtext.Builder, source []byte, node *ast.CodeBlock, base tview.Style) {
|
||||
func (r *Renderer) renderCodeBlock(builder *Builder, source []byte, node *ast.CodeBlock, base tview.Style) {
|
||||
var code strings.Builder
|
||||
lines := node.Value.Segments()
|
||||
for _, line := range lines {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,12 @@ func TestRendererRenderText(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
source := []byte(test.source)
|
||||
rendered := NewRenderer(&config.Config{Markdown: config.MarkdownConfig{MaskSpoilers: test.mask}}).RenderText(source, md.Parse(source), tcell.StyleDefault)
|
||||
lines := make([]string, len(rendered))
|
||||
for i, line := range rendered {
|
||||
for _, segment := range line {
|
||||
lines[i] += segment.Text
|
||||
}
|
||||
var got strings.Builder
|
||||
for _, span := range rendered {
|
||||
got.WriteString(span.Text)
|
||||
}
|
||||
if got := strings.Join(lines, "\n"); got != test.want {
|
||||
t.Fatalf("got %q, want %q", got, test.want)
|
||||
if got.String() != test.want {
|
||||
t.Fatalf("got %q, want %q", got.String(), test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/ayn2op/tview/keybind"
|
||||
"github.com/ayn2op/tview/textarea"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/rockorager/go-uucode"
|
||||
"github.com/yuin/goldmark/v2/ast"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
|
@ -556,7 +556,7 @@ func (p mentionsPopup) area(area tview.Rectangle) tview.Rectangle {
|
|||
c := p.c
|
||||
size := c.mentionsList.View().Layout(layout.Limits{Max: layout.Size{Width: area.Width, Height: area.Height}})
|
||||
value, cursor := c.text, c.cursor
|
||||
col := uniseg.StringWidth(value[strings.LastIndex(value[:cursor], "\n")+1 : cursor])
|
||||
col := uucode.StringWidth(value[strings.LastIndex(value[:cursor], "\n")+1 : cursor])
|
||||
x := min(c.box(false).InnerArea(tview.Rectangle{}).X+col, area.Width-size.Width)
|
||||
return tview.Rectangle{X: area.X + x, Y: area.Y + area.Height - size.Height, Width: size.Width, Height: size.Height}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/ayn2op/ningen/v3/states/read"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
"github.com/ayn2op/tview/richtext"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/ayn2op/tview/tree"
|
||||
"github.com/ayn2op/tview/viewport"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
|
|
@ -128,7 +128,7 @@ func (m *Model) rebuild(event *gateway.ReadyEvent) {
|
|||
func (m Model) refreshReadStyles(event *read.UpdateEvent) {
|
||||
if event.GuildID.IsValid() {
|
||||
if node := m.findNodeByReference(event.GuildID); node != nil {
|
||||
setNodeLineStyle(node, m.guildNodeStyle(event.GuildID))
|
||||
setNodeNameStyle(node, m.guildNodeStyle(event.GuildID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +139,10 @@ func (m Model) refreshReadStyles(event *read.UpdateEvent) {
|
|||
channel, err := m.state.Cabinet.Channel(event.ChannelID)
|
||||
if err != nil {
|
||||
indication := m.state.ChannelIsUnread(event.ChannelID, ningen.UnreadOpts{IncludeMutedCategories: true})
|
||||
setNodeLineStyle(node, unreadStyle(indication))
|
||||
setNodeNameStyle(node, unreadStyle(indication))
|
||||
return
|
||||
}
|
||||
setNodeLineStyle(node, m.channelNodeStyle(*channel))
|
||||
setNodeNameStyle(node, m.channelNodeStyle(*channel))
|
||||
}
|
||||
|
||||
func (m Model) updateDMNodeStyle(userID discord.UserID) {
|
||||
|
|
@ -155,7 +155,7 @@ func (m Model) updateDMNodeStyle(userID discord.UserID) {
|
|||
if node == nil || !ok {
|
||||
return
|
||||
}
|
||||
setNodeLineStyle(node, m.channelNodeStyle(*channel))
|
||||
setNodeNameStyle(node, m.channelNodeStyle(*channel))
|
||||
}
|
||||
|
||||
func (m Model) moveDMToFront(channelID discord.ChannelID) {
|
||||
|
|
@ -180,7 +180,7 @@ func (m Model) createFolderNode(folder gateway.GuildFolder, guildsByID map[disco
|
|||
folderNode := tree.NewNode(name).SetExpanded(m.cfg.UI.GuildsTree.AutoExpandFolders)
|
||||
if folder.Color != 0 {
|
||||
folderStyle := tcell.StyleDefault.Foreground(tcell.NewHexColor(int32(folder.Color)))
|
||||
setNodeLineStyle(folderNode, folderStyle)
|
||||
setNodeNameStyle(folderNode, folderStyle)
|
||||
}
|
||||
m.root.AddChild(folderNode)
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ func (m Model) createGuildNode(parent *tree.Node, guild discord.Guild) {
|
|||
SetExpandable(true).
|
||||
SetExpanded(false).
|
||||
SetIndent(m.cfg.Sidebar.Indents.Guild)
|
||||
setNodeLineStyle(guildNode, m.guildNodeStyle(guild.ID))
|
||||
setNodeNameStyle(guildNode, m.guildNodeStyle(guild.ID))
|
||||
parent.AddChild(guildNode)
|
||||
m.nodes[discord.Snowflake(guild.ID)] = guildNode
|
||||
}
|
||||
|
|
@ -230,7 +230,7 @@ func (m Model) createChannelNode(parent *tree.Node, channel discord.Channel) {
|
|||
|
||||
indents := m.cfg.Sidebar.Indents
|
||||
channelNode := tree.NewNode(ui.ChannelToString(channel, m.cfg.Icons, m.state)).SetReference(channel.ID)
|
||||
setNodeLineStyle(channelNode, m.channelNodeStyle(channel))
|
||||
setNodeNameStyle(channelNode, m.channelNodeStyle(channel))
|
||||
switch channel.Type {
|
||||
case discord.DirectMessage:
|
||||
channelNode.SetIndent(indents.DM)
|
||||
|
|
@ -274,13 +274,13 @@ func (m *Model) showVoiceMembers(guildID discord.GuildID) {
|
|||
}
|
||||
|
||||
// After its name, how full it is, if it can be.
|
||||
line := node.Line()[:1]
|
||||
spans := node.Spans()[:1]
|
||||
if channel.VoiceUserLimit > 0 {
|
||||
label := fmt.Sprintf("%d/%d", len(node.Children()), channel.VoiceUserLimit)
|
||||
line = append(line, richtext.NewSegment(" ", tview.Style{}), richtext.NewSegment(label, m.cfg.Theme.GuildsTree.VoiceLimitStyle.Style))
|
||||
spans = append(spans, text.Span{Text: " "}, text.Span{Text: label, Style: m.cfg.Theme.GuildsTree.VoiceLimitStyle.Style})
|
||||
}
|
||||
|
||||
node.SetLine(line)
|
||||
node.SetSpans(spans...)
|
||||
}
|
||||
|
||||
m.styleVoiceMembers()
|
||||
|
|
@ -327,7 +327,7 @@ func (m Model) styleVoiceMembers() {
|
|||
style = m.cfg.Theme.GuildsTree.SpeakingStyle.Style
|
||||
}
|
||||
|
||||
setNodeLineStyle(member, style)
|
||||
setNodeNameStyle(member, style)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -438,11 +438,11 @@ func isVoice(t discord.ChannelType) bool {
|
|||
return t == discord.GuildVoice || t == discord.GuildStageVoice
|
||||
}
|
||||
|
||||
// setNodeLineStyle styles the name of node, which is the first segment of its line.
|
||||
func setNodeLineStyle(node *tree.Node, style tview.Style) {
|
||||
line := node.Line()
|
||||
line[0].Style = style
|
||||
node.SetLine(line)
|
||||
// setNodeNameStyle styles the name of node, which is the first span of its text.
|
||||
func setNodeNameStyle(node *tree.Node, style tview.Style) {
|
||||
spans := node.Spans()
|
||||
spans[0].Style = style
|
||||
node.SetSpans(spans...)
|
||||
}
|
||||
|
||||
func (m Model) createChannelNodes(node *tree.Node, channels []discord.Channel) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
"github.com/ayn2op/tview/richtext"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/ayn2op/tview/tree"
|
||||
"github.com/ayn2op/tview/viewport"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
|
|
@ -251,7 +251,7 @@ func (m Model) addGroup(guildID discord.GuildID, group gateway.GuildMemberListGr
|
|||
|
||||
ref := groupRef(group.ID)
|
||||
node := m.reuse(ref.String()).
|
||||
SetLine(richtext.NewLine(richtext.NewSegment(name, style))).
|
||||
SetSpans(text.Span{Text: name, Style: style}).
|
||||
SetReference(ref).
|
||||
SetExpandable(true)
|
||||
m.root.AddChild(node)
|
||||
|
|
@ -269,21 +269,17 @@ func (m Model) addMember(group *tree.Node, guildID discord.GuildID, member disco
|
|||
if ok {
|
||||
nameStyle = nameStyle.Foreground(tcell.NewHexColor(int32(color)))
|
||||
}
|
||||
line := richtext.NewLine(
|
||||
richtext.NewSegment(statusMarker, tcell.StyleDefault),
|
||||
richtext.NewSegment(" ", tcell.StyleDefault),
|
||||
richtext.NewSegment(name, nameStyle),
|
||||
)
|
||||
m.setStatus(line, m.status(guildID, member.User.ID))
|
||||
group.AddChild(m.reuse(member.User.ID.String()).SetLine(line).SetReference(memberRef{userID: member.User.ID, index: index}))
|
||||
spans := []text.Span{{Text: statusMarker}, {Text: " "}, {Text: name, Style: nameStyle}}
|
||||
m.setStatus(spans, m.status(guildID, member.User.ID))
|
||||
group.AddChild(m.reuse(member.User.ID.String()).SetSpans(spans...).SetReference(memberRef{userID: member.User.ID, index: index}))
|
||||
}
|
||||
|
||||
const statusMarker = "●"
|
||||
|
||||
// setStatus styles the status dot and name of a member line for status.
|
||||
func (m Model) setStatus(line richtext.Line, status discord.Status) {
|
||||
line[0].Style = m.cfg.Theme.MembersTree.StatusStyle(status)
|
||||
line[2].Style = line[2].Style.Dim(status == discord.OfflineStatus)
|
||||
// setStatus styles the status dot and name of a member's spans for status.
|
||||
func (m Model) setStatus(spans []text.Span, status discord.Status) {
|
||||
spans[0].Style = m.cfg.Theme.MembersTree.StatusStyle(status)
|
||||
spans[2].Style = spans[2].Style.Dim(status == discord.OfflineStatus)
|
||||
}
|
||||
|
||||
// reuse takes the node of id out of the index, so that it is reused once, or else returns a new node. Either way the node has no children.
|
||||
|
|
@ -298,9 +294,9 @@ func (m Model) reuse(id string) *tree.Node {
|
|||
|
||||
func (m Model) refreshStatus(userID discord.UserID) {
|
||||
if node, ok := m.nodes[userID.String()]; ok {
|
||||
line := node.Line()
|
||||
m.setStatus(line, m.status(m.channel.GuildID, userID))
|
||||
node.SetLine(line)
|
||||
spans := node.Spans()
|
||||
m.setStatus(spans, m.status(m.channel.GuildID, userID))
|
||||
node.SetSpans(spans...)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,18 +52,18 @@ func setGuildMembers(t *testing.T, m Model, items string) *discord.Channel {
|
|||
func lines(m Model) []string {
|
||||
var lines []string
|
||||
for _, group := range m.root.Children() {
|
||||
lines = append(lines, text(group))
|
||||
lines = append(lines, nodeText(group))
|
||||
for _, member := range group.Children() {
|
||||
lines = append(lines, " "+text(member))
|
||||
lines = append(lines, " "+nodeText(member))
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func text(node *tree.Node) string {
|
||||
func nodeText(node *tree.Node) string {
|
||||
var s string
|
||||
for _, segment := range node.Line() {
|
||||
s += segment.Text
|
||||
for _, span := range node.Spans() {
|
||||
s += span.Text
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/ayn2op/tview/list"
|
||||
"github.com/ayn2op/tview/scrollbar"
|
||||
"github.com/ayn2op/tview/viewport"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/rockorager/go-uucode"
|
||||
)
|
||||
|
||||
// Model shows the mention suggestions as a list in a box.
|
||||
|
|
@ -91,7 +91,7 @@ func (m Model) SelectedInsertText() (string, bool) {
|
|||
func (m Model) maxDisplayWidth() int {
|
||||
width := 0
|
||||
for _, item := range m.items {
|
||||
width = max(width, uniseg.StringWidth(item.DisplayText))
|
||||
width = max(width, uucode.StringWidth(item.DisplayText))
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import (
|
|||
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/markdown"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/richtext"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/rockorager/go-uucode"
|
||||
"github.com/yuin/goldmark/v2/ast"
|
||||
)
|
||||
|
||||
|
|
@ -17,9 +18,9 @@ import (
|
|||
const embedPrefix = " ▎ "
|
||||
|
||||
// embedPrefixWidth is how many cells embedPrefix takes.
|
||||
var embedPrefixWidth = uniseg.StringWidth(embedPrefix)
|
||||
var embedPrefixWidth = uucode.StringWidth(embedPrefix)
|
||||
|
||||
func (m Model) drawEmbeds(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int, contentRoot ast.Node, contentSource []byte) {
|
||||
func (m Model) drawEmbeds(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, width int, contentRoot ast.Node, contentSource []byte) {
|
||||
if len(message.Embeds) == 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -48,12 +49,11 @@ func (m Model) drawEmbeds(builder *richtext.Builder, message discord.Message, ba
|
|||
continue
|
||||
}
|
||||
|
||||
embedText := make(richtext.Text, 0, len(lines)*2)
|
||||
prefixStyle := defaultPrefixStyle
|
||||
if embed.Color != discord.NullColor && embed.Color != 0 {
|
||||
prefixStyle = prefixStyle.Foreground(tcell.NewHexColor(int32(embed.Color)))
|
||||
}
|
||||
prefix := richtext.NewSegment(embedPrefix, prefixStyle)
|
||||
prefix := text.Span{Text: embedPrefix, Style: prefixStyle}
|
||||
builder.NewLine()
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line.text) == "" {
|
||||
|
|
@ -64,27 +64,27 @@ func (m Model) drawEmbeds(builder *richtext.Builder, message discord.Message, ba
|
|||
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, _, _ := m.renderContent(msg, lineStyle, line.kind == embedLineDescription)
|
||||
for _, renderedLine := range rendered {
|
||||
if line.url != "" {
|
||||
renderedLine = lineWithURL(renderedLine, line.url)
|
||||
}
|
||||
// The prefix is added after wrapping, so every visual line has it, not only the first.
|
||||
for _, wrapped := range richtext.Wrap(renderedLine, wrapWidth) {
|
||||
embedText = append(embedText, append(richtext.Line{prefix}, wrapped...))
|
||||
}
|
||||
if line.url != "" {
|
||||
rendered = spansWithURL(rendered, line.url)
|
||||
}
|
||||
}
|
||||
|
||||
if len(embedText) > 0 {
|
||||
builder.WriteText(embedText)
|
||||
// The prefix is added after wrapping, so every visual line has it, not only the first.
|
||||
for _, wrapped := range text.Lines(rendered, wrapWidth, text.WrappingGlyph) {
|
||||
if !builder.LineEmpty() {
|
||||
builder.NewLine()
|
||||
}
|
||||
|
||||
builder.WriteSpans(prefix)
|
||||
builder.WriteSpans(wrapped...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lineWithURL(line richtext.Line, rawURL string) richtext.Line {
|
||||
out := make(richtext.Line, len(line))
|
||||
for i, segment := range line {
|
||||
out[i] = segment
|
||||
func spansWithURL(spans []text.Span, rawURL string) []text.Span {
|
||||
out := make([]text.Span, len(spans))
|
||||
for i, span := range spans {
|
||||
out[i] = span
|
||||
out[i].Style = out[i].Style.Url(rawURL)
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import (
|
|||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
md "github.com/ayn2op/arikawa/v3/markdown"
|
||||
"github.com/ayn2op/arikawa/v3/state"
|
||||
"github.com/ayn2op/discordo/internal/markdown"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/layout"
|
||||
"github.com/ayn2op/tview/richtext"
|
||||
"github.com/ayn2op/tview/textview"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/gdamore/tcell/v3/color"
|
||||
"github.com/yuin/goldmark/v2/ast"
|
||||
|
|
@ -21,15 +21,15 @@ import (
|
|||
|
||||
// messageViews returns the views of the text of message and of what it replies to.
|
||||
// They capture the model and the message, so they are made here: capturing them in buildItem would copy both to the heap on every call, as the list builds every item each time it is laid out.
|
||||
func (m Model) messageViews(message discord.Message, grouped bool) (text, reply *messageView) {
|
||||
func (m Model) messageViews(message discord.Message, grouped bool) (content, reply *messageView) {
|
||||
style := m.cfg.Theme.MessagesList.MessageStyle.Style
|
||||
text = &messageView{render: func(width int) richtext.Text { return m.renderMessage(message, style, width, grouped) }}
|
||||
reply = &messageView{render: func(int) richtext.Text {
|
||||
builder := new(richtext.Builder)
|
||||
content = &messageView{render: func(width int) []text.Span { return m.renderMessage(message, style, width, grouped) }}
|
||||
reply = &messageView{render: func(int) []text.Span {
|
||||
builder := new(markdown.Builder)
|
||||
m.drawReply(builder, message, style)
|
||||
return builder.Finish()
|
||||
}}
|
||||
return text, reply
|
||||
return content, reply
|
||||
}
|
||||
|
||||
func (m Model) blocked(user discord.User) bool {
|
||||
|
|
@ -38,33 +38,32 @@ func (m Model) blocked(user discord.User) bool {
|
|||
|
||||
// messageView is the text of a message. It keeps the wrapped lines until the width changes, as the list measures every message when it draws.
|
||||
type messageView struct {
|
||||
render func(width int) richtext.Text
|
||||
render func(width int) []text.Span
|
||||
width int
|
||||
rendered bool
|
||||
lines richtext.Text
|
||||
lines [][]text.Span
|
||||
}
|
||||
|
||||
func (v *messageView) at(width int) textview.Widget {
|
||||
func (v *messageView) wrap(width int) {
|
||||
if !v.rendered || v.width != width {
|
||||
v.lines, v.width, v.rendered = nil, width, true
|
||||
for _, line := range v.render(width) {
|
||||
v.lines = append(v.lines, richtext.WrapWords(line, width)...)
|
||||
}
|
||||
v.lines, v.width, v.rendered = text.Lines(v.render(width), width, text.WrappingWordOrGlyph), width, true
|
||||
}
|
||||
return textview.New(v.lines).Wrap(false)
|
||||
}
|
||||
|
||||
func (v *messageView) Size() (width, height layout.Length) { return layout.Fill, layout.Shrink }
|
||||
|
||||
func (v *messageView) Layout(limits layout.Limits) layout.Size {
|
||||
return layout.Sized(limits, layout.Fill, layout.Shrink, func(limits layout.Limits) layout.Size {
|
||||
v.at(limits.Max.Width)
|
||||
v.wrap(limits.Max.Width)
|
||||
return layout.Size{Height: len(v.lines)}
|
||||
})
|
||||
}
|
||||
|
||||
func (v *messageView) Draw(screen tview.Screen, area tview.Rectangle) {
|
||||
v.at(area.Width).Draw(screen, area)
|
||||
v.wrap(area.Width)
|
||||
for i, line := range v.lines[:min(len(v.lines), area.Height)] {
|
||||
text.Rich(line...).Wrapping(text.WrappingNone).Draw(screen, tview.Rectangle{X: area.X, Y: area.Y + i, Width: area.Width, Height: 1})
|
||||
}
|
||||
}
|
||||
|
||||
func (v *messageView) Handle(msg tview.Msg, _ tview.Rectangle) tview.Msg { return msg }
|
||||
|
|
@ -95,18 +94,25 @@ func (d dateSeparator) Draw(screen tview.Screen, area tview.Rectangle) {
|
|||
|
||||
func (dateSeparator) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg }
|
||||
|
||||
func (m Model) renderMessage(message discord.Message, baseStyle tview.Style, width int, grouped bool) richtext.Text {
|
||||
builder := new(richtext.Builder)
|
||||
func (m Model) renderMessage(message discord.Message, baseStyle tview.Style, width int, grouped bool) []text.Span {
|
||||
builder := new(markdown.Builder)
|
||||
m.writeMessage(builder, message, baseStyle, width, grouped)
|
||||
text := builder.Finish()
|
||||
// Without a header or content, the first line is the empty one that attachments and embeds start below.
|
||||
if len(text) > 1 && len(text[0]) == 0 {
|
||||
text = text[1:]
|
||||
}
|
||||
return text
|
||||
// Without a header or content, the message starts with the line break that attachments and embeds start below.
|
||||
return trimLeadingLineBreaks(builder.Finish())
|
||||
}
|
||||
|
||||
func (m Model) writeMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int, grouped bool) {
|
||||
// trimLeadingLineBreaks returns spans without the line breaks they start with.
|
||||
func trimLeadingLineBreaks(spans []text.Span) []text.Span {
|
||||
for len(spans) > 0 {
|
||||
if spans[0].Text = strings.TrimLeft(spans[0].Text, "\n"); spans[0].Text != "" {
|
||||
break
|
||||
}
|
||||
spans = spans[1:]
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
func (m Model) writeMessage(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, width int, grouped bool) {
|
||||
if m.blocked(message.Author) {
|
||||
builder.Write("Blocked message", baseStyle.Foreground(color.Red).Bold(true))
|
||||
return
|
||||
|
|
@ -131,7 +137,7 @@ func (m Model) writeMessage(builder *richtext.Builder, message discord.Message,
|
|||
m.drawReactions(builder, message.Reactions, baseStyle)
|
||||
}
|
||||
|
||||
func (m Model) drawReactions(builder *richtext.Builder, reactions []discord.Reaction, baseStyle tview.Style) {
|
||||
func (m Model) drawReactions(builder *markdown.Builder, reactions []discord.Reaction, baseStyle tview.Style) {
|
||||
if len(reactions) == 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -161,7 +167,7 @@ func (m Model) formatTimestamp(ts discord.Timestamp) string {
|
|||
const editedIndicator = " (edited)"
|
||||
|
||||
// drawLayout draws the header of message, unless it is grouped, and its content.
|
||||
func (m Model) drawLayout(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, grouped bool, content func()) {
|
||||
func (m Model) drawLayout(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, grouped bool, content func()) {
|
||||
if m.cfg.UI.MessagesList.Compact {
|
||||
m.drawCompact(builder, message, baseStyle, grouped, content)
|
||||
return
|
||||
|
|
@ -170,7 +176,7 @@ func (m Model) drawLayout(builder *richtext.Builder, message discord.Message, ba
|
|||
m.drawCozy(builder, message, baseStyle, grouped, content)
|
||||
}
|
||||
|
||||
func (m Model) drawCompact(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, grouped bool, content func()) {
|
||||
func (m Model) drawCompact(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, grouped bool, content func()) {
|
||||
dimStyle := baseStyle.Dim(true)
|
||||
if m.cfg.Timestamps.Enabled {
|
||||
builder.Write(m.formatTimestamp(message.Timestamp)+" ", dimStyle)
|
||||
|
|
@ -191,7 +197,7 @@ func (m Model) drawCompact(builder *richtext.Builder, message discord.Message, b
|
|||
}
|
||||
}
|
||||
|
||||
func (m Model) drawCozy(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, grouped bool, content func()) {
|
||||
func (m Model) drawCozy(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, grouped bool, content func()) {
|
||||
dimStyle := baseStyle.Dim(true)
|
||||
if grouped {
|
||||
if content != nil {
|
||||
|
|
@ -218,7 +224,7 @@ func (m Model) drawCozy(builder *richtext.Builder, message discord.Message, base
|
|||
}
|
||||
}
|
||||
|
||||
func (m Model) drawAuthor(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) {
|
||||
func (m Model) drawAuthor(builder *markdown.Builder, message discord.Message, baseStyle tview.Style) {
|
||||
name := message.Author.DisplayOrUsername()
|
||||
foreground := tcell.ColorDefault
|
||||
|
||||
|
|
@ -256,7 +262,7 @@ func (m Model) memberForMessage(message discord.Message) *discord.Member {
|
|||
|
||||
// drawContent renders the message body and returns the parsed markdown AST 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 (m Model) drawContent(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) (ast.Node, []byte) {
|
||||
func (m Model) drawContent(builder *markdown.Builder, message discord.Message, baseStyle tview.Style) (ast.Node, []byte) {
|
||||
content, root, source := m.renderContent(message, baseStyle, false)
|
||||
if m.cfg.Markdown.Enabled {
|
||||
startsWithCodeBlock := false
|
||||
|
|
@ -266,26 +272,18 @@ func (m Model) drawContent(builder *richtext.Builder, message discord.Message, b
|
|||
}
|
||||
}
|
||||
|
||||
if startsWithCodeBlock {
|
||||
// Keep code blocks visually separate from "timestamp + author".
|
||||
if !builder.LineEmpty() {
|
||||
builder.NewLine()
|
||||
}
|
||||
|
||||
for len(content) > 0 && len(content[0]) == 0 {
|
||||
content = content[1:]
|
||||
}
|
||||
} else {
|
||||
for len(content) > 1 && len(content[0]) == 0 {
|
||||
content = content[1:]
|
||||
}
|
||||
// Keep code blocks visually separate from "timestamp + author".
|
||||
if startsWithCodeBlock && !builder.LineEmpty() {
|
||||
builder.NewLine()
|
||||
}
|
||||
|
||||
content = trimLeadingLineBreaks(content)
|
||||
}
|
||||
builder.WriteText(content)
|
||||
builder.WriteSpans(content...)
|
||||
return root, source
|
||||
}
|
||||
|
||||
func (m Model) renderContent(message discord.Message, baseStyle tview.Style, forceMarkdown bool) (richtext.Text, ast.Node, []byte) {
|
||||
func (m Model) renderContent(message discord.Message, baseStyle tview.Style, forceMarkdown bool) ([]text.Span, 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 || m.cfg.Markdown.Enabled {
|
||||
c := []byte(message.Content)
|
||||
|
|
@ -293,12 +291,12 @@ func (m Model) renderContent(message discord.Message, baseStyle tview.Style, for
|
|||
return m.renderer.RenderText(c, root, baseStyle), root, c
|
||||
}
|
||||
|
||||
b := new(richtext.Builder)
|
||||
b := new(markdown.Builder)
|
||||
b.Write(message.Content, baseStyle)
|
||||
return b.Finish(), nil, nil
|
||||
}
|
||||
|
||||
func (m Model) drawSnapshotContent(builder *richtext.Builder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tview.Style) {
|
||||
func (m Model) drawSnapshotContent(builder *markdown.Builder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tview.Style) {
|
||||
// Convert discord.MessageSnapshotMessage to discord.Message with common fields.
|
||||
message := discord.Message{
|
||||
Type: snapshot.Type,
|
||||
|
|
@ -318,7 +316,7 @@ func (m Model) drawSnapshotContent(builder *richtext.Builder, parent discord.Mes
|
|||
m.drawContent(builder, message, baseStyle)
|
||||
}
|
||||
|
||||
func (m Model) drawDefaultMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int, grouped bool) {
|
||||
func (m Model) drawDefaultMessage(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, width int, grouped bool) {
|
||||
var contentRoot ast.Node
|
||||
var contentSource []byte
|
||||
content := func() { contentRoot, contentSource = m.drawContent(builder, message, baseStyle) }
|
||||
|
|
@ -347,7 +345,7 @@ func (m Model) drawDefaultMessage(builder *richtext.Builder, message discord.Mes
|
|||
}
|
||||
}
|
||||
|
||||
func (m Model) drawForwardedMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, grouped bool) {
|
||||
func (m Model) drawForwardedMessage(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, grouped bool) {
|
||||
dimStyle := baseStyle.Dim(true)
|
||||
m.drawLayout(builder, message, baseStyle, grouped, func() {
|
||||
builder.Write(m.cfg.UI.MessagesList.ForwardedIndicator+" ", dimStyle)
|
||||
|
|
@ -356,7 +354,7 @@ func (m Model) drawForwardedMessage(builder *richtext.Builder, message discord.M
|
|||
})
|
||||
}
|
||||
|
||||
func (m Model) drawReply(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) {
|
||||
func (m Model) drawReply(builder *markdown.Builder, message discord.Message, baseStyle tview.Style) {
|
||||
dimStyle := baseStyle.Dim(true)
|
||||
builder.Write(m.cfg.UI.MessagesList.ReplyIndicator+" ", dimStyle)
|
||||
|
||||
|
|
@ -370,6 +368,6 @@ func (m Model) drawReply(builder *richtext.Builder, message discord.Message, bas
|
|||
}
|
||||
}
|
||||
|
||||
func drawPinnedMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) {
|
||||
func drawPinnedMessage(builder *markdown.Builder, message discord.Message, baseStyle tview.Style) {
|
||||
builder.Write(message.Author.DisplayOrUsername()+" pinned a message.", baseStyle)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,16 +216,12 @@ func TestModelDrawLayout(t *testing.T) {
|
|||
ml := newTestModel(t, cfg)
|
||||
message := discord.Message{Author: discord.User{Username: "a"}, Content: "hi", EditedTimestamp: discord.NowTimestamp()}
|
||||
|
||||
for compact, want := range map[bool]string{false: "a T (edited)\nhi\n", true: "T a hi (edited)\n"} {
|
||||
for compact, want := range map[bool]string{false: "a T (edited)\nhi", true: "T a hi (edited)"} {
|
||||
cfg.UI.MessagesList.Compact = compact
|
||||
|
||||
var got strings.Builder
|
||||
for _, line := range ml.renderMessage(message, tview.Style{}, 80, false) {
|
||||
for _, segment := range line {
|
||||
got.WriteString(segment.Text)
|
||||
}
|
||||
|
||||
got.WriteByte('\n')
|
||||
for _, span := range ml.renderMessage(message, tview.Style{}, 80, false) {
|
||||
got.WriteString(span.Text)
|
||||
}
|
||||
|
||||
if got.String() != want {
|
||||
|
|
@ -273,7 +269,7 @@ func TestModelShouldGroup(t *testing.T) {
|
|||
if !ml.items[newest].grouped || ml.items[ml.messageIndex(-1, 1)].grouped {
|
||||
t.Fatal("only the first message has a header")
|
||||
}
|
||||
if got := ml.renderMessage(ml.items[newest].message, tview.Style{}, 80, true); len(got) != 1 || got[0][0].Text != "hi" {
|
||||
if got := ml.renderMessage(ml.items[newest].message, tview.Style{}, 80, true); len(got) != 1 || got[0].Text != "hi" {
|
||||
t.Fatalf("grouped message = %v, want only its content", got)
|
||||
}
|
||||
second := ml.messageIndex(newest, -1)
|
||||
|
|
|
|||
|
|
@ -80,8 +80,7 @@ func (m Model) voiceMessageView(a discord.Attachment) tview.Widget {
|
|||
return rail[cell], style.Dim(true)
|
||||
})
|
||||
|
||||
width, _ := text.New(symbol).Size()
|
||||
play := button.New().Label(symbol).Width(width).OnClick(playMsg(a))
|
||||
play := button.New().Label(symbol).OnClick(playMsg(a))
|
||||
|
||||
return row.New(play, waveform, text.New(label)).Spacing(1).Height(layout.Fixed(1))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ func (m Model) View() tview.Widget {
|
|||
submit := button.New().
|
||||
Label("Login").
|
||||
Width(layout.Fixed(len("Login") + 4)).
|
||||
Height(layout.Fixed(1)).
|
||||
Focused(m.focus == focusButton).
|
||||
OnClick(submitMsg{})
|
||||
return column.New(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import (
|
|||
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/center"
|
||||
"github.com/ayn2op/tview/richtext"
|
||||
"github.com/ayn2op/tview/textview"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/ayn2op/tview/viewport"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/skip2/go-qrcode"
|
||||
|
|
@ -132,7 +131,7 @@ func halfBlock(top, bottom bool) rune {
|
|||
func (m Model) View() tview.Widget {
|
||||
code := m.code()
|
||||
return center.New(
|
||||
viewport.New(textview.New(code).Wrap(false).Alignment(tview.AlignmentCenter), m.scrollState).
|
||||
viewport.New(text.New(code).Wrapping(text.WrappingNone).AlignX(tview.AlignmentCenter), m.scrollState).
|
||||
Height(layout.Shrink).
|
||||
Focused(true).
|
||||
OnChange(func(s *viewport.ScrollState) tview.Msg { return scrollMsg(*s) }),
|
||||
|
|
@ -140,27 +139,27 @@ func (m Model) View() tview.Widget {
|
|||
}
|
||||
|
||||
// code returns the QR code above the status.
|
||||
func (m Model) code() richtext.Text {
|
||||
func (m Model) code() string {
|
||||
var out strings.Builder
|
||||
if m.qrCode != nil {
|
||||
bitmap := m.qrCode.Bitmap()
|
||||
for y := 0; y < len(bitmap); y += 2 {
|
||||
if y > 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
for x := range bitmap[y] {
|
||||
top := bitmap[y][x]
|
||||
bottom := y+1 < len(bitmap) && bitmap[y+1][x]
|
||||
out.WriteRune(halfBlock(top, bottom))
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
if m.status != "" {
|
||||
if out.Len() > 0 {
|
||||
out.WriteByte('\n')
|
||||
out.WriteString("\n\n")
|
||||
}
|
||||
out.WriteString(m.status)
|
||||
}
|
||||
|
||||
builder := new(richtext.Builder)
|
||||
builder.Write(out.String(), tcell.StyleDefault)
|
||||
return builder.Finish()
|
||||
return out.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ func (m Model) View() tview.Widget {
|
|||
submit := button.New().
|
||||
Label("Login").
|
||||
Width(layout.Fixed(len("Login") + 4)).
|
||||
Height(layout.Fixed(1)).
|
||||
Focused(m.buttonFocused).
|
||||
OnClick(submitMsg{})
|
||||
return column.New(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue