feat(messageslist): load previews at the terminal's cell size
This commit is contained in:
parent
d1867ee14c
commit
c947abdf0e
8 changed files with 57 additions and 27 deletions
2
go.mod
2
go.mod
|
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/andybalholm/brotli v1.2.6
|
||||
github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e
|
||||
github.com/ayn2op/tview v0.0.0-20261004194222-a506691173ce
|
||||
github.com/ayn2op/tview v0.0.0-20261004231500-f79adb05b560
|
||||
github.com/bogdanfinn/fhttp v0.6.9
|
||||
github.com/bogdanfinn/tls-client v1.16.0
|
||||
github.com/gdamore/tcell/v3 v3.5.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c h1:Qkl3W5RXzUQUR
|
|||
github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c/go.mod h1:SHqcr+EmZKeNtVKZybHJn4HNz+ufA1XbeKNSTigBCEE=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e h1:QAei/IVEyZV7GcpkpcFUh37+QRqwrt57z7OT3ciHrhI=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e/go.mod h1:RsvYiMzi4Si1F7vmml5NCL2DaKIZXkNUQC7QlGTMtCw=
|
||||
github.com/ayn2op/tview v0.0.0-20261004194222-a506691173ce h1:QbE9slkt2GwIw9zmE8ubmn+mejNsvfFaUp/4IOB5eBY=
|
||||
github.com/ayn2op/tview v0.0.0-20261004194222-a506691173ce/go.mod h1:oRPTSZgaEJPhv3dmmNvo6grBq/BemrLG3wFQLMTZJkY=
|
||||
github.com/ayn2op/tview v0.0.0-20261004231500-f79adb05b560 h1:joRW9LhEPGkbhKgs8tUMgmoXPzH8v+5GQSe6geS0crg=
|
||||
github.com/ayn2op/tview v0.0.0-20261004231500-f79adb05b560/go.mod h1:sMf5NqBlGJjsaGCUycTqTyNG5vewJU6jq/3BdJIzbCU=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ type Model struct {
|
|||
// kittyImages counts the images sent to the terminal.
|
||||
kitty bool
|
||||
kittyImages int
|
||||
// cellWidth and cellHeight are the size of a terminal cell in pixels, or zero if the terminal does not report it.
|
||||
cellWidth, cellHeight uint
|
||||
}
|
||||
|
||||
var _ help.KeyMap = Model{}
|
||||
|
|
@ -924,6 +926,12 @@ func (ml *Model) update(msg tview.Msg) tview.Cmd {
|
|||
// Invalidate the view so it is laid out with the preview.
|
||||
ml.items[index].view = nil
|
||||
return cmd
|
||||
case tview.ResizeMsg:
|
||||
cols, rows := msg.Size()
|
||||
width, height := msg.PixelSize()
|
||||
if cols > 0 && rows > 0 {
|
||||
ml.cellWidth, ml.cellHeight = uint(width/cols), uint(height/rows)
|
||||
}
|
||||
case tview.TerminalInfoMsg:
|
||||
switch ml.cfg.Attachments.Protocol {
|
||||
case "kitty":
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const (
|
|||
previewHeight = 20
|
||||
)
|
||||
|
||||
// kittyPixels is the number of pixels loaded per column of a kitty preview, roughly the width of a cell.
|
||||
// kittyPixels is the width of a cell in pixels assumed for kitty previews when the terminal does not report it.
|
||||
const kittyPixels = 10
|
||||
|
||||
type previewLoadedMsg struct {
|
||||
|
|
@ -57,19 +57,27 @@ func previewSources(message discord.Message) []previewSource {
|
|||
return slices.DeleteFunc(sources, func(s previewSource) bool { return s.width == 0 || s.height == 0 })
|
||||
}
|
||||
|
||||
func (s previewSource) cols() int {
|
||||
// A cell is about twice as tall as wide.
|
||||
return int(max(min(s.width, previewWidth, s.width*previewHeight*2/s.height), 1))
|
||||
// cols returns the preview's width in cells of cellWidth by cellHeight pixels: its own width at most, within the preview box.
|
||||
func (s previewSource) cols(cellWidth, cellHeight uint) int {
|
||||
return int(max(min((s.width+cellWidth-1)/cellWidth, previewWidth, s.width*previewHeight*cellHeight/(s.height*cellWidth)), 1))
|
||||
}
|
||||
|
||||
// previewCell returns the size of a cell in image pixels: the terminal's for kitty, and one pixel by two for half blocks.
|
||||
func (ml *Model) previewCell() (width, height uint) {
|
||||
switch {
|
||||
case !ml.kitty:
|
||||
return 1, 2
|
||||
case ml.cellWidth == 0 || ml.cellHeight == 0:
|
||||
return kittyPixels, kittyPixels * 2
|
||||
}
|
||||
return ml.cellWidth, ml.cellHeight
|
||||
}
|
||||
|
||||
func (ml *Model) loadPreviews() tview.Cmd {
|
||||
if !ml.cfg.Attachments.Preview {
|
||||
return nil
|
||||
}
|
||||
pixels := 1
|
||||
if ml.kitty {
|
||||
pixels = kittyPixels
|
||||
}
|
||||
cellWidth, cellHeight := ml.previewCell()
|
||||
var cmds []tview.Cmd
|
||||
for i := range ml.items {
|
||||
item := &ml.items[i]
|
||||
|
|
@ -80,25 +88,29 @@ func (ml *Model) loadPreviews() tview.Cmd {
|
|||
if item.previews == nil {
|
||||
item.previews = make(map[discord.URL]tviewimage.Widget)
|
||||
}
|
||||
width := min(s.width, uint(s.cols()*pixels))
|
||||
cols := s.cols(cellWidth, cellHeight)
|
||||
width := min(s.width, uint(cols)*cellWidth)
|
||||
bounds := image.Rect(0, 0, int(width), int(s.height*width/s.width))
|
||||
widget := func(img image.Image) tviewimage.Widget {
|
||||
return tviewimage.New(img).Width(cols).CellSize(int(cellWidth), int(cellHeight))
|
||||
}
|
||||
// Reserve the preview's rows with a blank image of the same size, so the messages below do not shift when it loads.
|
||||
item.previews[s.proxy] = tviewimage.New(image.NewAlpha(bounds)).Width(s.cols())
|
||||
cmds = append(cmds, loadPreview(item.message.ID, s, bounds))
|
||||
item.previews[s.proxy] = widget(image.NewAlpha(bounds))
|
||||
cmds = append(cmds, loadPreview(item.message.ID, s.proxy, bounds, widget))
|
||||
}
|
||||
}
|
||||
return tview.Batch(cmds...)
|
||||
}
|
||||
|
||||
// loadPreview yields an empty preview if loading fails.
|
||||
func loadPreview(messageID discord.MessageID, s previewSource, bounds image.Rectangle) tview.Cmd {
|
||||
func loadPreview(messageID discord.MessageID, proxy discord.URL, bounds image.Rectangle, widget func(image.Image) tviewimage.Widget) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
img, err := downloadPreview(s.proxy, bounds)
|
||||
img, err := downloadPreview(proxy, bounds)
|
||||
if err != nil {
|
||||
slog.Error("failed to load preview", "err", err)
|
||||
img = &image.Alpha{}
|
||||
}
|
||||
return previewLoadedMsg{messageID: messageID, proxy: s.proxy, preview: tviewimage.New(img).Width(s.cols())}
|
||||
return previewLoadedMsg{messageID: messageID, proxy: proxy, preview: widget(img)}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,17 +38,19 @@ func TestPreviewSources(t *testing.T) {
|
|||
|
||||
func TestPreviewSourceCols(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
width, height uint
|
||||
want int
|
||||
name string
|
||||
width, height uint
|
||||
cellWidth, cellHeight uint
|
||||
want int
|
||||
}{
|
||||
{"landscape", 1600, 900, 40},
|
||||
{"portrait", 900, 1600, 22},
|
||||
{"small", 10, 10, 10},
|
||||
{"very tall", 1, 10000, 1},
|
||||
{"landscape", 1600, 900, 1, 2, 40},
|
||||
{"portrait", 900, 1600, 1, 2, 22},
|
||||
{"small", 10, 10, 1, 2, 10},
|
||||
{"very tall", 1, 10000, 1, 2, 1},
|
||||
{"own width in cells", 200, 100, 10, 20, 20},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := (previewSource{width: tt.width, height: tt.height}).cols(); got != tt.want {
|
||||
if got := (previewSource{width: tt.width, height: tt.height}).cols(tt.cellWidth, tt.cellHeight); got != tt.want {
|
||||
t.Fatalf("cols() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ func (m *Model) update(msg tview.Msg) tview.Cmd {
|
|||
}
|
||||
title := ui.ChannelToString(msg.Channel, m.cfg.Icons, m.state) + " - " + consts.Name
|
||||
return tview.Batch(tview.SetTitle(title), m.messagesList.SetChannel(&msg.Channel, msg.Messages), membersCmd)
|
||||
case messageslist.Msg, tview.TerminalInfoMsg:
|
||||
case messageslist.Msg, tview.TerminalInfoMsg, tview.ResizeMsg:
|
||||
return m.updatePane(paneMessagesList, msg)
|
||||
case typingExpiredMsg:
|
||||
if until, ok := m.typers[msg.userID]; ok && !time.Now().Before(until) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ type Model struct {
|
|||
// modal is the request shown in a dialog, or nil when none is open, and dialogFocus its focused button.
|
||||
modal *ui.ModalMsg
|
||||
dialogFocus int
|
||||
// resize is the last size of the window, or nil before the first, kept for the models shown later.
|
||||
resize tview.ResizeMsg
|
||||
|
||||
cfg *config.Config
|
||||
}
|
||||
|
|
@ -128,6 +130,8 @@ func (m *Model) update(msg tview.Msg) tview.Cmd {
|
|||
if m.modal != nil {
|
||||
return nil
|
||||
}
|
||||
case tview.ResizeMsg:
|
||||
m.resize = msg
|
||||
}
|
||||
|
||||
return m.updateScreen(msg)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ func (m *Model) showLogin() tview.Cmd {
|
|||
|
||||
func (m *Model) showChat(token string) tview.Cmd {
|
||||
m.screen, m.chat, m.login = screenChat, chat.NewModel(m.cfg, token), login.Model{}
|
||||
return m.show(m.chat.Init())
|
||||
init := m.chat.Init()
|
||||
if m.resize != nil {
|
||||
init = tview.Batch(init, m.updateScreen(m.resize))
|
||||
}
|
||||
return m.show(init)
|
||||
}
|
||||
|
||||
// show closes any open modal after the shown model changed and returns what it needs done, with init its first command.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue