Show posters as real images in kitty and Ghostty (Kitty graphics with Unicode placeholders)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 23:03:29 +02:00
co-authored by Claude Sonnet 5.5
parent dc82b0b95d
commit 108541af91
12 changed files with 601 additions and 49 deletions
+49 -1
View File
@@ -52,6 +52,8 @@ type App struct {
bell bool
covers bool // poster thumbnails enabled
coverStyle coverStyle
gfx *kittyGraphics // real-image posters, nil when not used
cellAspect float64 // height of a terminal cell divided by its width
coverCache *coverCache
}
@@ -99,10 +101,16 @@ func New(cfg *config.Config) *App {
notifyDone: cfg.UI.ShouldNotifyDownloads(),
bell: cfg.UI.Bell,
covers: cfg.UI.ShowCovers(),
coverStyle: coverStyleFor(cfg.UI.CoverStyle, os.Getenv),
cellAspect: 2,
coverCache: newCoverCache(),
}
blocks, graphics := resolveCovers(cfg.UI.CoverStyle, os.Getenv)
a.coverStyle = blocks
if graphics {
a.gfx = newKittyGraphics(a.writeTerminal)
}
radarrClient := radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)
sonarrClient := sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)
a.logs = newLogView(a)
@@ -149,6 +157,7 @@ func New(cfg *config.Config) *App {
a.app.SetBeforeDrawFunc(func(s tcell.Screen) bool {
a.screen = s
a.cellAspect = cellAspectOf(s)
return false
})
a.app.SetRoot(&toastRoot{Flex: layout, app: a}, true).EnableMouse(false)
@@ -158,6 +167,40 @@ func New(cfg *config.Config) *App {
return a
}
// writeTerminal sends raw bytes (graphics commands) to the terminal through the
// screen's own connection. UI goroutine only, so it stays in step with drawing.
func (a *App) writeTerminal(b []byte) error {
if a.screen == nil {
return fmt.Errorf("no screen yet")
}
tty, ok := a.screen.Tty()
if !ok {
return fmt.Errorf("the screen has no terminal")
}
_, err := tty.Write(b)
return err
}
// cellAspectOf measures the height of a terminal cell divided by its width in
// pixels; 2 when the terminal does not report pixel sizes.
func cellAspectOf(s tcell.Screen) float64 {
if tty, ok := s.Tty(); ok {
if ws, err := tty.WindowSize(); err == nil && ws.Width > 0 && ws.Height > 0 && ws.PixelWidth > 0 && ws.PixelHeight > 0 {
return (float64(ws.PixelHeight) / float64(ws.Height)) / (float64(ws.PixelWidth) / float64(ws.Width))
}
}
return 2
}
// coverMaxHeight is how tall the stored poster copies are: real images keep
// more detail than block characters need.
func (a *App) coverMaxHeight() int {
if a.gfx != nil {
return graphicsCoverHeight
}
return storedCoverHeight
}
// SetService records the outcome of a request to a service and updates the
// connection indicators. UI goroutine only. A nil err means reachable.
func (a *App) SetService(name, version string, err error) {
@@ -246,6 +289,11 @@ func (a *App) post(f func()) {
func (a *App) Run() error {
stop := make(chan struct{})
defer close(stop)
defer func() {
if a.gfx != nil {
a.gfx.deleteAll()
}
}()
go a.watchServices(stop)
if a.notifyDone {
ctx, cancel := context.WithCancel(context.Background())