database-server-2-streamline-core.png

The 3/1/26 Brief: 9-segment displays

Mar 1, 2026 — Gibson

Welcome to yet another brief! I'm trying to keep this up. Not all briefs will be the same size, this one is quite short - but cool!

So, you know 7-segment displays?

https://commons.wikimedia.org/wiki/File:7_segment_display_labeled.svg

Well, they cannot display every character of the alphabet. So I made a segmented display that could. Inspired by some of the drafts from here I created a...

Just adding two lines in the middle made it able to display A-Z, 0-9, and punctuation.

Sure, K, R, and X look a little weird, but it can make legible sentences.

This is the character grid.

By the way, Claude made the script to make these SVGs, I've linked it to download. It requires Python 3 to run.

#!/usr/bin/env python3
"""9-segment display SVG generator.

A 9-segment display is like a 7-segment display with two vertical
segments in the middle (h = upper-middle, i = lower-middle).

Segment layout:
    aaaa
   f    b
   f h  b
    gggg
   e i  c
   e    c
    dddd

Usage:
    ./9seg a b e                  # SVG with segments a, b, e lit
    ./9seg --guide                # SVG guide showing segment names
    ./9seg -t "Hello world"       # SVG text using characters.toml
    ./9seg -c red -t "Hi"         # Custom active segment color
    ./9seg -c "#00ff00" a b c     # Custom color for single display
"""

import os
import sys

# Segment geometry: each segment is a polygon defined by points
# Display box is roughly 60w x 100h, with some padding
SEGMENTS = {
    "a": [(12, 4), (48, 4), (44, 8), (16, 8)],              # top horizontal
    "b": [(50, 6), (50, 46), (46, 42), (46, 10)],            # top-right vertical
    "c": [(50, 54), (50, 94), (46, 90), (46, 58)],           # bottom-right vertical
    "d": [(12, 96), (48, 96), (44, 92), (16, 92)],           # bottom horizontal
    "e": [(10, 54), (10, 94), (14, 90), (14, 58)],           # bottom-left vertical
    "f": [(10, 6), (10, 46), (14, 42), (14, 10)],            # top-left vertical
    "g": [(12, 48), (48, 48), (46, 52), (14, 52)],           # middle horizontal
    "h": [(28, 10), (32, 10), (32, 46), (28, 46)],           # upper-middle vertical
    "i": [(28, 54), (32, 54), (32, 90), (28, 90)],           # lower-middle vertical
}

# Label positions for --guide mode (x, y) roughly centered on each segment
LABEL_POS = {
    "a": (30, 8),
    "b": (51, 28),
    "c": (51, 76),
    "d": (30, 98),
    "e": (5, 76),
    "f": (5, 28),
    "g": (30, 52),
    "h": (30, 28),
    "i": (30, 76),
}

DEFAULT_LIT_COLOR = "#ff3300"
DIM_COLOR = "#e0e0e0"
GUIDE_COLORS = [
    "#e63946", "#457b9d", "#2a9d8f", "#e9c46a",
    "#f4a261", "#264653", "#6a4c93", "#1982c4", "#8ac926",
]

CHAR_WIDTH = 60
CHAR_HEIGHT = 100
CHAR_GAP = -2
SPACE_WIDTH = 30


def points_str(pts):
    return " ".join(f"{x},{y}" for x, y in pts)


def load_characters(path):
    """Load characters.toml, return case-insensitive {char: [segments]} mapping."""
    try:
        import tomllib
    except ImportError:
        tomllib = None

    if tomllib:
        with open(path, "rb") as f:
            raw = tomllib.load(f)
    else:
        # Fallback parser for flat "key" = "value" TOML
        raw = {}
        with open(path) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" not in line:
                    continue
                k, v = line.split("=", 1)
                k = k.strip().strip('"').strip("'")
                v = v.strip().strip('"').strip("'")
                raw[k] = v

    valid_segs = set(SEGMENTS.keys())
    chars = {}
    seen = {}  # lowercase -> original key (for error messages)

    for key, value in raw.items():
        if len(key) != 1:
            print(f"Error: characters.toml keys must be single characters, got '{key}'", file=sys.stderr)
            sys.exit(1)

        lower = key.lower()
        if lower in seen:
            print(
                f"Error: characters.toml has duplicate (case-insensitive) entries: '{seen[lower]}' and '{key}'",
                file=sys.stderr,
            )
            sys.exit(1)
        seen[lower] = key

        segments = value.split() if value.strip() else []
        for seg in segments:
            if seg not in valid_segs:
                print(f"Error: unknown segment '{seg}' in mapping for '{key}'", file=sys.stderr)
                sys.exit(1)

        chars[lower] = segments

    return chars


def make_svg(lit_segments, lit_color=DEFAULT_LIT_COLOR):
    lit = set(lit_segments)
    polys = []
    for name, pts in SEGMENTS.items():
        color = lit_color if name in lit else DIM_COLOR
        polys.append(
            f'  <polygon points="{points_str(pts)}" fill="{color}" />'
        )
    return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 100" width="240" height="400">
{chr(10).join(polys)}
</svg>"""


def make_guide():
    polys = []
    labels = []
    for idx, (name, pts) in enumerate(SEGMENTS.items()):
        color = GUIDE_COLORS[idx % len(GUIDE_COLORS)]
        polys.append(
            f'  <polygon points="{points_str(pts)}" fill="{color}" opacity="0.85" />'
        )
        lx, ly = LABEL_POS[name]
        labels.append(
            f'  <text x="{lx}" y="{ly}" text-anchor="middle" '
            f'dominant-baseline="central" font-family="monospace" '
            f'font-size="7" font-weight="bold" fill="white" '
            f'stroke="black" stroke-width="0.3">{name}</text>'
        )
    return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 100" width="240" height="400">
{chr(10).join(polys)}
{chr(10).join(labels)}
</svg>"""


def make_text_svg(text, chars, lit_color=DEFAULT_LIT_COLOR):
    """Generate combined SVG for a text string."""
    groups = []
    x = 0

    for ch in text:
        if ch == " ":
            x += SPACE_WIDTH
            continue

        lower = ch.lower()
        if lower not in chars:
            print(f"Error: no mapping for '{ch}' in characters.toml", file=sys.stderr)
            sys.exit(1)

        lit = set(chars[lower])
        polys = []
        for name, pts in SEGMENTS.items():
            color = lit_color if name in lit else DIM_COLOR
            polys.append(f'    <polygon points="{points_str(pts)}" fill="{color}" />')

        groups.append(f'  <g transform="translate({x}, 0)">\n' + "\n".join(polys) + "\n  </g>")
        x += CHAR_WIDTH + CHAR_GAP

    # Remove trailing gap after last non-space character
    if groups:
        x -= CHAR_GAP

    total_width = max(x, 1)
    pixel_height = 400
    pixel_width = int(pixel_height * total_width / CHAR_HEIGHT)

    return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_width} {CHAR_HEIGHT}" width="{pixel_width}" height="{pixel_height}">
{chr(10).join(groups)}
</svg>"""


def main():
    args = sys.argv[1:]

    if not args:
        print(__doc__.strip())
        sys.exit(1)

    if "--guide" in args:
        print(make_guide())
        sys.exit(0)

    # Parse -c / --color (must come first if present)
    lit_color = DEFAULT_LIT_COLOR
    if args and args[0] in ("-c", "--color"):
        if len(args) < 2:
            print("Error: -c/--color requires a color value", file=sys.stderr)
            sys.exit(1)
        lit_color = args[1]
        args = args[2:]

    if not args:
        print("Error: no segments or text specified", file=sys.stderr)
        sys.exit(1)

    # Parse -t / --text
    if args[0] in ("-t", "--text"):
        if len(args) < 2:
            print("Error: -t/--text requires a string argument", file=sys.stderr)
            sys.exit(1)
        text = args[1]

        # Find characters.toml next to this script
        script_dir = os.path.dirname(os.path.realpath(__file__))
        toml_path = os.path.join(script_dir, "characters.toml")

        if not os.path.exists(toml_path):
            print(f"Error: {toml_path} not found", file=sys.stderr)
            sys.exit(1)

        chars = load_characters(toml_path)
        print(make_text_svg(text, chars, lit_color))
        sys.exit(0)

    # Original mode: segment names as arguments
    valid = set(SEGMENTS.keys())
    for seg in args:
        if seg not in valid:
            print(f"Error: unknown segment '{seg}'. Valid: {', '.join(sorted(valid))}", file=sys.stderr)
            sys.exit(1)

    print(make_svg(args, lit_color))


if __name__ == "__main__":
    main()

Hope you enjoyed!

The 2/28/26 Brief: First look at IndexOS, HaPPN stack, and more The 3/3/26 Brief: Helpless Blobs