Fix frozen string crash in new_msg; remove redundant inner titles

Bug: frozen_string_literal: true makes '' a frozen String literal.
@input << ch raised FrozenError immediately on any keystroke, causing
the TUI runner to exit. Fixed by using +'' (mutable string) for @input
everywhere it is initialised or reset.

Also removed the section header text lines that were being drawn inside
each page's content area — the page title is already embedded in the
right panel's box border, so the inner header was redundant.

Adjusted content offsets and scroll window sizes to use the reclaimed row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:02:46 +02:00
parent 5c9cb269fb
commit bbeea678cf

80
bbs.rb
View File

@@ -74,22 +74,21 @@ MAIN_TUI = BBS::TUI.define do
lines
end
def wiki_list_render(title)
text title, x: CONT_X, y: CONT_Y, style: THEME[:header]
if @items.empty?
text 'No items found.', x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
return
end
list_h = cont_h - 5
def wiki_list_render
list_h = cont_h - 3
visible = @items[@scroll, list_h] || []
if visible.empty?
text 'No items found.', x: CONT_X, y: CONT_Y, style: THEME[:normal]
else
list visible.map { |p| p.title[0, cont_w - 2] },
x: CONT_X, y: CONT_Y + 2,
x: CONT_X, y: CONT_Y,
selected: @item_sel - @scroll,
style: THEME[:normal], highlight: THEME[:selected]
if (page = @items[@item_sel])
hint = page.description.to_s.strip[0, cont_w]
text hint, x: CONT_X, y: CONT_Y + cont_h - 2, style: THEME[:normal] unless hint.empty?
end
end
text '↑↓ navigate Enter read q back', x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
end
@@ -102,14 +101,14 @@ MAIN_TUI = BBS::TUI.define do
def scroll_list_down
return if @item_sel >= @items.size - 1
@item_sel += 1
list_h = cont_h - 5
list_h = cont_h - 3
@scroll = @item_sel - list_h + 1 if @item_sel >= @scroll + list_h
end
def open_wiki_page(back:)
return unless (page = @items[@item_sel])
@detail = wordwrap(WIKI.content(page.id), cont_w)
@page_header = page.title
@page_title = page.title
@page_meta = "#{fmt_date(page.created_at)} #{WIKI.page_url(page.locale, page.path)}"
@prev_page = back
@scroll = 0
@@ -124,10 +123,9 @@ MAIN_TUI = BBS::TUI.define do
@item_sel = 0
@scroll = 0
@detail = []
@page_header = ''
@page_meta = ''
@input = ''
@status = ''
@page_meta = +''
@input = +''
@status = +''
@prev_page = nil
end
@@ -190,32 +188,32 @@ MAIN_TUI = BBS::TUI.define do
end
render do
text '── Message Board ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
(@items[@scroll, cont_h - 2] || []).each_with_index do |line, i|
text line[0, cont_w], x: CONT_X, y: CONT_Y + 2 + i
(@items[@scroll, cont_h] || []).each_with_index do |line, i|
text line[0, cont_w], x: CONT_X, y: CONT_Y + i
end
text "#{@items.size} msg(s) ↑↓ scroll q back",
x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
end
key(:up) { @scroll = [@scroll - 1, 0].max }
key(:down) { @scroll = [@scroll + 1, [@items.size - (cont_h - 2), 0].max].min }
key(:down) { @scroll = [@scroll + 1, [@items.size - cont_h, 0].max].min }
key('r') { reload }
key('q') { go :idle }
key(:escape) { go :idle }
end
page :new_msg do
enter { @page_title = 'POST MESSAGE'; @input = ''; @status = '' }
enter { @page_title = 'POST MESSAGE'; @input = +''; @status = +'' }
render do
text '── Post Message ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
text 'Type your message and press Enter. Esc cancels.',
x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
x: CONT_X, y: CONT_Y, style: THEME[:normal]
text "#{tc(:bright, @username)}: #{tc(:input, @input)}_",
x: CONT_X, y: CONT_Y + 4
text @status, x: CONT_X, y: CONT_Y + 6,
style: @status.start_with?('Sent') ? THEME[:success] : THEME[:error] unless @status.empty?
x: CONT_X, y: CONT_Y + 2
unless @status.empty?
text @status, x: CONT_X, y: CONT_Y + 4,
style: @status.start_with?('Sent') ? THEME[:success] : THEME[:error]
end
end
key(:enter) do
@@ -224,7 +222,7 @@ MAIN_TUI = BBS::TUI.define do
else
MESSAGES.append(@username, @input.strip[0...200])
@status = 'Sent!'
@input = ''
@input = +''
end
end
key(:backspace) { @input.chop! unless @input.empty? }
@@ -234,7 +232,7 @@ MAIN_TUI = BBS::TUI.define do
page :blog do
enter { @page_title = 'BLOG'; @items = WIKI.list('blog'); @item_sel = 0; @scroll = 0 }
render { wiki_list_render('── Blog Posts ──') }
render { wiki_list_render }
key(:up) { scroll_list_up }
key(:down) { scroll_list_down }
key(:enter) { open_wiki_page(back: :blog) }
@@ -245,7 +243,7 @@ MAIN_TUI = BBS::TUI.define do
page :howto do
enter { @page_title = 'HOWTO'; @items = WIKI.list('howto'); @item_sel = 0; @scroll = 0 }
render { wiki_list_render('── HowTo Guides ──') }
render { wiki_list_render }
key(:up) { scroll_list_up }
key(:down) { scroll_list_down }
key(:enter) { open_wiki_page(back: :howto) }
@@ -256,17 +254,16 @@ MAIN_TUI = BBS::TUI.define do
page :page_view do
render do
text @page_header[0, cont_w], x: CONT_X, y: CONT_Y, style: THEME[:bright]
text @page_meta[0, cont_w], x: CONT_X, y: CONT_Y + 1, style: THEME[:normal]
(@detail[@scroll, cont_h - 3] || []).each_with_index do |l, i|
text l[0, cont_w], x: CONT_X, y: CONT_Y + 3 + i, style: THEME[:normal]
text @page_meta[0, cont_w], x: CONT_X, y: CONT_Y, style: THEME[:normal]
(@detail[@scroll, cont_h - 2] || []).each_with_index do |l, i|
text l[0, cont_w], x: CONT_X, y: CONT_Y + 2 + i, style: THEME[:normal]
end
text "#{@scroll + 1}/#{@detail.size} ↑↓ scroll q back",
x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
end
key(:up) { @scroll = [@scroll - 1, 0].max }
key(:down) { @scroll = [@scroll + 1, [@detail.size - (cont_h - 3), 0].max].min }
key(:down) { @scroll = [@scroll + 1, [@detail.size - (cont_h - 2), 0].max].min }
key('q') { @scroll = 0; go(@prev_page || :idle) }
key(:escape) { @scroll = 0; go(@prev_page || :idle) }
end
@@ -275,24 +272,23 @@ MAIN_TUI = BBS::TUI.define do
enter { @page_title = 'GAME CATALOG'; @items = CATALOG.fetch; @item_sel = 0 }
render do
text '── Game Catalog ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
if @items.empty?
text 'No games found.', x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
text 'No games found.', x: CONT_X, y: CONT_Y, style: THEME[:normal]
else
game = @items[@item_sel]
text "#{@item_sel + 1}/#{@items.size} #{game.title}"[0, cont_w],
x: CONT_X, y: CONT_Y + 1, style: THEME[:bright]
x: CONT_X, y: CONT_Y, style: THEME[:bright]
text "#{game.platform} #{game.author}"[0, cont_w],
x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
wordwrap(game.desc.to_s, cont_w).first(cont_h - 10).each_with_index do |l, i|
text l, x: CONT_X, y: CONT_Y + 4 + i, style: THEME[:normal]
x: CONT_X, y: CONT_Y + 1, style: THEME[:normal]
wordwrap(game.desc.to_s, cont_w).first(cont_h - 8).each_with_index do |l, i|
text l, x: CONT_X, y: CONT_Y + 3 + i, style: THEME[:normal]
end
badges = []
badges << tc(:success, '[▶ Play]') unless game.play_path.empty?
badges << tc(:label, '[⬇ Download]') unless game.download_path.empty?
badges << tc(:bright, '[Source]') unless game.source_path.empty?
badges << tc(:header, '[Docs]') unless game.docs_path.empty?
text badges.join(' '), x: CONT_X, y: CONT_Y + cont_h - 5
text badges.join(' '), x: CONT_X, y: CONT_Y + cont_h - 3
text '↑↓ navigate q back', x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
end
end
@@ -308,10 +304,9 @@ MAIN_TUI = BBS::TUI.define do
enter { @page_title = 'ONLINE USERS'; @items = ONLINE.snapshot.sort.map { |_, name| name } }
render do
text '── Online Users ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
@items.each_with_index do |uname, i|
you = uname == @username ? tc(:normal, ' ← you') : ''
text tc(:bright, uname) + you, x: CONT_X, y: CONT_Y + 2 + i
text tc(:bright, uname) + you, x: CONT_X, y: CONT_Y + i
end
text "#{@items.size} user(s) online q back",
x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
@@ -336,9 +331,8 @@ MAIN_TUI = BBS::TUI.define do
end
render do
text '── System Info ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
@items.each_with_index do |row, i|
text row[0, cont_w], x: CONT_X, y: CONT_Y + 2 + i, style: THEME[:normal]
text row[0, cont_w], x: CONT_X, y: CONT_Y + i, style: THEME[:normal]
end
text 'q back', x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
end