diff --git a/docs/changes/unreleased/1106-wordmark-a-is-not-an-eight.md b/docs/changes/unreleased/1106-wordmark-a-is-not-an-eight.md new file mode 100644 index 0000000000..2d12fd6fad --- /dev/null +++ b/docs/changes/unreleased/1106-wordmark-a-is-not-an-eight.md @@ -0,0 +1,23 @@ +--- +kind: changed +title: The wordmark is drawn `CodeAF`, four rows tall, with the capitals on the `d`'s cap line +pr: 1106 +surface: [chat] +invalidates: + - "The wordmark was three rows. It is four: the x-height letters (`c`, `o`, `e`) sit in the lower three and leave the top row blank, and the capital `A`, the capital `F` and the `d`'s ascender start in the top row. `wordmarkRows` returns four rows and `wordmarkGlyphs` is `map[rune][4]string`; anything holding three rows, or a `[3]string`, is holding the old letterforms." + - "The last two letters were lowercase forms, and the `a` was `┌─┐ / ├─┤ / └─┘`, which reads as an `8`. They are a capital `A` (`┌─┐ / │ │ / ├─┤ / ╵ ╵`) and a capital `F` with the longer top arm (`┌── / ├─ / │ / ╵ `). The whole word is ` ╷ ┌─┐ ┌──` over `┌─ ┌─┐ ┌─┤ ┌─┐ │ │ ├─ ` over `│ │ │ │ │ ├─╴ ├─┤ │ ` over `└─ └─┘ └─┘ └─┘ ╵ ╵ ╵ `." + - "A bar in box-drawing sits at the middle of its cell and only a vertical reaches the edge, so the `d`'s ascender is now the half-stroke `╷` (its top is the capitals' top bar, not the edge above it) and every stem that ends on the baseline ends as `╵` (it stops where `└─┘` does). A `│` in the bottom row is a descender and nothing else. `d` is ` ╷ / ┌─┤ / │ │ / └─┘`, with a bowl the height of the `o`'s." + - "The DRAWING is mixed case and the TEXT is not: `product` is still `codeaf`, the glyph table is still keyed by its lowercase letters, and a terminal that cannot draw boxes still gets the word `codeaf`. The lowercase spelling law over sentences, titles and `--help` is unchanged; only the letterforms under two of its letters are." + - "`internal/manual/chat/empty-screen.md` drew the retired `aforge` letterforms, with the pre-terminal `e`, under the label \"the codeaf wordmark\". It draws the shipped letterforms now. The name law walks text and cannot see a name spelled in box-drawing, so that page had spelled the old name in pictures since the rename." +--- + +Two closed bowls of equal size stacked on each other are an `8`, whatever letter +they were drawn for, and that is what the second-to-last glyph of the product's +own name had been on the first screen of every fresh install. The owner's answer +was to have the two letters at the end stand up out of the word as capitals — +and a capital's top is the ascender's top, which three rows of box-drawing could +not give it: a bar is drawn at the middle of its cell, a stem reaches the edge, +so the `d` stood half a row over the `A` and the `F` however they were formed. +The fourth row is that half a row made whole, with the x-height letters a row +lower and the half-strokes `╷` and `╵` putting the ascender's top and every +capital's feet on the same two lines the bowls use. diff --git a/internal/manual/chat/empty-screen.md b/internal/manual/chat/empty-screen.md index 6d7d26423e..6591997bdf 100644 --- a/internal/manual/chat/empty-screen.md +++ b/internal/manual/chat/empty-screen.md @@ -5,9 +5,10 @@ A new conversation opens on **one centred group** and nothing else. From the top: ``` -┌─┐ ┌─ ┌─┐ ┌─┐ ┌─┐ ┌─┐ -├─┤ ├─ │ │ │ └─┤ ├─ the codeaf wordmark -└─┘ │ └─┘ │ └─┘ └─┘ + ╷ ┌─┐ ┌── +┌─ ┌─┐ ┌─┤ ┌─┐ │ │ ├─ the codeaf wordmark, drawn CodeAF +│ │ │ │ │ ├─╴ ├─┤ │ +└─ └─┘ └─┘ └─┘ ╵ ╵ ╵ anthropic/claude-sonnet-4.5 · balanced crew a blank row › _ the message box, cursor in it diff --git a/internal/tui3/chrome_test.go b/internal/tui3/chrome_test.go index 245c359c8f..bef5bc8dd8 100644 --- a/internal/tui3/chrome_test.go +++ b/internal/tui3/chrome_test.go @@ -610,10 +610,10 @@ func TestTheEmptyScreenIsOneCentredUnit(t *testing.T) { screen := strings.Join(lines, "\n") wordmark := wordmarkRows(false) - if len(wordmark) != 3 { + if len(wordmark) != 4 { t.Fatalf("the wordmark is %d rows", len(wordmark)) } - order := []string{wordmark[0], wordmark[2], "openai/gpt-4.1-mini", glyphYou, + order := []string{wordmark[0], wordmark[3], "openai/gpt-4.1-mini", glyphYou, starterTryWord, "recent sessions", "porting the parser", "reading the registry"} last := -1 for _, want := range order { @@ -641,14 +641,17 @@ func TestTheEmptyScreenIsOneCentredUnit(t *testing.T) { } // EVERY ROW OF THE UNIT SHARES ONE LEFT EDGE, and the edge is centred at - // every width the unit is drawn at. + // every width the unit is drawn at. The edge is read off the wordmark's + // SECOND row: the first is the cap line, and the x-height letters that + // begin the word leave it blank, so its lead is the unit's edge plus the + // width of three letters that are not there yet. for _, width := range []int{60, 90, 120, 200} { a.width = width a.touch() lines := screenLines(a) unit := min(width-4, welcomeUnitWidth) want := (width - unit) / 2 - for _, row := range []string{wordmark[0], "openai/gpt-4.1-mini", glyphYou, starterSlashWord, "recent sessions"} { + for _, row := range []string{wordmark[1], "openai/gpt-4.1-mini", glyphYou, starterSlashWord, "recent sessions"} { at := unitRow(lines, row) if at < 0 { t.Fatalf("at width %d the greeting lost %q:\n%s", width, row, strings.Join(lines, "\n")) diff --git a/internal/tui3/firstrun.go b/internal/tui3/firstrun.go index c1b7575a87..5f46c85cbf 100644 --- a/internal/tui3/firstrun.go +++ b/internal/tui3/firstrun.go @@ -924,7 +924,7 @@ func setupTitle(s *setupFlow) string { // is answered before anything is asked. // // AND EVERY ONE OF THEM NAMES THE PRODUCT FROM [product] AND NEVER FROM A -// LITERAL. The wordmark three rows above this prose is drawn from that same +// LITERAL. The wordmark above this prose is drawn from that same // constant, and when the two were spelled separately the first screen anybody // ever sees said one name in the letterforms and a different one in the // sentence three rows under them. diff --git a/internal/tui3/productname_test.go b/internal/tui3/productname_test.go index 01f7198547..816c4ffa12 100644 --- a/internal/tui3/productname_test.go +++ b/internal/tui3/productname_test.go @@ -24,7 +24,7 @@ func TestTheProductIsNamedOnceAndItIsTheNameYouType(t *testing.T) { // TestTheWordmarkCanSpellTheProductsWholeName is the gate the letterforms need: // [wordmarkRows] SKIPS a letter it has no glyph for, so a missing form is not a -// build error — it is the product's name with a hole in it, three rows tall, on +// build error — it is the product's name with a hole in it, four rows tall, on // the first screen anybody sees. func TestTheWordmarkCanSpellTheProductsWholeName(t *testing.T) { for _, letter := range product { @@ -33,8 +33,8 @@ func TestTheWordmarkCanSpellTheProductsWholeName(t *testing.T) { } } rows := wordmarkRows(false) - if len(rows) != 3 { - t.Fatalf("the wordmark is %d rows, want 3", len(rows)) + if len(rows) != 4 { + t.Fatalf("the wordmark is %d rows, want 4", len(rows)) } // Three cells a letter and one between them, which is what says every letter // of the name is on the row rather than most of them. @@ -99,8 +99,8 @@ func drawnWordmark(t *testing.T) string { // strokes, which is what this asserts and nothing more. func TestTheWordmarksRightEdgeIsNeverAHoleBetweenTwoStrokes(t *testing.T) { rows := wordmarkRows(false) - if len(rows) < 3 { - t.Fatalf("the wordmark is %d rows, want at least 3", len(rows)) + if len(rows) < 4 { + t.Fatalf("the wordmark is %d rows, want at least 4", len(rows)) } edge := make([]rune, len(rows)) for i, row := range rows { @@ -117,9 +117,10 @@ func TestTheWordmarksRightEdgeIsNeverAHoleBetweenTwoStrokes(t *testing.T) { r, string(edge[r-1]), string(edge[r+1]), strings.Join(rows, "\n")) } } - // AND THE EDGE IS STILL A LETTER AND NOT A BOX. `a` closes its crossbar with - // `┤` and `e` must not, or the two letters of this name that differ in one - // cell stop differing at all. + // AND THE EDGE IS STILL A LETTER AND NOT A BOX. `e` closes its crossbar with + // the half-stroke `╴` and stands on a bowl; `a` closes its own with `┤`, + // stands on two legs and starts a row higher. Respell either into the other + // and this name has one letterform where it needs two. if wordmarkGlyphs['e'] == wordmarkGlyphs['a'] { t.Fatalf("`e` and `a` are now the same letterform %v — the name would read %q", wordmarkGlyphs['e'], strings.ReplaceAll(product, "e", "a")) } diff --git a/internal/tui3/welcome.go b/internal/tui3/welcome.go index d8d0abe67a..6380ec6c49 100644 --- a/internal/tui3/welcome.go +++ b/internal/tui3/welcome.go @@ -626,25 +626,49 @@ func (a *app) welcomePress(slot int) tea.Cmd { // ── the drawing ───────────────────────────────────────────────────────────── -// The wordmark, three rows of it, and it MUST HOLD A LETTERFORM FOR EVERY LETTER +// The wordmark, four rows of it, and it MUST HOLD A LETTERFORM FOR EVERY LETTER // OF [product] — [wordmarkRows] skips a letter it has never heard of, so a // missing glyph is not a build error, it is a word with a hole in it on the // first screen of a fresh install (TestTheWordmarkCanSpellTheProductsWholeName // is what holds the two together). // +// THE WORD IS SPELLED `codeaf` AND DRAWN `CodeAF`. The table is keyed by the +// letters of [product], which is the one spelling every sentence uses, but the +// forms under `a` and `f` are capitals: the owner wants the two letters at the +// end to stand up out of the word (2026-09-17), and a letterform table is the +// one place a drawing may differ from the text without the text changing. The +// terminal that gets the word instead of the drawing still gets `codeaf`. +// +// THE TOP ROW IS THE CAP LINE AND THE BASELINE IS ONE LINE, and both come from +// one fact about box-drawing: a bar sits at the MIDDLE of its cell and only a +// vertical reaches a cell's edge. So a letter whose top is a bar (`c`, `o`, +// `e`, the capitals) tops out half a row below a letter whose top is a stem, +// and a letter whose foot is a stem hangs half a row under one whose foot is +// a bowl. With three rows that put the `d`'s ascender half a row over the +// capitals' tops, which is the one thing a capital is not allowed to be. Now +// the x-height letters live in the lower three rows and leave the top row +// blank; the capitals and the ascender start in the top row, the ascender as +// the half-stroke `╷` so its top is the capitals' bar and not the edge above +// it; and every stem that ends on the baseline ends as the half-stroke `╵`, +// which stops where a bowl's `└─┘` does. A `│` in the bottom row is a +// DESCENDER and nothing else. +// // It is drawn from box-drawing characters rather than from a figlet font because // a figlet wordmark is nine rows of hash marks and this surface owns two: the // letterform here is the same vocabulary the rail and the rules are drawn in, // which is the whole reason it reads as part of the surface rather than as // something pasted onto it. -var wordmarkGlyphs = map[rune][3]string{ - // The bowl open on the right, on the stem `f` and `r` are drawn with. - 'c': {"┌─ ", "│ ", "└─ "}, - 'o': {"┌─┐", "│ │", "└─┘"}, - // The bowl of an `o` with the ascender on its right, which is the one - // letter of this name that rises above the others. - 'd': {" │", "┌─┤", "└─┘"}, - 'p': {"┌─┐", "├─┘", "│ "}, +var wordmarkGlyphs = map[rune][4]string{ + // The bowl open on the right, on the stem `r` is drawn with. + 'c': {" ", "┌─ ", "│ ", "└─ "}, + 'o': {" ", "┌─┐", "│ │", "└─┘"}, + // The bowl of an `o` with the ascender on its right: the one lowercase + // letter of this name that rises to the cap line, and it rises to it and + // not past it, because `╷` starts at the middle of its cell where the + // capitals' top bars are, and `│` would start at the edge half a row over + // them — which is the discrepancy the fourth row exists to remove. + 'd': {" ╷", "┌─┤", "│ │", "└─┘"}, + 'p': {" ", "┌─┐", "├─┘", "│ "}, // THE CROSSBAR ENDS IN A TERMINAL AND NOT IN A BLANK. `e` sits between two // closed letterforms, and its right column used to be `├─ `, a blank cell // with the bowl's `┐` directly above it and its `┘` directly below. A hole @@ -652,35 +676,51 @@ var wordmarkGlyphs = map[rune][3]string{ // as an open letterform; it reads as a word the terminal cut off, which is // what the wave that found this filed it as. The half-stroke closes that // cell while keeping the aperture a lowercase `e` has and `a` (`├─┤`) has - // not — the one cell that tells those two letters apart here. `c` and `f` - // need no such closing: their right column is blank on all three rows, so - // the eye reads a letter that ends rather than a stroke that is missing. - 'e': {"┌─┐", "├─╴", "└─┘"}, - 'n': {"┌─┐", "│ │", "│ │"}, - 'a': {"┌─┐", "├─┤", "└─┘"}, - 'f': {"┌─ ", "├─ ", "│ "}, - // The shoulder alone, on the stem every other ascender here is drawn with. - 'r': {"┌─┐", "│ ", "│ "}, - // The bowl of an `a` with the tail under it, which is the one letter of this - // name that hangs below the line. - 'g': {"┌─┐", "└─┤", "└─┘"}, + // not — the one cell that tells those two letters apart here. `c` needs no + // such closing: its right column is blank on all three rows, so the eye + // reads a letter that ends rather than a stroke that is missing, and `f` + // has ink in that column on its top row alone, with nothing under it to + // make a hole of the two blanks below. + 'e': {" ", "┌─┐", "├─╴", "└─┘"}, + 'n': {" ", "┌─┐", "│ │", "╵ ╵"}, + // A CAPITAL: the apex on the cap line, the crossbar a third of the way up, + // and two legs that reach the baseline apart. It used to be `┌─┐ / ├─┤ / + // └─┘`, which is not an `a` but two closed bowls of equal size stacked on + // each other, and two equal closed bowls are an `8` — which is what the + // owner read on the first screen. Opening the bottom is what makes it a + // letter, and the crossbar is what keeps it from being the `n` above. + 'a': {"┌─┐", "│ │", "├─┤", "╵ ╵"}, + // A CAPITAL, AND THE TOP ARM IS THE LONGER ONE. Both arms of the old `f` + // were two cells, which is a lowercase `f` drawn without its hook and reads + // as neither case in particular; a capital `F` is the one letter of the Latin + // alphabet whose case is told by its arms alone, the upper reaching past the + // lower, and its middle arm sits above the centre as the letter's does. That + // third cell is the only ink in the word's right-hand column, so the edge + // below it is a letter stopping and not a hole between strokes + // (TestTheWordmarksRightEdgeIsNeverAHoleBetweenTwoStrokes is the line). + 'f': {"┌──", "├─ ", "│ ", "╵ "}, + // The shoulder alone, on a stem that stops at the baseline. + 'r': {" ", "┌─┐", "│ ", "╵ "}, + // The bowl of an `o` with the tail under it, which is the one letter of this + // table that hangs below the line. + 'g': {" ", "┌─┐", "└─┤", "└─┘"}, } -// wordmarkRows is the wordmark as three unpainted rows, and the column each +// wordmarkRows is the wordmark as four unpainted rows, and the column each // letter starts on. A terminal that cannot draw the box characters gets the -// word itself — the same information, one row instead of three, and no +// word itself — the same information, one row instead of four, and no // mojibake (styles.go's [detectASCII] is the same veto the rail obeys). func wordmarkRows(ascii bool) []string { if ascii { return []string{product} } - rows := [3]string{} + rows := [4]string{} for i, letter := range product { glyph, ok := wordmarkGlyphs[letter] if !ok { continue } - for r := 0; r < 3; r++ { + for r := range rows { if i > 0 { rows[r] += " " }