Posters in macOS Terminal as plain colored cells
Terminal.app draws block elements from the font, with the halves off the cell's middle, so quadrant posters came out as rows of shifted dots. The new cover style "cells" uses one background color per cell and no glyph; auto picks it when TERM_PROGRAM is Apple_Terminal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
@@ -38,12 +38,14 @@ type UI struct {
|
||||
// Bell also rings the terminal bell for those notifications (default false).
|
||||
Bell bool `json:"bell,omitempty"`
|
||||
// CoverStyle selects the block characters posters are drawn with:
|
||||
// "quadrant" (2x2 pixels per cell, every font has the glyphs), "sextant"
|
||||
// "quadrant" (2x2 pixels per cell, every font has the glyphs), "cells" (1
|
||||
// pixel per cell, background colors only, for terminals that draw block
|
||||
// glyphs unevenly such as macOS Terminal), "sextant"
|
||||
// (2x3, sharper, needs a font with the Symbols for Legacy Computing
|
||||
// block, e.g. kitty, WezTerm, foot, Ghostty), "kitty" (real images through
|
||||
// the Kitty graphics protocol with Unicode placeholders: kitty and
|
||||
// Ghostty) or "auto" (default: the Kitty protocol where the terminal is
|
||||
// known to support it, else sextant on terminals known to draw them or when
|
||||
// known to support it, else cells in macOS Terminal, sextant on terminals known to draw them or when
|
||||
// fontconfig finds a font with the glyphs, else quadrant). The block styles are also the fallback for "kitty".
|
||||
CoverStyle string `json:"cover_style,omitempty"`
|
||||
}
|
||||
@@ -264,9 +266,9 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
switch c.UI.CoverStyle {
|
||||
case "", "auto", "quadrant", "sextant", "kitty":
|
||||
case "", "auto", "quadrant", "sextant", "cells", "kitty":
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf(`ui.cover_style: %q must be "auto", "kitty", "sextant" or "quadrant"`, 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...)
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func TestShowCovers(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":`
|
||||
for _, ok := range []string{`"auto"`, `"quadrant"`, `"sextant"`, `"kitty"`, `""`} {
|
||||
for _, ok := range []string{`"auto"`, `"quadrant"`, `"sextant"`, `"cells"`, `"kitty"`, `""`} {
|
||||
if _, err := Parse([]byte(base + ok + `}}`)); err != nil {
|
||||
t.Errorf("%s rejected: %v", ok, err)
|
||||
}
|
||||
|
||||
+13
-2
@@ -95,6 +95,10 @@ var (
|
||||
// sextantStyle (2×3 pixels): sharper, but the glyphs live in the Symbols
|
||||
// for Legacy Computing block, which many fonts lack.
|
||||
sextantStyle = coverStyle{"sextant", 2, 3, sextantGlyph}
|
||||
// cellsStyle (1 pixel per cell): only background colors, no glyphs at all.
|
||||
// Coarse, but exact in terminals that draw block elements from the font
|
||||
// with the halves off the cell's middle (macOS Terminal).
|
||||
cellsStyle = coverStyle{"cells", 1, 1, func(int) rune { return ' ' }}
|
||||
)
|
||||
|
||||
// quadrants[mask] is the glyph that fills the quadrants set in mask
|
||||
@@ -127,14 +131,21 @@ func sextantGlyph(mask int) rune {
|
||||
}
|
||||
|
||||
// coverStyleFor resolves the block-character style for the setting. "auto"
|
||||
// picks sextants on terminals known to have the glyphs and quadrants everywhere
|
||||
// else.
|
||||
// picks sextants on terminals known to have the glyphs, plain cells in macOS
|
||||
// Terminal and quadrants everywhere else.
|
||||
func coverStyleFor(setting string, env func(string) string) coverStyle {
|
||||
switch setting {
|
||||
case "sextant":
|
||||
return sextantStyle
|
||||
case "quadrant":
|
||||
return quadrantStyle
|
||||
case "cells":
|
||||
return cellsStyle
|
||||
}
|
||||
// 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.
|
||||
if env("TERM_PROGRAM") == "Apple_Terminal" {
|
||||
return cellsStyle
|
||||
}
|
||||
term, program := strings.ToLower(env("TERM")), strings.ToLower(env("TERM_PROGRAM"))
|
||||
for _, known := range []string{"kitty", "foot", "ghostty", "wezterm"} {
|
||||
|
||||
@@ -275,6 +275,9 @@ func TestCoverStyleFor(t *testing.T) {
|
||||
{"auto", nil, "quadrant"},
|
||||
{"quadrant", map[string]string{"TERM": "xterm-kitty"}, "quadrant"},
|
||||
{"sextant", nil, "sextant"},
|
||||
{"auto", map[string]string{"TERM": "xterm-256color", "TERM_PROGRAM": "Apple_Terminal"}, "cells"},
|
||||
{"cells", map[string]string{"TERM": "xterm-kitty"}, "cells"},
|
||||
{"quadrant", map[string]string{"TERM_PROGRAM": "Apple_Terminal"}, "quadrant"},
|
||||
} {
|
||||
if got := coverStyleFor(tc.setting, env(tc.env)).name; got != tc.want {
|
||||
t.Errorf("%q %v = %s, want %s", tc.setting, tc.env, got, tc.want)
|
||||
@@ -347,3 +350,11 @@ func TestDescribeBytesAndTheErrorItGives(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCellsStyleIsFlat(t *testing.T) {
|
||||
c := color.RGBA{10, 200, 30, 255}
|
||||
ch, fg, bg := cellsStyle.bestCell([]color.RGBA{c})
|
||||
if ch != ' ' || fg != c || bg != c {
|
||||
t.Errorf("got %q %v %v", ch, fg, bg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type settingsValues struct {
|
||||
notify, bell bool
|
||||
}
|
||||
|
||||
var coverStyles = []string{"auto", "kitty", "sextant", "quadrant"}
|
||||
var coverStyles = []string{"auto", "kitty", "sextant", "quadrant", "cells"}
|
||||
|
||||
// settingsFrom reads the settings in use.
|
||||
func settingsFrom(c *config.Config) settingsValues {
|
||||
|
||||
Reference in New Issue
Block a user