From 997a77c0338488228b7175035f606a391b1783b9 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Sat, 17 Oct 2026 13:50:18 -0400 Subject: [PATCH] feat(messageslist): show custom emoji images in reactions, wrapped with a separator --- docs/config.md | 14 ++++++ go.mod | 2 +- go.sum | 4 +- internal/config/ui.go | 29 +++++++---- internal/ui/chat/mentionslist/model.go | 2 +- internal/ui/chat/messageslist/message.go | 37 +++++++++----- internal/ui/chat/messageslist/model.go | 34 ++++++++----- internal/ui/chat/messageslist/model_test.go | 46 +++++++++++++++++ internal/ui/chat/messageslist/preview.go | 49 +++++++++++++++++-- internal/ui/chat/messageslist/preview_test.go | 21 ++++++++ internal/ui/chat/state.go | 10 ++-- internal/ui/login/model.go | 2 +- internal/ui/login/password/model.go | 2 +- internal/ui/login/qr/model.go | 4 +- internal/ui/login/token/model.go | 2 +- internal/ui/util.go | 4 +- 16 files changed, 208 insertions(+), 54 deletions(-) diff --git a/docs/config.md b/docs/config.md index 357d6b9..6df789c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -424,6 +424,20 @@ The longest time between two messages by the same author for them to share a hea Default: `"10m0s"` +### ui.messages_list.reactions + +#### ui.messages_list.reactions.separator + +The text between reactions. + +Default: `" • "` + +#### ui.messages_list.reactions.colons_around_name + +Whether the names of custom emoji are shown between colons, as in `:name:`. + +Default: `false` + ## ui.mentions_list The list of mention suggestions above the composer. Its width and height are capped to the available space. diff --git a/go.mod b/go.mod index 404dc61..9cc9c95 100644 --- a/go.mod +++ b/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-20261017014958-0756c1611d0f + github.com/ayn2op/tview v0.0.0-20261017195448-33f4b52a0ef5 github.com/bogdanfinn/fhttp v0.6.9 github.com/bogdanfinn/tls-client v1.16.0 github.com/gdamore/tcell/v3 v3.6.0 diff --git a/go.sum b/go.sum index 3a3ae44..3638462 100644 --- a/go.sum +++ b/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-20261017014958-0756c1611d0f h1:f3Kiqss18xrNTzyB5MfzVTGfrX1tUwMPENmku8BdNGg= -github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f/go.mod h1:kn8Up2Tk2vhDUtW6H9nqAvRk9hDVyCzk18CCAo2qkjA= +github.com/ayn2op/tview v0.0.0-20261017195448-33f4b52a0ef5 h1:IbG2cMS8rH6fEtktBc/k+HDyIt0Dosx+pB4sxtXXpRM= +github.com/ayn2op/tview v0.0.0-20261017195448-33f4b52a0ef5/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= diff --git a/internal/config/ui.go b/internal/config/ui.go index 8cc9249..4355375 100644 --- a/internal/config/ui.go +++ b/internal/config/ui.go @@ -37,10 +37,10 @@ func (e enum[T]) text(value T) ([]byte, error) { var ( boxDrawing = scrollbar.SymbolSet{Track: "│", Thumb: "█", Begin: "▲", End: "▼"} - alignments = enum[tview.Alignment]{ - {"left", tview.AlignmentLeft}, - {"center", tview.AlignmentCenter}, - {"right", tview.AlignmentRight}, + alignments = enum[tview.HorizontalAlignment]{ + {"left", tview.HorizontalAlignmentLeft}, + {"center", tview.HorizontalAlignmentCenter}, + {"right", tview.HorizontalAlignmentRight}, } borderSets = enum[tview.BorderSet]{ {"hidden", tview.BorderSetHidden()}, @@ -67,14 +67,16 @@ var ( } ) -type AlignmentWrapper struct{ tview.Alignment } +type AlignmentWrapper struct{ tview.HorizontalAlignment } func (aw *AlignmentWrapper) UnmarshalText(text []byte) (err error) { - aw.Alignment, err = alignments.parse(string(text)) + aw.HorizontalAlignment, err = alignments.parse(string(text)) return err } -func (aw AlignmentWrapper) MarshalText() ([]byte, error) { return alignments.text(aw.Alignment) } +func (aw AlignmentWrapper) MarshalText() ([]byte, error) { + return alignments.text(aw.HorizontalAlignment) +} type BorderSetWrapper struct{ tview.BorderSet } @@ -181,6 +183,14 @@ type ( Attachments AttachmentsUIConfig `toml:"attachments"` Avatars AvatarsUIConfig `toml:"avatars"` Grouping GroupingUIConfig `toml:"grouping"` + Reactions ReactionsUIConfig `toml:"reactions"` + } + + ReactionsUIConfig struct { + // The text between reactions. + Separator string `toml:"separator"` + // Whether the names of custom emoji are shown between colons, as in `:name:`. + ColonsAroundName bool `toml:"colons_around_name"` } // GroupingUIConfig shows consecutive messages by the same author under one header. @@ -214,7 +224,7 @@ type ( ) func defaultUI() UIConfig { - left := TitleUIConfig{AlignmentWrapper{tview.AlignmentLeft}} + left := TitleUIConfig{AlignmentWrapper{tview.HorizontalAlignmentLeft}} round := BorderSetWrapper{tview.BorderSetRound()} attachments := AttachmentsUIConfig{ ShowLinks: true, @@ -223,7 +233,7 @@ func defaultUI() UIConfig { } return UIConfig{ Tabs: TabsUIConfig{ - Alignment: AlignmentWrapper{tview.AlignmentCenter}, + Alignment: AlignmentWrapper{tview.HorizontalAlignmentCenter}, Separator: "│", Padding: [2]string{" ", " "}, Arrows: [2]string{"◀", "▶"}, @@ -246,6 +256,7 @@ func defaultUI() UIConfig { Attachments: attachments, Avatars: AvatarsUIConfig{Preview: true, Height: 2}, Grouping: GroupingUIConfig{Enabled: true, Interval: 10 * time.Minute}, + Reactions: ReactionsUIConfig{Separator: " • "}, }, MentionsList: MentionsListUIConfig{MinWidth: 20}, } diff --git a/internal/ui/chat/mentionslist/model.go b/internal/ui/chat/mentionslist/model.go index 9779193..13e0bb9 100644 --- a/internal/ui/chat/mentionslist/model.go +++ b/internal/ui/chat/mentionslist/model.go @@ -121,7 +121,7 @@ func (r row) Draw(screen tview.Screen, area tview.Rectangle) { for x := area.X; x < area.X+area.Width; x++ { screen.Put(x, area.Y, " ", r.style) } - tview.Print(screen, r.text, area.X, area.Y, area.Width, tview.AlignmentLeft, r.style) + tview.Print(screen, r.text, area.X, area.Y, area.Width, tview.HorizontalAlignmentLeft, r.style) } func (row) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg } diff --git a/internal/ui/chat/messageslist/message.go b/internal/ui/chat/messageslist/message.go index 3226eb9..97d0d51 100644 --- a/internal/ui/chat/messageslist/message.go +++ b/internal/ui/chat/messageslist/message.go @@ -13,6 +13,7 @@ import ( "github.com/ayn2op/discordo/internal/markdown" "github.com/ayn2op/tview" "github.com/ayn2op/tview/layout" + "github.com/ayn2op/tview/row" "github.com/ayn2op/tview/text" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" @@ -89,7 +90,7 @@ func (d dateSeparator) Draw(screen tview.Screen, area tview.Rectangle) { fill := area.Width - labelWidth line = strings.Repeat(d.fill, fill/2) + label + strings.Repeat(d.fill, fill-fill/2) } - tview.Print(screen, line, area.X, area.Y, area.Width, tview.AlignmentLeft, d.style) + tview.Print(screen, line, area.X, area.Y, area.Width, tview.HorizontalAlignmentLeft, d.style) } func (dateSeparator) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg } @@ -134,30 +135,42 @@ func (m Model) writeMessage(builder *markdown.Builder, message discord.Message, default: m.drawLayout(builder, message, baseStyle, false, nil) } - m.drawReactions(builder, message.Reactions, baseStyle) } -func (m Model) drawReactions(builder *markdown.Builder, reactions []discord.Reaction, baseStyle tview.Style) { +// reactionsView returns the reactions wrapped to the width of the message, with the image of each custom emoji that has one before its name, or nil if there are none. +func (m Model) reactionsView(reactions []discord.Reaction) tview.Widget { if len(reactions) == 0 { - return + return nil } - builder.NewLine() + theme, cfg := m.cfg.Theme.MessagesList, m.cfg.UI.MessagesList.Reactions + separatorStyle := tview.MergeStyle(theme.MessageStyle.Style, theme.ReactionStyle.Style) + chips := make([]tview.Widget, len(reactions)) for i, reaction := range reactions { - if i > 0 { - builder.Write(" ", baseStyle) + style := theme.ReactionStyle.Style + if reaction.Me { + style = theme.OwnReactionStyle.Style } + style = tview.MergeStyle(theme.MessageStyle.Style, style) name := reaction.Emoji.Name - if reaction.Emoji.IsCustom() { + if reaction.Emoji.IsCustom() && cfg.ColonsAroundName { name = ":" + name + ":" } - style := m.cfg.Theme.MessagesList.ReactionStyle.Style - if reaction.Me { - style = m.cfg.Theme.MessagesList.OwnReactionStyle.Style + + spans := []text.Span{{Text: name + " " + strconv.Itoa(reaction.Count), Style: style}} + // Each reaction but the last ends with the separator, so a line never starts with one. + if i < len(reactions)-1 { + spans = append(spans, text.Span{Text: cfg.Separator, Style: separatorStyle}) } - builder.Write(name+" "+strconv.Itoa(reaction.Count), tview.MergeStyle(baseStyle, style)) + + var chip tview.Widget = text.Rich(spans...) + if emoji, ok := m.images[emojiURL(reaction.Emoji)]; ok { + chip = row.New(emoji, chip).Spacing(1).Width(layout.Shrink).Height(layout.Shrink) + } + chips[i] = chip } + return row.New(chips...).Height(layout.Shrink).Wrap() } func (m Model) formatTimestamp(ts discord.Timestamp) string { diff --git a/internal/ui/chat/messageslist/model.go b/internal/ui/chat/messageslist/model.go index f73d1ae..d3c838a 100644 --- a/internal/ui/chat/messageslist/model.go +++ b/internal/ui/chat/messageslist/model.go @@ -47,7 +47,8 @@ type Model struct { items []messageItem renderer *markdown.Renderer - avatars map[discord.URL]tviewimage.Widget + // images are the avatars and emoji, shared by every message that shows them. + images map[discord.URL]tviewimage.Widget // voicePlayer plays the voice message played last, and voiceStatus is what it last said of it. voicePlayer *voice.Player voiceStatus voice.PlayerStatus @@ -69,7 +70,7 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { cfg: cfg, state: state, renderer: markdown.NewRenderer(cfg), - avatars: map[discord.URL]tviewimage.Widget{}, + images: map[discord.URL]tviewimage.Widget{}, voicePlayer: voice.NewPlayer(), } @@ -155,10 +156,14 @@ func (m *Model) UpdateMessage(message discord.Message) tview.Cmd { } // RefreshMessage shows the cached state of a message, such as after a reaction. -func (m *Model) RefreshMessage(channelID discord.ChannelID, id discord.MessageID) { - if message, err := m.state.Cabinet.Message(channelID, id); err == nil { - m.setMessage(m.indexOf(channelID, id), *message) +func (m *Model) RefreshMessage(channelID discord.ChannelID, id discord.MessageID) tview.Cmd { + message, err := m.state.Cabinet.Message(channelID, id) + if err != nil { + return nil } + + m.setMessage(m.indexOf(channelID, id), *message) + return m.loadPreviews() } // DeleteMessage removes the shown message with id. @@ -176,7 +181,7 @@ func (m Model) indexOf(channelID discord.ChannelID, id discord.MessageID) int { func (m *Model) reset() { m.items = nil - clear(m.avatars) + clear(m.images) m.cursor, m.target = -1, -1 m.scrollState = viewport.ScrollState{} m.scrollState.ScrollToEnd() @@ -266,21 +271,26 @@ func (m Model) buildItem(index int) tview.Widget { } } - if avatar, ok := m.avatars[avatarURL(message.Author)]; ok { + if !m.blocked(message.Author) { + body = body.Push(m.reactionsView(message.Reactions)) + } + + var view tview.Widget = body + if avatar, ok := m.images[avatarURL(message.Author)]; ok { var gutter tview.Widget = avatar // A grouped message leaves the avatar's column empty, under the avatar of the group. if item.grouped { width, _ := avatar.Size() gutter = spacer.New().Width(width) } - body = row.New(gutter, body).Spacing(1).Height(layout.Shrink) + view = row.New(gutter, body).Spacing(1).Height(layout.Shrink) } if message.Type == discord.InlinedReplyMessage && !m.blocked(message.Author) { - body = column.New(item.reply, body).Height(layout.Shrink) + view = column.New(item.reply, view).Height(layout.Shrink) } - return body + return view } // shouldGroup reports whether message is drawn under the header of previous, the message before it, which is nil for the first. @@ -447,8 +457,8 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { cmd = preview.Transmit() } - if _, ok := m.avatars[msg.proxy]; ok { - m.avatars[msg.proxy] = preview + if _, ok := m.images[msg.proxy]; ok { + m.images[msg.proxy] = preview return m, cmd } diff --git a/internal/ui/chat/messageslist/model_test.go b/internal/ui/chat/messageslist/model_test.go index 0ebe8e1..ffb6f4c 100644 --- a/internal/ui/chat/messageslist/model_test.go +++ b/internal/ui/chat/messageslist/model_test.go @@ -2,6 +2,7 @@ package messageslist import ( "encoding/json" + "image" "path/filepath" "slices" "strings" @@ -13,8 +14,10 @@ import ( "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/ningen/v3" "github.com/ayn2op/tview" + tviewimage "github.com/ayn2op/tview/image" "github.com/ayn2op/tview/layout" "github.com/gdamore/tcell/v3" + "github.com/gdamore/tcell/v3/vt" ) func testConfig(t *testing.T) *config.Config { @@ -353,3 +356,46 @@ func TestModelScrollToTop(t *testing.T) { t.Fatalf("scrolling to the new top fetched %d times, want once", fetches) } } + +func TestModelReactionsView(t *testing.T) { + ml := newTestModel(t, testConfig(t)) + custom := discord.Emoji{ID: 5, Name: "cat"} + reactions := []discord.Reaction{{Count: 2, Emoji: discord.Emoji{Name: "ok"}}, {Count: 1, Emoji: custom}} + draw := func() string { + screen, err := tcell.NewTerminfoScreenFromTty(vt.NewMockTerm(vt.MockOptSize{X: 20, Y: 1})) + if err != nil { + t.Fatal(err) + } + if err := screen.Init(); err != nil { + t.Fatal(err) + } + defer screen.Fini() + + ml.reactionsView(reactions).Draw(screen, tview.Rectangle{Width: 20, Height: 1}) + var row strings.Builder + for x := range 20 { + str, _, _ := screen.Get(x, 0) + row.WriteString(str) + } + return strings.TrimRight(row.String(), " ") + } + + t.Run("names", func(t *testing.T) { + if got := draw(); got != "ok 2 • cat 1" { + t.Fatalf("reactions = %q", got) + } + }) + t.Run("emoji image", func(t *testing.T) { + ml.images[emojiURL(custom)] = tviewimage.New(image.NewAlpha(image.Rect(0, 0, 2, 2))).Width(2) + // The blank image takes two cells before the name. + if got := draw(); got != "ok 2 • cat 1" { + t.Fatalf("reactions = %q", got) + } + }) + t.Run("with colons", func(t *testing.T) { + ml.cfg.UI.MessagesList.Reactions.ColonsAroundName = true + if got := draw(); got != "ok 2 • :cat: 1" { + t.Fatalf("reactions = %q", got) + } + }) +} diff --git a/internal/ui/chat/messageslist/preview.go b/internal/ui/chat/messageslist/preview.go index 7672e4d..ea4c31b 100644 --- a/internal/ui/chat/messageslist/preview.go +++ b/internal/ui/chat/messageslist/preview.go @@ -2,6 +2,7 @@ package messageslist import ( "image" + "image/draw" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -86,6 +87,10 @@ func (m Model) loadPreviews() tview.Cmd { } cmds = append(cmds, m.loadAvatar(item.message.Author)) + for _, reaction := range item.message.Reactions { + cmds = append(cmds, m.loadEmoji(reaction.Emoji)) + } + if !m.cfg.UI.MessagesList.Attachments.Preview { continue } @@ -116,21 +121,55 @@ func avatarURL(user discord.User) discord.URL { } func (m Model) loadAvatar(user discord.User) tview.Cmd { - url, cfg := avatarURL(user), m.cfg.UI.MessagesList.Avatars - if _, ok := m.avatars[url]; ok || !cfg.Preview || cfg.Height == 0 || m.blocked(user) { + cfg := m.cfg.UI.MessagesList.Avatars + if !cfg.Preview || cfg.Height == 0 || m.blocked(user) { + return nil + } + return m.loadImage(avatarURL(user), uint(cfg.Height)) +} + +func emojiURL(emoji discord.Emoji) discord.URL { + return emoji.EmojiURLWithType(discord.PNGImage) +} + +// loadEmoji loads the image of a custom emoji, a row tall. +func (m Model) loadEmoji(emoji discord.Emoji) tview.Cmd { + if !emoji.IsCustom() { + return nil + } + return m.loadImage(emojiURL(emoji), 1) +} + +// loadImage loads the image at url in a square rows tall, unless it is loaded already. +func (m Model) loadImage(url discord.URL, rows uint) tview.Cmd { + if _, ok := m.images[url]; ok { return nil } cellWidth, cellHeight := m.previewCell() - cols := uint(cfg.Height) * cellHeight / cellWidth + cols := rows * cellHeight / cellWidth bounds := image.Rect(0, 0, int(cols*cellWidth), int(cols*cellWidth)) widget := func(img image.Image) tviewimage.Widget { - return tviewimage.New(img).Width(int(cols)).CellSize(int(cellWidth), int(cellHeight)) + return tviewimage.New(square(img)).Width(int(cols)).CellSize(int(cellWidth), int(cellHeight)) } - m.avatars[url] = widget(image.NewAlpha(bounds)) + m.images[url] = widget(image.NewAlpha(bounds)) return m.loadPreview(0, url, bounds, widget) } +// square returns img centered on a transparent square, so an image of any shape takes the square's cells. +func square(img image.Image) image.Image { + b := img.Bounds() + if b.Dx() == b.Dy() { + return img + } + + side := max(b.Dx(), b.Dy()) + dst := image.NewNRGBA(image.Rect(0, 0, side, side)) + at := image.Pt((side-b.Dx())/2, (side-b.Dy())/2) + draw.Draw(dst, b.Sub(b.Min).Add(at), img, b.Min, draw.Src) + return dst +} + // loadPreview yields an empty preview if loading fails. func (m Model) loadPreview(messageID discord.MessageID, proxy discord.URL, bounds image.Rectangle, widget func(image.Image) tviewimage.Widget) tview.Cmd { return func() tview.Msg { diff --git a/internal/ui/chat/messageslist/preview_test.go b/internal/ui/chat/messageslist/preview_test.go index 9c68420..be927fd 100644 --- a/internal/ui/chat/messageslist/preview_test.go +++ b/internal/ui/chat/messageslist/preview_test.go @@ -1,6 +1,7 @@ package messageslist import ( + "image" "slices" "testing" @@ -56,3 +57,23 @@ func TestPreviewSourceCols(t *testing.T) { }) } } + +func TestSquare(t *testing.T) { + t.Run("square", func(t *testing.T) { + img := image.NewAlpha(image.Rect(0, 0, 20, 20)) + if got := square(img); got != image.Image(img) { + t.Fatal("square image was copied") + } + }) + t.Run("tall", func(t *testing.T) { + img := image.NewAlpha(image.Rect(0, 0, 10, 30)) + img.Pix[0] = 0xff + got := square(img) + if got.Bounds() != image.Rect(0, 0, 30, 30) { + t.Fatalf("bounds = %v, want 30 by 30", got.Bounds()) + } + if _, _, _, a := got.At(10, 0).RGBA(); a == 0 { + t.Fatal("image is not centered") + } + }) +} diff --git a/internal/ui/chat/state.go b/internal/ui/chat/state.go index f27771d..55ae149 100644 --- a/internal/ui/chat/state.go +++ b/internal/ui/chat/state.go @@ -116,15 +116,15 @@ func (m *Model) applyEvent(event gateway.Event) tview.Cmd { case *gateway.MessageDeleteEvent: m.messagesList.DeleteMessage(event.ChannelID, event.ID) case *gateway.MessageReactionAddEvent: - m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) + return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) case *gateway.MessageReactionAddManyEvent: - m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) + return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) case *gateway.MessageReactionRemoveEvent: - m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) + return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) case *gateway.MessageReactionRemoveAllEvent: - m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) + return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) case *gateway.MessageReactionRemoveEmojiEvent: - m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) + return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID) case *gateway.GuildMembersChunkEvent: return m.refreshMemberNames(event) diff --git a/internal/ui/login/model.go b/internal/ui/login/model.go index b92fb23..f2b76c3 100644 --- a/internal/ui/login/model.go +++ b/internal/ui/login/model.go @@ -72,7 +72,7 @@ func (m Model) View() tview.Widget { func (m Model) tabs() tabs.Widget { cfg := m.cfg.UI.Tabs return tabs.New(m.password.Label(), m.qr.Label(), m.token.Label()). - Alignment(cfg.Alignment.Alignment). + Alignment(cfg.Alignment.HorizontalAlignment). Wrap(cfg.Wrap). Separator(cfg.Separator). Padding(cfg.Padding[0], cfg.Padding[1]). diff --git a/internal/ui/login/password/model.go b/internal/ui/login/password/model.go index f14c26e..ca291bb 100644 --- a/internal/ui/login/password/model.go +++ b/internal/ui/login/password/model.go @@ -82,5 +82,5 @@ func (m Model) View() tview.Widget { row.New(column.New(text.New("Login")).Width(labelWidth), login).Height(layout.Shrink), row.New(column.New(text.New("Password")).Width(labelWidth), password).Height(layout.Shrink), submit, - ).Spacing(1).Align(tview.AlignmentCenter) + ).Spacing(1).AlignX(tview.HorizontalAlignmentCenter) } diff --git a/internal/ui/login/qr/model.go b/internal/ui/login/qr/model.go index a683316..303c048 100644 --- a/internal/ui/login/qr/model.go +++ b/internal/ui/login/qr/model.go @@ -2,12 +2,12 @@ package qr import ( "crypto/rsa" - "github.com/ayn2op/tview/layout" "strings" "time" "github.com/ayn2op/tview" "github.com/ayn2op/tview/center" + "github.com/ayn2op/tview/layout" "github.com/ayn2op/tview/text" "github.com/ayn2op/tview/viewport" "github.com/gdamore/tcell/v3" @@ -131,7 +131,7 @@ func halfBlock(top, bottom bool) rune { func (m Model) View() tview.Widget { code := m.code() return center.New( - viewport.New(text.New(code).Wrapping(text.WrappingNone).AlignX(tview.AlignmentCenter), m.scrollState). + viewport.New(text.New(code).Wrapping(text.WrappingNone).AlignX(tview.HorizontalAlignmentCenter), m.scrollState). Height(layout.Shrink). Focused(true). OnChange(func(s *viewport.ScrollState) tview.Msg { return scrollMsg(*s) }), diff --git a/internal/ui/login/token/model.go b/internal/ui/login/token/model.go index dafefdb..e215d4b 100644 --- a/internal/ui/login/token/model.go +++ b/internal/ui/login/token/model.go @@ -62,5 +62,5 @@ func (m Model) View() tview.Widget { return column.New( row.New(column.New(text.New("Token")).Width(layout.Fixed(len("Token")+1)), input).Height(layout.Shrink), submit, - ).Spacing(1).Align(tview.AlignmentCenter) + ).Spacing(1).AlignX(tview.HorizontalAlignmentCenter) } diff --git a/internal/ui/util.go b/internal/ui/util.go index a293d2d..ceff92e 100644 --- a/internal/ui/util.go +++ b/internal/ui/util.go @@ -18,8 +18,8 @@ func Box(child tview.Widget, cfg *config.Config, focused bool) box.Widget { padding := ui.Border.Padding b := box.New(child). Padding(padding[0], padding[1], padding[2], padding[3]). - TitleAlignment(ui.Title.Alignment.Alignment). - FooterAlignment(ui.Footer.Alignment.Alignment) + TitleAlignment(ui.Title.Alignment.HorizontalAlignment). + FooterAlignment(ui.Footer.Alignment.HorizontalAlignment) if ui.Border.Enabled { b = b.Borders(tview.BordersAll) }