diff options
| -rw-r--r-- | data/economy/journal.jsonl | 0 | ||||
| -rw-r--r-- | data/help/alias.yaml | 3 | ||||
| -rw-r--r-- | data/help/attack.yaml | 3 | ||||
| -rw-r--r-- | data/help/autotrigger.yaml | 3 | ||||
| -rw-r--r-- | data/help/color.yaml | 3 | ||||
| -rw-r--r-- | data/help/description.yaml | 3 | ||||
| -rw-r--r-- | data/help/equipment.yaml | 3 | ||||
| -rw-r--r-- | data/help/farming.yaml | 3 | ||||
| -rw-r--r-- | data/help/fishing.yaml | 3 | ||||
| -rw-r--r-- | data/help/fletching.yaml | 3 | ||||
| -rw-r--r-- | data/help/get.yaml | 4 | ||||
| -rw-r--r-- | data/help/inventory.yaml | 4 | ||||
| -rw-r--r-- | data/help/look.yaml | 3 | ||||
| -rw-r--r-- | data/help/mining.yaml | 3 | ||||
| -rw-r--r-- | data/help/mods.yaml | 3 | ||||
| -rw-r--r-- | data/help/movement.yaml | 19 | ||||
| -rw-r--r-- | data/help/newplayer.yaml | 56 | ||||
| -rw-r--r-- | data/help/option.yaml | 3 | ||||
| -rw-r--r-- | data/help/remove.yaml | 4 | ||||
| -rw-r--r-- | data/help/score.yaml | 3 | ||||
| -rw-r--r-- | data/help/talk.yaml | 4 | ||||
| -rw-r--r-- | data/help/use.yaml | 4 | ||||
| -rw-r--r-- | data/help/wear.yaml | 3 | ||||
| -rw-r--r-- | data/help/woodcutting.yaml | 4 | ||||
| -rw-r--r-- | internal/game/render_help.go | 91 | ||||
| -rw-r--r-- | internal/game/render_help_test.go | 81 | ||||
| -rw-r--r-- | internal/ui/table.go | 107 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 91 |
28 files changed, 486 insertions, 28 deletions
diff --git a/data/economy/journal.jsonl b/data/economy/journal.jsonl new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/data/economy/journal.jsonl diff --git a/data/help/alias.yaml b/data/help/alias.yaml index 4be2f98..2caf8ed 100644 --- a/data/help/alias.yaml +++ b/data/help/alias.yaml @@ -13,3 +13,6 @@ description: | Aliases resolve before built-in commands. An alias can shadow an existing command name. + +aliases: + - "unalias" diff --git a/data/help/attack.yaml b/data/help/attack.yaml index 5873aaa..9cf32ff 100644 --- a/data/help/attack.yaml +++ b/data/help/attack.yaml @@ -34,3 +34,6 @@ description: | base_level + 9 (science, no style bonus) See 'combat', 'stats', 'style', and 'science' for details. + +aliases: + - "kill" diff --git a/data/help/autotrigger.yaml b/data/help/autotrigger.yaml index 08ca57e..25582aa 100644 --- a/data/help/autotrigger.yaml +++ b/data/help/autotrigger.yaml @@ -28,3 +28,6 @@ description: | autotrigger none - Disable autotrigger See also: trigger, mods, science + +aliases: + - "auto" diff --git a/data/help/color.yaml b/data/help/color.yaml index 82e8fed..87148c3 100644 --- a/data/help/color.yaml +++ b/data/help/color.yaml @@ -104,3 +104,6 @@ description: | You can use color blocks in your prompt: prompt {02 bold}Ready>{/} %h/%Hhp See 'help prompt' for full prompt customization. + +aliases: + - "colors" diff --git a/data/help/description.yaml b/data/help/description.yaml index 3436c5b..d527307 100644 --- a/data/help/description.yaml +++ b/data/help/description.yaml @@ -10,3 +10,6 @@ description: | room look output including description, mobs, objects, items, and exits. When disabled, only the room name is shown on move. + +aliases: + - "desc" diff --git a/data/help/equipment.yaml b/data/help/equipment.yaml index 3c0d70c..14bcb67 100644 --- a/data/help/equipment.yaml +++ b/data/help/equipment.yaml @@ -11,3 +11,6 @@ description: | Shows what you have in each slot (main hand, head, torso, etc.). Use "remove <item>" to unequip. + +aliases: + - "eq" diff --git a/data/help/farming.yaml b/data/help/farming.yaml index 2dd2ff2..ca281a8 100644 --- a/data/help/farming.yaml +++ b/data/help/farming.yaml @@ -29,3 +29,6 @@ description: | See also: help plant, help harvest, help rake, help water, help cure, help inspect + +aliases: + - "farm" diff --git a/data/help/fishing.yaml b/data/help/fishing.yaml index 61d7309..7f47756 100644 --- a/data/help/fishing.yaml +++ b/data/help/fishing.yaml @@ -20,3 +20,6 @@ description: | Look at a spot to see what equipment it needs. Enable the "xpdrops" toggle to see XP gains with each catch. + +aliases: + - "fish" diff --git a/data/help/fletching.yaml b/data/help/fletching.yaml index b2fe254..c347954 100644 --- a/data/help/fletching.yaml +++ b/data/help/fletching.yaml @@ -25,3 +25,6 @@ description: | wear arrows, wear 100 arrows, remove arrows, remove 100 arrows See also: fletch, use, wear, remove + +aliases: + - "fletch" diff --git a/data/help/get.yaml b/data/help/get.yaml index 777541b..a40affa 100644 --- a/data/help/get.yaml +++ b/data/help/get.yaml @@ -10,3 +10,7 @@ description: | added to your credit balance and never take inventory space. Picking up an item cancels any active gathering or crafting action. + +aliases: + - "take" + - "grab" diff --git a/data/help/inventory.yaml b/data/help/inventory.yaml index 7a60650..73a9999 100644 --- a/data/help/inventory.yaml +++ b/data/help/inventory.yaml @@ -6,3 +6,7 @@ description: | Usage: inventory, i, inv Shows all 28 inventory slots and any equipped items. + +aliases: + - "i" + - "inv" diff --git a/data/help/look.yaml b/data/help/look.yaml index d547bb6..2ce3d5f 100644 --- a/data/help/look.yaml +++ b/data/help/look.yaml @@ -28,3 +28,6 @@ description: | - Directions: shows the adjacent room as if you were there Use "toggle depletion" to show depletion and despawn timers on objects. + +aliases: + - "l" diff --git a/data/help/mining.yaml b/data/help/mining.yaml index 231027f..7eecfd6 100644 --- a/data/help/mining.yaml +++ b/data/help/mining.yaml @@ -19,3 +19,6 @@ description: | Use numbered targeting (e.g. "mine 2.rock") when multiple rocks of the same type are present. "look rock" shows which rocks are depleted. Use "toggle depletion" to see exact respawn timers. + +aliases: + - "mine" diff --git a/data/help/mods.yaml b/data/help/mods.yaml index d545b54..71b8abc 100644 --- a/data/help/mods.yaml +++ b/data/help/mods.yaml @@ -20,3 +20,6 @@ description: | Aliases: mods, mod, module, modules See also: trigger, autotrigger, science + +aliases: + - "modlist" diff --git a/data/help/movement.yaml b/data/help/movement.yaml index c9bc792..1b684dc 100644 --- a/data/help/movement.yaml +++ b/data/help/movement.yaml @@ -57,4 +57,21 @@ description: | Dying refills your run energy to full (alongside your HP). related: - walk - - agility
\ No newline at end of file + - agility +aliases: + - "north" + - "south" + - "east" + - "west" + - "up" + - "down" + - "ne" + - "nw" + - "se" + - "sw" + - "n" + - "s" + - "e" + - "w" + - "u" + - "d" diff --git a/data/help/newplayer.yaml b/data/help/newplayer.yaml new file mode 100644 index 0000000..5f0eb17 --- /dev/null +++ b/data/help/newplayer.yaml @@ -0,0 +1,56 @@ +name: "newplayer" +aliases: + - "new player" + - "getting started" + - "guide" + - "basics" +category: "Getting Started" +description: | + Welcome to The House of Icarus! This guide covers the basics. + + Getting around + Move with north, south, east, west, up, down (or the shortcuts + n, s, e, w, u, d). "exits" lists the ways out of a room, "map" + draws a local map, and "walk <room>" auto-pathfinds for you. + Movement uses run energy; see 'help movement' for the details. + + Looking around + "look" describes the room, "look <thing>" examines an item, + object, or mob. Use numbers to pick between identical targets, + e.g. "look 2.rock". + + Your character + "score" shows your stats, skills, and combat level. "inventory" + (i) shows your 28-slot pack, and "equipment" (eq) shows what + you're wearing and wielding. "stats" lists your combat bonuses. + + Items + "get <item>" picks things up ("get all" grabs everything), + "drop <item>" puts them down, "wear <item>" and "wield <item>" + equip gear, "remove <item>" unequips it. "eat <food>" and + "drink <potion>" restore hitpoints. "use <item> on <object>" + interacts with the world. + + Skills + Gathering: mine (Mining), fish (Fishing), chop (Woodcutting), + farm (Farming), search (Scavenging) + Crafting: cook, smelt, smith, craft, fletch, mix, construct + Most skills need the right tool, either wielded or in your pack. + Many actions take a few ticks, so you can queue several commands. + + Combat + "attack <mob>" starts a fight (e.g. "attack man" or "attack 1.man"). + Combat runs automatically on the tick; "style" changes whether you + train accuracy, strength, or defense. To flee, move in any + direction - see 'help combat' for the rules. + + Talking + "talk <mob>" starts an NPC conversation (pick numbered options; + type "bye" to leave). "say <msg>" chats to your room and + "global <msg>" to everyone online. + + Tips + "help <command>" shows detailed usage of any command. + "options" tweaks display settings (width, colors, unicode, map). + "help <topic>" pages include a "related:" list of nearby topics - + follow them as you explore. diff --git a/data/help/option.yaml b/data/help/option.yaml index 75b1794..0eb871a 100644 --- a/data/help/option.yaml +++ b/data/help/option.yaml @@ -19,3 +19,6 @@ description: | Use help <option> for details on a specific option. Use 'prompt' to set a custom per-character prompt. + +aliases: + - "options" diff --git a/data/help/remove.yaml b/data/help/remove.yaml index cce21f4..b44630d 100644 --- a/data/help/remove.yaml +++ b/data/help/remove.yaml @@ -13,3 +13,7 @@ description: | enough inventory space for everything, nothing is removed. Examples: remove axe, remove main_hand, remove all + +aliases: + - "unwear" + - "unwield" diff --git a/data/help/score.yaml b/data/help/score.yaml index cacf0ba..df8040d 100644 --- a/data/help/score.yaml +++ b/data/help/score.yaml @@ -10,3 +10,6 @@ description: | The vitals block includes three bars: HP (red/green/yellow), BAT (battery, blue), and ENG (run energy, green). Run energy maximum is 3x your Agility level; see 'help movement' for run energy rules. + +aliases: + - "sc" diff --git a/data/help/talk.yaml b/data/help/talk.yaml index 4a59f68..3ac60ef 100644 --- a/data/help/talk.yaml +++ b/data/help/talk.yaml @@ -9,3 +9,7 @@ description: | choices. Type the number to select an option. Type "bye", "exit", or "end" to leave the conversation at any time. + +aliases: + - "speak" + - "ask" diff --git a/data/help/use.yaml b/data/help/use.yaml index d9bcfd4..34f5a4b 100644 --- a/data/help/use.yaml +++ b/data/help/use.yaml @@ -36,3 +36,7 @@ description: | push gate Push a gate open (shortcut for use) See also: cook, smelt, smith, recipe + +aliases: + - "pull" + - "push" diff --git a/data/help/wear.yaml b/data/help/wear.yaml index 2a92fbd..72d7e64 100644 --- a/data/help/wear.yaml +++ b/data/help/wear.yaml @@ -6,3 +6,6 @@ description: | Usage: wear <item> Equip an item from inventory wear all Auto-equip best items per slot wear View equipped items + +aliases: + - "wield" diff --git a/data/help/woodcutting.yaml b/data/help/woodcutting.yaml index 80179a9..2ce502f 100644 --- a/data/help/woodcutting.yaml +++ b/data/help/woodcutting.yaml @@ -20,3 +20,7 @@ description: | nest alongside the logs. Use "search nest" to open a nest for loot. Use the "depletion" toggle to see timers on trees and rocks. + +aliases: + - "chop" + - "cut" diff --git a/internal/game/render_help.go b/internal/game/render_help.go index e2893b5..2d3a304 100644 --- a/internal/game/render_help.go +++ b/internal/game/render_help.go @@ -13,9 +13,11 @@ import ( ) type HelpDef struct { - Name string `yaml:"name"` - Category string `yaml:"category"` - Content string `yaml:"description"` + Name string `yaml:"name"` + Aliases []string `yaml:"aliases"` + Related []string `yaml:"related"` + Category string `yaml:"category"` + Content string `yaml:"description"` } type cmdEntry struct { @@ -63,7 +65,7 @@ var commandList = []cmdEntry{ {"mine", "Active", "Mine rocks (Mining)"}, {"mix", "Active", "Mix potions (Pharmacy)"}, {"mods / modlist", "Instant", "List available science modules"}, - {"north / south / east / west / ne / nw / se / sw / up / down", "Active", "Move in a direction"}, + {"north / south / east / west / up / down", "Active", "Move in a direction"}, {"option / options", "Instant", "View or change settings"}, {"prompt", "Instant", "Set custom command prompt"}, {"pull / push", "Active", "Interact with objects"}, @@ -94,16 +96,56 @@ func LoadHelp(dataDir string) ([]HelpDef, error) { dir := filepath.Join(dataDir, "help") var helps []HelpDef behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { - var h HelpDef - if err := yaml.Unmarshal(data, &h); err != nil { + h := parseHelpDef(data) + if h == nil { return nil } - helps = append(helps, h) + helps = append(helps, *h) return nil }) return helps, nil } +// helpRaw accepts both the canonical schema (name/description) and the +// legacy variant (title/body) used by a few help files. +type helpRaw struct { + Name string `yaml:"name"` + Title string `yaml:"title"` + Aliases []string `yaml:"aliases"` + Related []string `yaml:"related"` + Category string `yaml:"category"` + Description string `yaml:"description"` + Body string `yaml:"body"` +} + +// parseHelpDef unmarshals a help definition, normalizing legacy title/body +// files onto the canonical HelpDef. Returns nil for files that are not help +// topics (no name at all). +func parseHelpDef(data []byte) *HelpDef { + var raw helpRaw + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil + } + name := raw.Name + if name == "" { + name = raw.Title + } + if name == "" { + return nil + } + content := raw.Description + if content == "" { + content = raw.Body + } + return &HelpDef{ + Name: name, + Aliases: raw.Aliases, + Related: raw.Related, + Category: raw.Category, + Content: content, + } +} + func (g *Game) doHelp(sess *net.Session, topic string) { if topic == "" { unicode := true @@ -141,16 +183,47 @@ func (g *Game) doHelp(sess *net.Session, topic string) { return } - topic = strings.ToLower(topic) + topic = strings.ToLower(strings.TrimSpace(topic)) for _, h := range helps { - if strings.ToLower(h.Name) == topic { + if matchesHelpTopic(h, topic) { sess.WriteLine(fmt.Sprintf(" %s", h.Content)) + if len(h.Related) > 0 { + sess.WriteLine("") + sess.WriteLine(fmt.Sprintf(" Related topics: %s", strings.Join(h.Related, ", "))) + } return } } + + // No dedicated help page. If it's a known command, at least show its + // summary from the command table instead of a dead end. + for _, c := range commandList { + for _, name := range strings.Split(c.Name, " / ") { + if strings.ToLower(strings.TrimSpace(name)) == topic { + sess.WriteLine(fmt.Sprintf(" %s - %s", c.Name, c.Desc)) + sess.WriteLine("") + sess.WriteLine(" (No detailed help page for this command yet.)") + return + } + } + } sess.WriteLine(fmt.Sprintf("No help found for '%s'.", topic)) } +// matchesHelpTopic reports whether the help entry answers the given topic, +// matching its name or any declared alias (case-insensitive). +func matchesHelpTopic(h HelpDef, topic string) bool { + if strings.EqualFold(h.Name, topic) { + return true + } + for _, a := range h.Aliases { + if strings.EqualFold(strings.TrimSpace(a), topic) { + return true + } + } + return false +} + func firstLine(s string) string { if idx := strings.Index(s, "\n"); idx >= 0 { return s[:idx] diff --git a/internal/game/render_help_test.go b/internal/game/render_help_test.go new file mode 100644 index 0000000..b8ad614 --- /dev/null +++ b/internal/game/render_help_test.go @@ -0,0 +1,81 @@ +package game + +import ( + "strings" + "testing" +) + +func loadHelpOrSkip(t *testing.T) []HelpDef { + t.Helper() + helps, err := LoadHelp("../../data") + if err != nil { + t.Fatal(err) + } + if len(helps) == 0 { + t.Fatal("no help topics loaded") + } + return helps +} + +func TestLoadHelpAllFilesParse(t *testing.T) { + helps := loadHelpOrSkip(t) + seen := map[string]bool{} + for _, h := range helps { + if strings.TrimSpace(h.Name) == "" { + t.Errorf("help entry with empty name: %q", h.Content) + } + if seen[h.Name] { + t.Errorf("duplicate help topic %q", h.Name) + } + seen[h.Name] = true + } +} + +func TestNewPlayerHelpExists(t *testing.T) { + helps := loadHelpOrSkip(t) + found := false + for _, h := range helps { + if h.Name == "newplayer" { + found = true + } + } + if !found { + t.Fatal("help topic 'newplayer' missing (referenced by intro room)") + } +} + +func TestHelpAliasesResolveWithoutCollision(t *testing.T) { + helps := loadHelpOrSkip(t) + owner := map[string]string{} + for _, h := range helps { + for _, a := range h.Aliases { + a = strings.ToLower(strings.TrimSpace(a)) + if a == "" { + continue + } + if prev, dup := owner[a]; dup { + t.Errorf("alias %q claimed by both %q and %q", a, prev, h.Name) + } + owner[a] = h.Name + } + } + for topic, want := range map[string]string{ + "mine": "mining", + "chop": "woodcutting", + "fish": "fishing", + "fletch": "fletch", + "newplayer": "newplayer", + "new player": "newplayer", + } { + resolved := "" + for _, h := range helps { + if matchesHelpTopic(h, topic) { + resolved = h.Name + break + } + } + if resolved != want { + t.Errorf("help %q resolved to %q, want %q", topic, resolved, want) + } + } +} diff --git a/internal/ui/table.go b/internal/ui/table.go index 5ef2c99..e85e505 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -38,6 +38,68 @@ type Table struct { Rows [][]string } +// rebalanceWidths shrinks column widths in place so their sum fits budget. +// Rather than crushing only the last column, every column wider than the +// fair share (budget/nCols) is capped first, then columns are trimmed one +// rune at a time from the widest until the table fits. Leftover slack is +// handed back round-robin to columns that lost width so wide layouts still +// use the full line. +func rebalanceWidths(colWidths []int, budget int) { + original := make([]int, len(colWidths)) + copy(original, colWidths) + + fair := budget / len(colWidths) + if fair < 1 { + fair = 1 + } + for i, w := range colWidths { + if w > fair { + colWidths[i] = fair + } + } + + sum := func() int { + t := 0 + for _, w := range colWidths { + t += w + } + return t + } + + // Tight tables: trim the widest column until we fit (or hit 1). + for sum() > budget { + widest := 0 + for i, w := range colWidths { + if w > colWidths[widest] { + widest = i + } + } + if colWidths[widest] <= 1 { + break + } + colWidths[widest]-- + } + + // Give unused width back to the columns that were reduced. + for { + extra := budget - sum() + if extra <= 0 { + break + } + progress := false + for i := 0; i < len(colWidths) && extra > 0; i++ { + if colWidths[i] < original[i] { + colWidths[i]++ + extra-- + progress = true + } + } + if !progress { + break + } + } +} + func (t *Table) Render(unicode bool, maxWidth int) []string { g := tableGlyphSet(unicode) @@ -65,29 +127,38 @@ func (t *Table) Render(unicode bool, maxWidth int) []string { } } + headerCells := t.Columns + bodyRows := t.Rows + if maxWidth > 0 { + // Cell padding: one space on each side of every cell, plus one + // separator between columns. overhead := 3*nCols + 1 - total := overhead + budget := maxWidth - overhead + if budget < nCols { + budget = nCols + } + + total := 0 for _, w := range colWidths { total += w } - if total > maxWidth { - last := nCols - 1 - budget := maxWidth - overhead - for i := 0; i < last; i++ { - budget -= colWidths[i] - } - if budget < 5 { - budget = 5 - } - colWidths[last] = budget - - if len(t.Columns) > last { - t.Columns[last] = color.TruncateVisible(t.Columns[last], budget, unicode) + if total > budget { + rebalanceWidths(colWidths, budget) + + // Truncate every cell to its (possibly reduced) column width. + // Work on copies so rendering never mutates the table. + headerCells = make([]string, len(t.Columns)) + copy(headerCells, t.Columns) + for i := range headerCells { + headerCells[i] = color.TruncateVisible(headerCells[i], colWidths[i], unicode) } + bodyRows = make([][]string, len(t.Rows)) for i := range t.Rows { - if len(t.Rows[i]) > last { - t.Rows[i][last] = color.TruncateVisible(t.Rows[i][last], budget, unicode) + bodyRows[i] = make([]string, len(t.Rows[i])) + copy(bodyRows[i], t.Rows[i]) + for j := 0; j < len(bodyRows[i]) && j < nCols; j++ { + bodyRows[i][j] = color.TruncateVisible(bodyRows[i][j], colWidths[j], unicode) } } } @@ -164,11 +235,11 @@ func (t *Table) Render(unicode bool, maxWidth int) []string { } if hasCols { - out = append(out, makeRow(t.Columns)) + out = append(out, makeRow(headerCells)) out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) } - for _, row := range t.Rows { + for _, row := range bodyRows { out = append(out, makeRow(row)) } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go new file mode 100644 index 0000000..68b6d32 --- /dev/null +++ b/internal/ui/table_test.go @@ -0,0 +1,91 @@ +package ui + +import ( + "strings" + "testing" + + "thehouseoficarus/internal/color" +) + +func renderWidths(t *testing.T, tt *Table, unicode bool, maxWidth int) []string { + t.Helper() + lines := tt.Render(unicode, maxWidth) + for _, l := range lines { + if vl := color.VisibleLen(l); maxWidth > 0 && vl > maxWidth+1 { + t.Errorf("line exceeds maxWidth %d: %d (%q)", maxWidth, vl, l) + } + } + // All border/content lines must be the same visible width so the box + // lines up. + widths := map[int]bool{} + for _, l := range lines { + widths[color.VisibleLen(l)] = true + } + if len(widths) > 1 { + var ws []string + for w := range widths { + ws = append(ws, string(rune('0'+w))) + } + t.Errorf("inconsistent line widths: %v", strings.Join(ws, ",")) + } + return lines +} + +func TestTableFitsWithinWidth(t *testing.T) { + tt := &Table{ + Title: "Commands", + Columns: []string{"Command", "Type", "Description"}, + Rows: [][]string{ + {"north / south / east / west / ne / nw / se / sw / up / down", "Active", "Move in a direction"}, + {"bet", "Active", "Place a casino wager (e.g. bet 100 on banker)"}, + {"attack / kill", "Active", "Attack a mob"}, + }, + } + for _, w := range []int{0, 40, 80, 100} { + lines := renderWidths(t, tt, true, w) + // Description column must never be crushed: with three columns the + // description always keeps at least a fair share of the budget. + if w == 80 { + // lines: border, title, sep, header, sep, rows... + if !strings.Contains(lines[5], "Move i") { + t.Errorf("description column crushed at width 80: %q", lines[5]) + } + } + } +} + +func TestTableNoMutation(t *testing.T) { + tt := &Table{ + Title: "T", + Columns: []string{"A", "B"}, + Rows: [][]string{{"very long cell content here", "x"}}, + } + before := tt.Columns[0] + _ = tt.Render(true, 20) + if tt.Columns[0] != before { + t.Errorf("Render mutated table columns: %q", tt.Columns[0]) + } +} + +func TestTableASCIIFallback(t *testing.T) { + tt := &Table{Title: "T", Columns: []string{"A", "B"}, Rows: [][]string{{"1", "2"}}} + lines := renderWidths(t, tt, false, 0) + if !strings.Contains(lines[0], "+") { + t.Errorf("expected ASCII border, got %q", lines[0]) + } +} + +func TestTableColoredCellWidth(t *testing.T) { + // ANSI-colored cells must be measured and padded by visible length. + red := "\x1b[31m" + reset := "\x1b[0m" + tt := &Table{Columns: []string{"A", "B"}, Rows: [][]string{{red + "hi" + reset, "x"}}} + lines := renderWidths(t, tt, true, 0) + // lines: top border, header, sep, data row, bottom border + if len(lines) < 5 { + t.Fatalf("expected 5 lines, got %d: %q", len(lines), lines) + } + if !strings.Contains(lines[3], "hi") || !strings.Contains(lines[3], "x") { + t.Errorf("unexpected row: %q", lines[3]) + } +} |
