Remove the "halves" cover style again

Tried in macOS Terminal: it does not look good there either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
2026-10-21 11:44:52 +02:00
co-authored by Claude Opus 5.5
parent 8f162729ea
commit 7e77d56879
8 changed files with 9 additions and 27 deletions
+1 -2
View File
@@ -8,8 +8,7 @@
- **Posters in macOS Terminal:** Terminal.app draws the half and quarter blocks from the font, and - **Posters in macOS Terminal:** Terminal.app draws the half and quarter blocks from the font, and
their halves do not meet at the middle of the cell, so posters looked like rows of shifted dots. their halves do not meet at the middle of the cell, so posters looked like rows of shifted dots.
`auto` now draws them there as plain colored cells (new `ui.cover_style` value `cells`): coarser, `auto` now draws them there as plain colored cells (new `ui.cover_style` value `cells`): coarser,
but even. `halves` (left and right half blocks only, twice the detail across) can be tried there but even. For real images on macOS use Ghostty or kitty.
too. For real images on macOS use Ghostty or kitty.
- **Real images in Ghostty and kitty on macOS:** the question whether the terminal supports the - **Real images in Ghostty and kitty on macOS:** the question whether the terminal supports the
Kitty graphics protocol gave up at once on macOS (a terminal device there cannot have a read Kitty graphics protocol gave up at once on macOS (a terminal device there cannot have a read
timeout in Go), so posters were always drawn with blocks. It now waits with select(2). timeout in Go), so posters were always drawn with blocks. It now waits with select(2).
+2 -3
View File
@@ -19,9 +19,8 @@ A terminal UI to search for movies and series with [Radarr](https://radarr.video
plain cells in macOS Terminal, sextants on terminals known to have the glyphs or when fontconfig plain cells in macOS Terminal, sextants on terminals known to have the glyphs or when fontconfig
finds an installed font with them (not over SSH), else quadrants), `kitty` (force real images), finds an installed font with them (not over SSH), else quadrants), `kitty` (force real images),
`sextant` (2x3 pixels per cell, needs a font with the Symbols for Legacy Computing block) or `sextant` (2x3 pixels per cell, needs a font with the Symbols for Legacy Computing block) or
`quadrant` (2x2, every font has them), `halves` (2x1, only the left and right half blocks) or `quadrant` (2x2, every font has them) or `cells` (one colored cell per pixel, no glyphs; for
`cells` (one colored cell per pixel, no glyphs); the last two are for terminals whose block terminals whose block characters come out uneven, like macOS Terminal). Inside tmux or screen the
characters come out uneven, like macOS Terminal. Inside tmux or screen the
block styles are used. If a poster looks wrong, set `quadrant`, or `cells` if the rows look shifted. block styles are used. If a poster looks wrong, set `quadrant`, or `cells` if the rows look shifted.
- **Calendar**: the next two weeks of movie releases (cinemas, digital, physical) and airing episodes - **Calendar**: the next two weeks of movie releases (cinemas, digital, physical) and airing episodes
in one list grouped by day, with poster and plot. Move by weeks, include unmonitored items, search in one list grouped by day, with poster and plot. Move by weeks, include unmonitored items, search
+1 -1
View File
@@ -65,7 +65,7 @@ Notes:
- **Status bar / help:** the bottom bar lists the keys that work in the current view or panel; the top right shows the connection state of each service; `F9` opens the full key reference. - **Status bar / help:** the bottom bar lists the keys that work in the current view or panel; the top right shows the connection state of each service; `F9` opens the full key reference.
- **Posters:** the detail panes show the poster at the top right; the head (title, facts, path) and the plot are one text that flows along the poster's left side and continues at full width below it (text is wrapped by arr-tui itself, with a narrower width for the first lines); on panes too small for the poster, the text simply uses the full width. Two ways to draw the poster, chosen by `ui.cover_style`: - **Posters:** the detail panes show the poster at the top right; the head (title, facts, path) and the plot are one text that flows along the poster's left side and continues at full width below it (text is wrapped by arr-tui itself, with a narrower width for the first lines); on panes too small for the poster, the text simply uses the full width. Two ways to draw the poster, chosen by `ui.cover_style`:
- **Real images** (`kitty`, and `auto` in kitty/Ghostty): the Kitty graphics protocol with Unicode placeholders. The poster (kept up to 640 px tall) is uploaded as PNG (`a=t,f=100,q=2`, 4096-byte base64 chunks) and placed virtually with the cell footprint (`a=p,U=1,c=,r=`) right away. Every footprint (crop and cell size) is its own image id, placed once and never changed: Ghostty kept showing the old, small placement when the size of an existing one was changed, so a new size uploads under a new id and deletes the old one. The poster is shown by writing U+10EEEE placeholder characters whose combining marks give row and column (the 297-entry table from the protocol) and whose foreground color is the image id. Since it is ordinary text, popups, dialogs and view changes cover it correctly and tcell's diffing keeps it in place. The footprint uses the real cell size in pixels (the window's pixel size divided by its cell count, rounded down to whole pixels so padding around the grid does not skew it; all images are deleted whenever the window or cell size changes, block characters stand in while the size keeps changing, and real images are uploaded afresh once it has been stable for 300 ms, because uploading and placing for every intermediate size of a window drag raced with the terminal's own resizing) and a box whose shape is within 3% of the poster's; the remaining sliver is cropped from the poster's edges before upload (one upload per size, the previous one is deleted), because the terminal letterboxes an image that does not fit its box exactly. As a second safeguard the placeholder cells use the panes' background color, so anything the picture does not cover blends in (without it the terminal's own, slightly different, default background showed as grey bars beside the poster). Writes to the terminal go through the screen's tty on the UI goroutine, so they cannot interleave with drawing. Requires 24-bit color; otherwise, and inside tmux/screen, the block fallback is used. Terminals are recognised from `TERM`/`TERM_PROGRAM`/`KITTY_WINDOW_ID`/`GHOSTTY_RESOURCES_DIR`, and then asked before the screen starts (a Kitty graphics query followed by a device-attributes request, 300 ms timeout): only a terminal that answers the query gets real images, also with `cover_style: kitty`, because a terminal without the protocol prints the commands as text. The Log says which way posters are drawn. With `ARR_TUI_DEBUG_GFX=1` the Log (F7) lists every window size, the cell shape, every upload (poster size, crop, cells) and every change of what is drawn (image id, position, cells); with a path (`ARR_TUI_DEBUG_GFX=/tmp/arr-gfx.log`) the same lines also go to that file with millisecond timestamps, so they can be read without leaving the view that shows the problem. The go-termimg library was looked at and not used: its placeholder support is marked experimental, its diacritic table does not match the protocol's, and it writes images straight to the terminal instead of through the screen. - **Real images** (`kitty`, and `auto` in kitty/Ghostty): the Kitty graphics protocol with Unicode placeholders. The poster (kept up to 640 px tall) is uploaded as PNG (`a=t,f=100,q=2`, 4096-byte base64 chunks) and placed virtually with the cell footprint (`a=p,U=1,c=,r=`) right away. Every footprint (crop and cell size) is its own image id, placed once and never changed: Ghostty kept showing the old, small placement when the size of an existing one was changed, so a new size uploads under a new id and deletes the old one. The poster is shown by writing U+10EEEE placeholder characters whose combining marks give row and column (the 297-entry table from the protocol) and whose foreground color is the image id. Since it is ordinary text, popups, dialogs and view changes cover it correctly and tcell's diffing keeps it in place. The footprint uses the real cell size in pixels (the window's pixel size divided by its cell count, rounded down to whole pixels so padding around the grid does not skew it; all images are deleted whenever the window or cell size changes, block characters stand in while the size keeps changing, and real images are uploaded afresh once it has been stable for 300 ms, because uploading and placing for every intermediate size of a window drag raced with the terminal's own resizing) and a box whose shape is within 3% of the poster's; the remaining sliver is cropped from the poster's edges before upload (one upload per size, the previous one is deleted), because the terminal letterboxes an image that does not fit its box exactly. As a second safeguard the placeholder cells use the panes' background color, so anything the picture does not cover blends in (without it the terminal's own, slightly different, default background showed as grey bars beside the poster). Writes to the terminal go through the screen's tty on the UI goroutine, so they cannot interleave with drawing. Requires 24-bit color; otherwise, and inside tmux/screen, the block fallback is used. Terminals are recognised from `TERM`/`TERM_PROGRAM`/`KITTY_WINDOW_ID`/`GHOSTTY_RESOURCES_DIR`, and then asked before the screen starts (a Kitty graphics query followed by a device-attributes request, 300 ms timeout): only a terminal that answers the query gets real images, also with `cover_style: kitty`, because a terminal without the protocol prints the commands as text. The Log says which way posters are drawn. With `ARR_TUI_DEBUG_GFX=1` the Log (F7) lists every window size, the cell shape, every upload (poster size, crop, cells) and every change of what is drawn (image id, position, cells); with a path (`ARR_TUI_DEBUG_GFX=/tmp/arr-gfx.log`) the same lines also go to that file with millisecond timestamps, so they can be read without leaving the view that shows the problem. The go-termimg library was looked at and not used: its placeholder support is marked experimental, its diacritic table does not match the protocol's, and it writes images straight to the terminal instead of through the screen.
- **Block characters** (`quadrant`, `sextant`, `halves`, `cells`, and the fallback): every cell shows several pixels in the best two-color split (`quadrant`: 2×2 pixels, glyphs from the Block Elements range every font has; `sextant`: 2×3 pixels, sharper, glyphs from Symbols for Legacy Computing; `halves`: 2×1 pixels with the left and right half blocks only; `cells`: 1 pixel per cell drawn as background color only, because macOS Terminal draws block elements from the font with the halves off the cell's middle). `auto` picks cells in macOS Terminal (`TERM_PROGRAM=Apple_Terminal`), sextants on kitty, WezTerm, foot and Ghostty, and on other terminals (not on macOS, whose terminals do not use fontconfig's fonts) when fontconfig lists a font with the sextant characters (`fc-list :charset=1fb00`, about 20 ms at startup; terminals draw glyphs their font lacks from fallback fonts; skipped over SSH because the installed fonts are the remote machine's), and quadrants otherwise. - **Block characters** (`quadrant`, `sextant`, `cells`, and the fallback): every cell shows several pixels in the best two-color split (`quadrant`: 2×2 pixels, glyphs from the Block Elements range every font has; `sextant`: 2×3 pixels, sharper, glyphs from Symbols for Legacy Computing; `cells`: 1 pixel per cell drawn as background color only, because macOS Terminal draws block elements from the font with the halves off the cell's middle). `auto` picks cells in macOS Terminal (`TERM_PROGRAM=Apple_Terminal`), sextants on kitty, WezTerm, foot and Ghostty, and on other terminals (not on macOS, whose terminals do not use fontconfig's fonts) when fontconfig lists a font with the sextant characters (`fc-list :charset=1fb00`, about 20 ms at startup; terminals draw glyphs their font lacks from fallback fonts; skipped over SSH because the installed fonts are the remote machine's), and quadrants otherwise.
Posters are JPEG, PNG, GIF or WebP (Radarr and Sonarr keep whatever bytes the provider sent under a `.jpg` name, which can be WebP), decoded and downscaled in the process; for any other format the Log says what the data looked like (e.g. AVIF, an HTML error page, or its first bytes). Library items fetch `/MediaCover/<id>/poster-250.jpg` from their Radarr/Sonarr instance with the API key (then the original `poster.jpg`; an instance that answers 200 with an HTML page, i.e. wants a login for `/MediaCover`, is also tried with the key as the `apikey` query parameter; and if the instance has no usable copy, the provider's `remoteUrl`, fetched without credentials); search results fall back to the provider's `remoteUrl` (fetched without credentials). Posters are cached in memory (the terminal's copies are deleted when the cache is dropped and on exit), failed downloads are not retried for two minutes and are written to the Log (F7) with the reason, and the poster shrinks or disappears when the pane is too short. `ui.covers: false` disables it. Posters are JPEG, PNG, GIF or WebP (Radarr and Sonarr keep whatever bytes the provider sent under a `.jpg` name, which can be WebP), decoded and downscaled in the process; for any other format the Log says what the data looked like (e.g. AVIF, an HTML error page, or its first bytes). Library items fetch `/MediaCover/<id>/poster-250.jpg` from their Radarr/Sonarr instance with the API key (then the original `poster.jpg`; an instance that answers 200 with an HTML page, i.e. wants a login for `/MediaCover`, is also tried with the key as the `apikey` query parameter; and if the instance has no usable copy, the provider's `remoteUrl`, fetched without credentials); search results fall back to the provider's `remoteUrl` (fetched without credentials). Posters are cached in memory (the terminal's copies are deleted when the cache is dropped and on exit), failed downloads are not retried for two minutes and are written to the Log (F7) with the reason, and the poster shrinks or disappears when the pane is too short. `ui.covers: false` disables it.
- **Calendar view (F5):** a two-week window (starting today, moved a week at a time with `[`/`]`, `t` returns to today) with the library's movie releases and airing episodes, grouped by day. Radarr `GET /calendar` supplies the cinema, digital and physical release dates (taken as written: midnight UTC means that calendar day) and Sonarr `GET /calendar?includeSeries=true` the episodes with their air time (shown in local time). Unmonitored items are included with `u`. A movie appears once per release date in the window. Each entry shows type, title, episode or release kind and status (downloaded / missing / upcoming); the detail pane shows poster and plot; `Enter`/`i` starts the release search and `s` an automatic search. - **Calendar view (F5):** a two-week window (starting today, moved a week at a time with `[`/`]`, `t` returns to today) with the library's movie releases and airing episodes, grouped by day. Radarr `GET /calendar` supplies the cinema, digital and physical release dates (taken as written: midnight UTC means that calendar day) and Sonarr `GET /calendar?includeSeries=true` the episodes with their air time (shown in local time). Unmonitored items are included with `u`. A movie appears once per release date in the window. Each entry shows type, title, episode or release kind and status (downloaded / missing / upcoming); the detail pane shows poster and plot; `Enter`/`i` starts the release search and `s` an automatic search.
- **Download notifications:** a background poller (independent of the visible view) reads SABnzbd's history every 15 s and announces downloads that reach `Completed` or `Failed` as a popup and log entry; the first poll only records the current state. `ui.notify_downloads` (default true) and `ui.bell` (default false, rings the terminal bell) control it. Connection errors are not reported here, the indicators and the log already do that. - **Download notifications:** a background poller (independent of the visible view) reads SABnzbd's history every 15 s and announces downloads that reach `Completed` or `Failed` as a popup and log entry; the first poll only records the current state. `ui.notify_downloads` (default true) and `ui.bell` (default false, rings the terminal bell) control it. Connection errors are not reported here, the indicators and the log already do that.
+3 -4
View File
@@ -40,8 +40,7 @@ type UI struct {
// CoverStyle selects the block characters posters are drawn with: // CoverStyle selects the block characters posters are drawn with:
// "quadrant" (2x2 pixels per cell, every font has the glyphs), "cells" (1 // "quadrant" (2x2 pixels per cell, every font has the glyphs), "cells" (1
// pixel per cell, background colors only, for terminals that draw block // pixel per cell, background colors only, for terminals that draw block
// glyphs unevenly such as macOS Terminal), "halves" (2x1, left and right // glyphs unevenly such as macOS Terminal), "sextant"
// half blocks only, for the same terminals), "sextant"
// (2x3, sharper, needs a font with the Symbols for Legacy Computing // (2x3, sharper, needs a font with the Symbols for Legacy Computing
// block, e.g. kitty, WezTerm, foot, Ghostty), "kitty" (real images through // block, e.g. kitty, WezTerm, foot, Ghostty), "kitty" (real images through
// the Kitty graphics protocol with Unicode placeholders: kitty and // the Kitty graphics protocol with Unicode placeholders: kitty and
@@ -267,9 +266,9 @@ func (c *Config) Validate() error {
} }
} }
switch c.UI.CoverStyle { switch c.UI.CoverStyle {
case "", "auto", "quadrant", "sextant", "halves", "cells", "kitty": case "", "auto", "quadrant", "sextant", "cells", "kitty":
default: default:
errs = append(errs, fmt.Errorf(`ui.cover_style: %q must be "auto", "kitty", "sextant", "quadrant", "halves" or "cells"`, c.UI.CoverStyle)) errs = append(errs, fmt.Errorf(`ui.cover_style: %q must be "auto", "kitty", "sextant", "quadrant" or "cells"`, c.UI.CoverStyle))
} }
return errors.Join(errs...) return errors.Join(errs...)
} }
+1 -1
View File
@@ -100,7 +100,7 @@ func TestShowCovers(t *testing.T) {
func TestCoverStyleValidation(t *testing.T) { func TestCoverStyleValidation(t *testing.T) {
base := `{"radarr":{"url":"http://r","apikey":"k"},"sonarr":{"url":"http://s","apikey":"k"},"sabnzbd":{"url":"http://a","apikey":"k"},"ui":{"cover_style":` base := `{"radarr":{"url":"http://r","apikey":"k"},"sonarr":{"url":"http://s","apikey":"k"},"sabnzbd":{"url":"http://a","apikey":"k"},"ui":{"cover_style":`
for _, ok := range []string{`"auto"`, `"quadrant"`, `"sextant"`, `"cells"`, `"halves"`, `"kitty"`, `""`} { for _, ok := range []string{`"auto"`, `"quadrant"`, `"sextant"`, `"cells"`, `"kitty"`, `""`} {
if _, err := Parse([]byte(base + ok + `}}`)); err != nil { if _, err := Parse([]byte(base + ok + `}}`)); err != nil {
t.Errorf("%s rejected: %v", ok, err) t.Errorf("%s rejected: %v", ok, err)
} }
-6
View File
@@ -99,10 +99,6 @@ var (
// Coarse, but exact in terminals that draw block elements from the font // Coarse, but exact in terminals that draw block elements from the font
// with the halves off the cell's middle (macOS Terminal). // with the halves off the cell's middle (macOS Terminal).
cellsStyle = coverStyle{"cells", 1, 1, func(int) rune { return ' ' }} cellsStyle = coverStyle{"cells", 1, 1, func(int) rune { return ' ' }}
// halvesStyle (2×1 pixels): left and right half blocks only. Twice the
// detail of cells across, for terminals whose upper and lower halves do not
// meet at the middle but whose left and right ones do.
halvesStyle = coverStyle{"halves", 2, 1, func(mask int) rune { return [4]rune{' ', '▌', '▐', '█'}[mask] }}
) )
// quadrants[mask] is the glyph that fills the quadrants set in mask // quadrants[mask] is the glyph that fills the quadrants set in mask
@@ -145,8 +141,6 @@ func coverStyleFor(setting string, env func(string) string) coverStyle {
return quadrantStyle return quadrantStyle
case "cells": case "cells":
return cellsStyle return cellsStyle
case "halves":
return halvesStyle
} }
// Terminal.app draws block elements from the font, and their halves do // Terminal.app draws block elements from the font, and their halves do
// not meet at the cell's middle: rows of a poster come out uneven. // not meet at the cell's middle: rows of a poster come out uneven.
-9
View File
@@ -277,7 +277,6 @@ func TestCoverStyleFor(t *testing.T) {
{"sextant", nil, "sextant"}, {"sextant", nil, "sextant"},
{"auto", map[string]string{"TERM": "xterm-256color", "TERM_PROGRAM": "Apple_Terminal"}, "cells"}, {"auto", map[string]string{"TERM": "xterm-256color", "TERM_PROGRAM": "Apple_Terminal"}, "cells"},
{"cells", map[string]string{"TERM": "xterm-kitty"}, "cells"}, {"cells", map[string]string{"TERM": "xterm-kitty"}, "cells"},
{"halves", map[string]string{"TERM_PROGRAM": "Apple_Terminal"}, "halves"},
{"quadrant", map[string]string{"TERM_PROGRAM": "Apple_Terminal"}, "quadrant"}, {"quadrant", map[string]string{"TERM_PROGRAM": "Apple_Terminal"}, "quadrant"},
} { } {
if got := coverStyleFor(tc.setting, env(tc.env)).name; got != tc.want { if got := coverStyleFor(tc.setting, env(tc.env)).name; got != tc.want {
@@ -359,11 +358,3 @@ func TestCellsStyleIsFlat(t *testing.T) {
t.Errorf("got %q %v %v", ch, fg, bg) t.Errorf("got %q %v %v", ch, fg, bg)
} }
} }
func TestHalvesStyle(t *testing.T) {
dark, light := color.RGBA{0, 0, 0, 255}, color.RGBA{250, 250, 250, 255}
ch, fg, bg := halvesStyle.bestCell([]color.RGBA{light, dark})
if ch != '▌' || fg != light || bg != dark {
t.Errorf("light left half: %q %v %v", ch, fg, bg)
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ type settingsValues struct {
notify, bell bool notify, bell bool
} }
var coverStyles = []string{"auto", "kitty", "sextant", "quadrant", "halves", "cells"} var coverStyles = []string{"auto", "kitty", "sextant", "quadrant", "cells"}
// settingsFrom reads the settings in use. // settingsFrom reads the settings in use.
func settingsFrom(c *config.Config) settingsValues { func settingsFrom(c *config.Config) settingsValues {