From 12ed73f9ff9de1a145683de8a73c170613371979 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 29 Jun 2026 01:41:49 -0400 Subject: wrap_width and table improvements, targeting 80 character default width --- internal/color/color.go | 31 +++++++++++++++++++++++++++++++ internal/game/cmd_aps.go | 2 +- internal/game/cmd_bank.go | 2 +- internal/game/cmd_color.go | 2 +- internal/game/cmd_look.go | 24 ++++++++++++++++++------ internal/game/cmd_mods.go | 2 +- internal/game/cmd_option.go | 2 +- internal/game/cmd_shop.go | 4 +++- internal/game/cmd_skills.go | 2 +- internal/game/cmd_tech.go | 2 +- internal/game/look_room.go | 3 +-- internal/game/production_menu.go | 4 ++-- internal/game/render_help.go | 4 +++- internal/player/player.go | 3 +-- internal/ui/table.go | 30 +++++++++++++++++++++++++++++- 15 files changed, 95 insertions(+), 22 deletions(-) (limited to 'internal') diff --git a/internal/color/color.go b/internal/color/color.go index e971382..5a1aa54 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -180,6 +180,37 @@ func VisibleLen(s string) int { return utf8.RuneCountInString(ansiRe.ReplaceAllString(s, "")) } +// TruncateVisible truncates s to at most maxVisible visible characters, +// preserving ANSI SGR codes. Assumes cells follow the common pattern +// "\x1b[...mTEXT\x1b[0m" or are plain text. Appends "..." when truncated. +func TruncateVisible(s string, maxVisible int) string { + if maxVisible < 3 { + maxVisible = 3 + } + if VisibleLen(s) <= maxVisible { + return s + } + var pre, suf string + rest := s + if idx := strings.IndexByte(rest, 'm'); idx >= 0 && strings.HasPrefix(rest, "\x1b[") { + pre = rest[:idx+1] + rest = rest[idx+1:] + } + if strings.HasSuffix(rest, Reset) { + suf = Reset + rest = rest[:len(rest)-len(Reset)] + } + runes := []rune(rest) + budget := maxVisible - 3 + if budget < 0 { + budget = 0 + } + if len(runes) > budget { + rest = string(runes[:budget]) + "..." + } + return pre + rest + suf +} + // WrapANSI wraps text to the given visible width, ignoring ANSI color codes // when measuring. Each input line (split on "\n") is handled on its own: a line // that already fits is returned untouched, so indentation and aligned UI are diff --git a/internal/game/cmd_aps.go b/internal/game/cmd_aps.go index 5e209a6..a5cff78 100644 --- a/internal/game/cmd_aps.go +++ b/internal/game/cmd_aps.go @@ -148,7 +148,7 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int) Rows: rows, } - for _, line := range t.Render(p.OptionBool("unicode")) { + for _, line := range t.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { sess.WriteLine(line) } } diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go index c489ad8..bbf600f 100644 --- a/internal/game/cmd_bank.go +++ b/internal/game/cmd_bank.go @@ -110,7 +110,7 @@ func (g *Game) showBankBrowse(sess *net.Session) { }) } - for _, line := range t.Render(unicode) { + for _, line := range t.Render(unicode, p.OptionInt("wrap_width")) { sess.WriteLine(line) } } diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 8e90f02..00b4ce8 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -193,7 +193,7 @@ func showColorTable(g *Game, sess *net.Session) { color.Render(mode, color.Parse("F3"), source), }) } - for _, line := range table.Render(p.OptionBool("unicode")) { + for _, line := range table.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { sess.WriteLine(line) } } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index a1ad708..c34052a 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -22,22 +22,34 @@ func (g *Game) doLook(sess *net.Session) { g.showRoomName(sess, p, room) + wrapWidth := p.OptionInt("wrap_width") + if wrapWidth < 80 { + wrapWidth = 80 + } + var mapLines []string - if p.OptionString("tiny_map") != "off" { + mapOn := p.OptionString("tiny_map") != "off" + if mapOn { mapLines = buildTinyMap(g, sess, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode"))) if len(mapLines) != 7 { mapLines = nil + mapOn = false } } + descWidth := wrapWidth + if mapOn { + descWidth = wrapWidth - 9 + } + var content []string - content = append(content, g.showRoomDescription(sess, p, room)...) + content = append(content, g.showRoomDescription(sess, p, room, descWidth)...) content = append(content, g.showRoomObjects(sess, p, room)...) content = append(content, g.showRoomMobs(sess, p, room)...) content = append(content, g.showGroundItems(sess, p, room)...) - if len(mapLines) == 7 { - g.writeLookSideBySide(sess, p, content, mapLines) + if mapOn { + g.writeLookSideBySide(sess, p, content, mapLines, descWidth) } else { for _, line := range content { sess.WriteLine(line) @@ -138,13 +150,13 @@ func wrapText(text string, width int) []string { return lines } -func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, contentLines []string, mapLines []string) { +func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, contentLines []string, mapLines []string, descWidth int) { total := len(contentLines) if len(mapLines) > total { total = len(mapLines) } leftMap := p.OptionString("tiny_map") == "left" - mapWidth := p.OptionInt("room_desc_width") + mapWidth := descWidth if mapWidth <= 0 { mapWidth = 70 } diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go index 4f3f9ff..9468e89 100644 --- a/internal/game/cmd_mods.go +++ b/internal/game/cmd_mods.go @@ -92,7 +92,7 @@ func (g *Game) doMods(sess *net.Session, showAll bool) { } } - for _, line := range t.Render(p.OptionBool("unicode")) { + for _, line := range t.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { sess.WriteLine(line) } sess.WriteLine("") diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index a2e54c2..958fc0b 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -35,7 +35,7 @@ func (g *Game) doOption(sess *net.Session, input string) { color.Render(mode, color.Parse("FC"), def.Description), }) } - for _, line := range table.Render(p.OptionBool("unicode")) { + for _, line := range table.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { sess.WriteLine(line) } return diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go index 91a11c6..69de750 100644 --- a/internal/game/cmd_shop.go +++ b/internal/game/cmd_shop.go @@ -118,8 +118,10 @@ func (g *Game) showShopBrowse(sess *net.Session) { } unicode := true + wrapWidth := 0 if p := sess.Player; p != nil { unicode = p.OptionBool("unicode") + wrapWidth = p.OptionInt("wrap_width") } t := ui.Table{ @@ -145,7 +147,7 @@ func (g *Game) showShopBrowse(sess *net.Session) { }) } - for _, line := range t.Render(unicode) { + for _, line := range t.Render(unicode, wrapWidth) { sess.WriteLine(line) } } diff --git a/internal/game/cmd_skills.go b/internal/game/cmd_skills.go index 9321b96..4aacd31 100644 --- a/internal/game/cmd_skills.go +++ b/internal/game/cmd_skills.go @@ -35,7 +35,7 @@ func (g *Game) doSkills(sess *net.Session) { color.Render(mode, color.Parse("B3"), strconv.Itoa(next)), }) } - for _, line := range t.Render(p.OptionBool("unicode")) { + for _, line := range t.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { sess.WriteLine(line) } } diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go index 8c6f14f..cb0465f 100644 --- a/internal/game/cmd_tech.go +++ b/internal/game/cmd_tech.go @@ -150,7 +150,7 @@ func (g *Game) doTechList(sess *net.Session) { }) } - for _, line := range t.Render(p.OptionBool("unicode")) { + for _, line := range t.Render(p.OptionBool("unicode"), p.OptionInt("wrap_width")) { sess.WriteLine(line) } diff --git a/internal/game/look_room.go b/internal/game/look_room.go index 585efb0..f35d564 100644 --- a/internal/game/look_room.go +++ b/internal/game/look_room.go @@ -35,8 +35,7 @@ func (g *Game) roomSymbolBracket(sess *net.Session, p *player.Player, room *worl color.Render(mode, dimSpec, "]") } -func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) []string { - descWidth := p.OptionInt("room_desc_width") +func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room, descWidth int) []string { if descWidth <= 0 { descWidth = 70 } diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go index f625d54..bc2fccc 100644 --- a/internal/game/production_menu.go +++ b/internal/game/production_menu.go @@ -169,7 +169,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items [] unicode := p.OptionBool("unicode") sess.WriteLine("") - for _, line := range tbl.Render(unicode) { + for _, line := range tbl.Render(unicode, p.OptionInt("wrap_width")) { sess.WriteLine(line) } @@ -350,7 +350,7 @@ func (g *Game) showMenuTable(sess *net.Session, title string, names []string) { } unicode := p.OptionBool("unicode") sess.WriteLine("") - for _, line := range tbl.Render(unicode) { + for _, line := range tbl.Render(unicode, p.OptionInt("wrap_width")) { sess.WriteLine(line) } } diff --git a/internal/game/render_help.go b/internal/game/render_help.go index 869d306..798eadf 100644 --- a/internal/game/render_help.go +++ b/internal/game/render_help.go @@ -100,8 +100,10 @@ func LoadHelp(dataDir string) ([]HelpDef, error) { func (g *Game) doHelp(sess *net.Session, topic string) { if topic == "" { unicode := true + wrapWidth := 0 if p := sess.Player; p != nil { unicode = p.OptionBool("unicode") + wrapWidth = p.OptionInt("wrap_width") } sess.WriteLine("") @@ -112,7 +114,7 @@ func (g *Game) doHelp(sess *net.Session, topic string) { for _, c := range commandList { t.Rows = append(t.Rows, []string{c.Name, c.Type, c.Desc}) } - for _, line := range t.Render(unicode) { + for _, line := range t.Render(unicode, wrapWidth) { sess.WriteLine(line) } sess.WriteLine("") diff --git a/internal/player/player.go b/internal/player/player.go index 845dbb0..efe13e2 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -125,8 +125,7 @@ var OptionDefs = []OptionDef{ {"map_padding", OptString, "none", []string{"none", "x", "y", "xy"}, "Map output padding mode"}, {"automap", OptBool, false, nil, "Show map automatically after moving"}, {"show_queued_cmds", OptBool, false, nil, "Show confirmation messages for queued tick actions"}, - {"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"}, - {"wrap_width", OptInt, 120, nil, "Wrap all output to this many columns (minimum 80)"}, + {"wrap_width", OptInt, 80, nil, "Wrap all output to this many columns (minimum 80)"}, {"unicode", OptBool, true, nil, "Unicode box-drawing characters"}, {"run_countdown", OptBool, false, nil, "Show countdown messages when fleeing combat"}, {"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"}, diff --git a/internal/ui/table.go b/internal/ui/table.go index 0ac41ad..5061417 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -38,7 +38,7 @@ type Table struct { Rows [][]string } -func (t *Table) Render(unicode bool) []string { +func (t *Table) Render(unicode bool, maxWidth int) []string { g := tableGlyphSet(unicode) nCols := len(t.Columns) @@ -65,6 +65,34 @@ func (t *Table) Render(unicode bool) []string { } } + if maxWidth > 0 { + overhead := 3*nCols + 1 + total := overhead + 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) + } + for i := range t.Rows { + if len(t.Rows[i]) > last { + t.Rows[i][last] = color.TruncateVisible(t.Rows[i][last], budget) + } + } + } + } + makeSep := func(left, cross, right rune, fill rune) string { var b strings.Builder b.WriteRune(left) -- cgit v1.2.3