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:
2026-10-21 11:18:03 +02:00
co-authored by Claude Opus 5.5
parent 2aa28c7e18
commit 080ef9ec8e
8 changed files with 42 additions and 13 deletions
+13 -2
View File
@@ -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"} {
+11
View File
@@ -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)
}
}
+1 -1
View File
@@ -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 {