Real-image posters: blocks while the window is being resized; ARR_TUI_DEBUG_GFX log
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+17
-1
@@ -122,7 +122,7 @@ func New(cfg *config.Config) *App {
|
||||
blocks, graphics := resolveCovers(cfg.UI.CoverStyle, os.Getenv)
|
||||
a.coverStyle = blocks
|
||||
if graphics {
|
||||
a.gfx = newKittyGraphics(a.writeTerminal)
|
||||
a.gfx = a.newGraphics()
|
||||
}
|
||||
|
||||
a.cfg = cfg
|
||||
@@ -192,6 +192,17 @@ func New(cfg *config.Config) *App {
|
||||
return a
|
||||
}
|
||||
|
||||
// newGraphics creates the real-image drawer. With ARR_TUI_DEBUG_GFX set it also
|
||||
// writes what it does (window size, cell shape, every upload) to the Log, which
|
||||
// helps to find out why a poster is drawn wrongly in some terminal.
|
||||
func (a *App) newGraphics() *kittyGraphics {
|
||||
k := newKittyGraphics(a.writeTerminal)
|
||||
if os.Getenv("ARR_TUI_DEBUG_GFX") != "" {
|
||||
k.debug = func(msg string) { a.logs.add(levelInfo, msg) }
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// noteWindow notices when the terminal's size or cell size changed. The
|
||||
// real-image posters are then uploaded and placed again from scratch instead of
|
||||
// relying on the terminal to rescale what is there.
|
||||
@@ -205,6 +216,11 @@ func (a *App) noteWindow(s tcell.Screen) {
|
||||
a.lastWindow = win
|
||||
if !first && a.gfx != nil {
|
||||
a.gfx.deleteAll()
|
||||
a.gfx.hold() // blocks while the size is changing, real images when it has settled
|
||||
time.AfterFunc(settleDelay+30*time.Millisecond, func() { a.post(func() {}) })
|
||||
}
|
||||
if a.gfx != nil && a.gfx.debug != nil {
|
||||
a.gfx.debug(fmt.Sprintf("window: %d×%d cells, cell shape %.3f", w, h, a.cellAspect))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func (a *App) restyleCovers() {
|
||||
a.coverStyle = blocks
|
||||
switch {
|
||||
case graphics && a.gfx == nil:
|
||||
a.gfx = newKittyGraphics(a.writeTerminal)
|
||||
a.gfx = a.newGraphics()
|
||||
case !graphics:
|
||||
a.gfx = nil
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ func cropToBox(b image.Rectangle, cols, rows int, cellAspect float64) image.Rect
|
||||
// color, too many rows or columns, or the terminal could not be written to), so
|
||||
// the caller can draw block characters instead.
|
||||
func (c *coverView) drawGraphics(screen tcell.Screen, x, y, cols, rows int) bool {
|
||||
if c.gfx == nil || screen.Colors() < 1<<24 || cols > maxPlaceholders || rows > maxPlaceholders {
|
||||
if c.gfx == nil || c.gfx.settling() || screen.Colors() < 1<<24 || cols > maxPlaceholders || rows > maxPlaceholders {
|
||||
return false
|
||||
}
|
||||
id, err := c.gfx.show(c.img, cropToBox(c.img.Bounds(), cols, rows, c.aspectOfCells()), cols, rows)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"image"
|
||||
"image/png"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
@@ -39,8 +40,24 @@ type kittyGraphics struct {
|
||||
nextID uint32
|
||||
ids map[footprint]uint32
|
||||
latest map[*image.RGBA]footprint // the footprint of each poster that is currently uploaded
|
||||
|
||||
holdUntil time.Time // no real images until then (the window is being resized)
|
||||
debug func(string) // reports what is uploaded, if set
|
||||
}
|
||||
|
||||
// settleDelay is how long the window size must stay the same before real
|
||||
// images are shown again. While a window is dragged to another size, tens of
|
||||
// sizes go by; uploading and placing images for each of them races with the
|
||||
// terminal's own resizing, and the result can be a placement that does not
|
||||
// match the cells (a small poster in a big area, or a corner of a big one).
|
||||
const settleDelay = 300 * time.Millisecond
|
||||
|
||||
// hold stops real images being drawn until the window has been left alone for settleDelay.
|
||||
func (k *kittyGraphics) hold() { k.holdUntil = time.Now().Add(settleDelay) }
|
||||
|
||||
// settling reports whether the window size changed so recently that blocks are drawn instead.
|
||||
func (k *kittyGraphics) settling() bool { return time.Now().Before(k.holdUntil) }
|
||||
|
||||
// footprint identifies an uploaded image: a poster, the part of it that was
|
||||
// sent, and the cells it covers.
|
||||
type footprint struct {
|
||||
@@ -112,6 +129,9 @@ func (k *kittyGraphics) show(img *image.RGBA, rect image.Rectangle, cols, rows i
|
||||
}
|
||||
k.ids[key] = id
|
||||
k.latest[img] = key
|
||||
if k.debug != nil {
|
||||
k.debug(fmt.Sprintf("graphics: id %d, poster %dx%d px cropped to %v, placed over %dx%d cells", id, img.Bounds().Dx(), img.Bounds().Dy(), rect, cols, rows))
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
@@ -318,3 +319,52 @@ func TestPosterSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphicsAreHeldWhileTheWindowChanges(t *testing.T) {
|
||||
sim := tcell.NewSimulationScreen("")
|
||||
if err := sim.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sim.Fini()
|
||||
sim.SetSize(40, 20)
|
||||
screen := trueColor{sim}
|
||||
|
||||
rec := &recorder{}
|
||||
k := newKittyGraphics(rec.write)
|
||||
c := newCoverView(quadrantStyle, k, nil)
|
||||
c.setImage(stripes(40, 60))
|
||||
c.SetRect(0, 0, 30, 10)
|
||||
|
||||
k.hold()
|
||||
if !k.settling() {
|
||||
t.Fatal("hold must start the settling period")
|
||||
}
|
||||
c.Draw(screen)
|
||||
if len(rec.cmds) != 0 {
|
||||
t.Fatalf("graphics commands while the window changes: %q", rec.cmds)
|
||||
}
|
||||
if ch, _, _, _ := sim.GetContent(29, 9); ch == placeholderRune {
|
||||
t.Error("placeholders drawn while settling; blocks are the stand-in")
|
||||
}
|
||||
|
||||
k.holdUntil = time.Now().Add(-time.Millisecond) // the period is over
|
||||
c.Draw(screen)
|
||||
if len(rec.cmds) != 2 {
|
||||
t.Errorf("after settling: %d commands, want upload + placement", len(rec.cmds))
|
||||
}
|
||||
if ch, _, _, _ := sim.GetContent(29, 9); ch != placeholderRune {
|
||||
t.Error("real image not shown after settling")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphicsDebugReportsUploads(t *testing.T) {
|
||||
rec := &recorder{}
|
||||
k := newKittyGraphics(rec.write)
|
||||
var lines []string
|
||||
k.debug = func(s string) { lines = append(lines, s) }
|
||||
img := noisy(10, 15)
|
||||
k.show(img, img.Bounds(), 12, 8)
|
||||
if len(lines) != 1 || !strings.Contains(lines[0], "id 1") || !strings.Contains(lines[0], "12x8 cells") || !strings.Contains(lines[0], "10x15 px") {
|
||||
t.Errorf("debug lines = %q", lines)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user