diff --git a/go.mod b/go.mod index c771013..4356a14 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/andybalholm/brotli v1.2.2 github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6 - github.com/ayn2op/tview v0.0.0-20260807213801-ef1b0c0bb239 + github.com/ayn2op/tview v0.0.0-20260808011018-71a6ffd9ad39 github.com/bogdanfinn/fhttp v0.6.8 github.com/bogdanfinn/tls-client v1.15.1 github.com/gdamore/tcell/v3 v3.4.1 diff --git a/go.sum b/go.sum index 2d98d4e..d09299d 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e h1:rsMifRXwxkbha github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e/go.mod h1:gqklpHK7WHYTzpd1ZY6Ho/p9GURBghzBCzRiBYpaUP8= github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6 h1:/khHm6G+GWwXpmwA8bJoaXoUhubqMEUG6spjZsnklIw= github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6/go.mod h1:vqLIvETLkjc2vIgQzVEBqpGNCrgNe7YJqS9TUAJsXRI= -github.com/ayn2op/tview v0.0.0-20260807213801-ef1b0c0bb239 h1:xxtbWv2Rp8DvHu9MSd0ChChR0mRifSQVlzYb3QFIwR4= -github.com/ayn2op/tview v0.0.0-20260807213801-ef1b0c0bb239/go.mod h1:bJZdl/FfspPLEW3nuX6ubt7YbmDoSj7f2IoWH37w1NE= +github.com/ayn2op/tview v0.0.0-20260808011018-71a6ffd9ad39 h1:SOzvhkwbIQtlIkRUUFTBKdnECPuFjUBqptO8Pe7aP9Q= +github.com/ayn2op/tview v0.0.0-20260808011018-71a6ffd9ad39/go.mod h1:bJZdl/FfspPLEW3nuX6ubt7YbmDoSj7f2IoWH37w1NE= 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/ui/chat/composer.go b/internal/ui/chat/composer.go index 8bd6deb..4acd072 100644 --- a/internal/ui/chat/composer.go +++ b/internal/ui/chat/composer.go @@ -86,8 +86,6 @@ func newComposer(cfg *config.Config, chat *Model) *composer { return c } -// forwardToTextArea passes a key event to the embedded TextArea then resizes for the new content. -// SetChangedFunc isn't used: it fires inside replace's defer, before truncateLines/findCursor, so it would observe stale state. func (c *composer) forwardToTextArea(ev *tcell.EventKey) tview.Cmd { cmd := c.TextArea.Update(ev) c.resizeForContent() diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index ac7371a..19bf1c0 100644 --- a/internal/ui/chat/messages_list.go +++ b/internal/ui/chat/messages_list.go @@ -83,7 +83,6 @@ func newMessagesList(cfg *config.Config, chat *Model) *messagesList { ui.ConfigureBox(ml.Box, &cfg.Theme) ml.SetTitle("Messages") ml.SetBuilder(ml.buildItem) - ml.SetChangedFunc(ml.onRowCursorChanged) ml.SetTrackEnd(true) ml.SetSelectedStyle(cfg.Theme.MessagesList.SelectedMessageStyle.Style) ml.SetKeybinds(list.Keybinds{ @@ -887,8 +886,6 @@ func (ml *messagesList) Update(msg tview.Msg) tview.Cmd { case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind): return ml.confirmDelete() } - return ml.Model.Update(msg) - case olderMessagesLoadedMsg: selectedChannel, ok := ml.chat.SelectedChannel() if !ok || selectedChannel.ID != msg.ChannelID { @@ -918,7 +915,9 @@ func (ml *messagesList) Update(msg tview.Msg) tview.Cmd { } return nil } - return ml.Model.Update(msg) + cmd := ml.Model.Update(msg) + ml.onRowCursorChanged(ml.Model.Cursor()) + return cmd } func (ml *messagesList) selectUp() tview.Cmd { diff --git a/internal/ui/login/qr/model.go b/internal/ui/login/qr/model.go index 62696c6..ec6dd21 100644 --- a/internal/ui/login/qr/model.go +++ b/internal/ui/login/qr/model.go @@ -21,7 +21,7 @@ type Model struct { fingerprint string qrCode *qrcode.QRCode - msg string + status string } func NewModel() *Model { @@ -33,7 +33,7 @@ func NewModel() *Model { SetWrap(false). SetTextAlign(tview.AlignmentCenter) - m.msg = "Press Ctrl+N to open QR login" + m.setStatus("Press Ctrl+N to open QR login") return m } @@ -46,18 +46,18 @@ func (m *Model) Label() string { func (m *Model) Update(msg tview.Msg) tview.Cmd { switch msg := msg.(type) { case tview.InitMsg: - m.msg = "Connecting to Remote Auth Gateway..." + m.setStatus("Connecting to Remote Auth Gateway...") return m.connect() case tview.KeyMsg: if msg.Key() == tcell.KeyEsc { - m.msg = "Canceled" + m.setStatus("Canceled") return m.close() } return m.TextView.Update(msg) case connCreateMsg: m.conn = msg.conn - m.msg = "Connected. Handshaking..." + m.setStatus("Connected. Handshaking...") return m.listen() case connCloseMsg: m.conn = nil @@ -76,7 +76,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { return tview.Batch(m.listen(), m.generateQRCode(msg.fingerprint)) case qrCodeMsg: m.qrCode = msg.qrCode - m.msg = "Scan this with the Discord mobile app to log in instantly." + m.setStatus("Scan this with the Discord mobile app to log in instantly.") return m.listen() case pendingTicketMsg: return tview.Batch(m.listen(), m.decryptUserPayload(msg.encryptedUserPayload)) @@ -85,13 +85,13 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { if msg.discriminator != "0" { name += "#" + msg.discriminator } - m.msg = "Check your phone! Logging in as " + name + m.setStatus("Check your phone! Logging in as " + name) return m.listen() case pendingLoginMsg: - m.msg = "Authenticating..." + m.setStatus("Authenticating...") return tview.Batch(m.close(), m.exchangeTicket(msg.ticket)) case cancelMsg: - m.msg = "Login canceled on mobile" + m.setStatus("Login canceled on mobile") return m.close() case heartbeatTickMsg: @@ -101,7 +101,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { return tview.Batch(m.heartbeat(), m.sendHeartbeat()) case errMsg: - m.msg = msg.Error() + m.setStatus(msg.Error()) return m.close() } @@ -123,28 +123,46 @@ func halfBlock(top, bottom bool) rune { } } -func (m *Model) View(screen tcell.Screen) { - var contents []string +func (m *Model) SetRect(x, y, width, height int) { + _, _, _, oldHeight := m.Rect() + m.TextView.SetRect(x, y, width, height) + if oldHeight != height { + m.render() + } +} + +func (m *Model) setStatus(status string) { + m.status = status + m.render() +} + +func (m *Model) render() { + var text strings.Builder if m.qrCode != nil { bitmap := m.qrCode.Bitmap() - var b strings.Builder 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] - b.WriteRune(halfBlock(top, bottom)) + text.WriteRune(halfBlock(top, bottom)) } - b.WriteByte('\n') + text.WriteByte('\n') } - contents = append(contents, b.String()) } - if m.msg != "" { - contents = append(contents, m.msg) + if m.status != "" { + if text.Len() > 0 { + text.WriteByte('\n') + } + text.WriteString(m.status) } builder := tview.NewLineBuilder() - builder.Write(strings.Join(contents, "\n"), tcell.StyleDefault) + builder.Write(text.String(), tcell.StyleDefault) m.SetLines(m.centerLines(builder.Finish())) + m.TextView.SetRect(m.Rect()) +} + +func (m *Model) View(screen tcell.Screen) { m.TextView.View(screen) } diff --git a/internal/ui/login/qr/model_test.go b/internal/ui/login/qr/model_test.go new file mode 100644 index 0000000..c6c6d4d --- /dev/null +++ b/internal/ui/login/qr/model_test.go @@ -0,0 +1,22 @@ +package qr + +import ( + "strings" + "testing" + + "github.com/skip2/go-qrcode" +) + +func TestSetRectRendersContent(t *testing.T) { + m := NewModel() + code, err := qrcode.New("test", qrcode.Low) + if err != nil { + t.Fatal(err) + } + m.qrCode = code + m.SetRect(0, 0, 80, 24) + text := m.Text() + if !strings.Contains(text, m.status) || !strings.ContainsAny(text, "█▀▄") { + t.Fatal("missing content") + } +} diff --git a/internal/ui/root/model.go b/internal/ui/root/model.go index a54a2dc..d72c7f4 100644 --- a/internal/ui/root/model.go +++ b/internal/ui/root/model.go @@ -155,7 +155,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { } return tview.Sequence(innerCmd, tview.Quit()) } - case tview.MouseMsg, tview.PasteMsg, tview.FormSubmitMsg, tview.FormCancelMsg, tview.ButtonExitMsg: + case tview.MouseMsg, tview.PasteMsg, tview.FormSubmitMsg, tview.FormCancelMsg: if m.modalRequest != nil { return m.Layers.Update(msg) }