Add fetch, pick, body, output primitives

fetch: loading-indicator + ctx store for API calls
pick: numbered list with selection and sub-flow drill-down
body: word-wrapped article text (strip markdown, reflow)
output: raw string block for complex multi-line content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 09:27:45 +02:00
parent 272e75bd6f
commit c01159dc23
2 changed files with 123 additions and 0 deletions

View File

@@ -66,6 +66,10 @@ module BBS
when :rows then run_rows(step)
when :table then run_table(step)
when :wait_enter then run_wait_enter(step)
when :fetch then run_fetch(step)
when :pick then run_pick(step)
when :body then run_body(step)
when :output then run_output(step)
end
end
@@ -244,8 +248,108 @@ module BBS
nil
end
def run_fetch(step)
write "\r\n #{STYLES[:muted]}#{step[:loading]}\e[0m\r\n"
result = step[:block].call(@ctx)
write "\e[1A\e[2K"
@ctx[step[:name]] = result
nil
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
:halt
rescue => e
warn "BBS fetch error: #{e.class}: #{e.message}"
write "\r\n #{STYLES[:error]}Error loading content.\e[0m\r\n"
nil
end
def run_pick(step)
items = Array(@ctx[step[:from]]).first(25)
if items.empty?
write "\r\n #{STYLES[:error]}#{step[:empty]}\e[0m\r\n"
write "\r\n #{STYLES[:muted]}Press ENTER to continue...\e[0m"
return :halt if readline.nil?
return nil
end
write "\r\n"
items.each_with_index do |item, i|
write " #{step[:item].call(item, i + 1)}\r\n"
if step[:hint]
hint = step[:hint].call(item, i + 1).to_s.strip
write " #{STYLES[:muted]}#{hint}\e[0m\r\n" unless hint.empty?
end
end
write "\r\n #{STYLES[:muted]}#{step[:prompt]}\e[0m "
input = readline&.strip
return :halt if input.nil?
return nil if input.empty?
idx = input.to_i - 1
picked = items[idx]
unless picked
write "\r\n #{STYLES[:error]}Invalid selection.\e[0m\r\n"
return nil
end
@ctx[:picked] = picked
write "\r\n"
execute(step[:sub_steps])
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
:halt
end
def run_body(step)
text = strip_md(step[:block].call(@ctx).to_s)
width = step[:width]
prefix = ' ' * step[:indent]
words = text.split
lines = []
line = +''
words.each do |word|
if line.empty?
line << word
elsif line.length + 1 + word.length <= width
line << ' ' << word
else
lines << line.dup
line = +word
end
end
lines << line unless line.empty?
write "\r\n"
lines.each { |l| write "#{prefix}#{l}\r\n" }
write "\r\n"
nil
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
:halt
rescue => e
warn "BBS body error: #{e.class}: #{e.message}"
nil
end
def run_output(step)
content = step[:block].call(@ctx).to_s
write content unless content.empty?
nil
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
:halt
rescue => e
warn "BBS output error: #{e.class}: #{e.message}"
write "\r\n #{STYLES[:error]}Error rendering content.\e[0m\r\n"
nil
end
# ── helpers ────────────────────────────────────────────────────────────────
def strip_md(text)
text.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
.gsub(/[#*_`~>|\\]/, '')
.gsub(/\r?\n+/, ' ')
.strip
end
def style_color(value)
value.is_a?(Symbol) ? STYLES.fetch(value, STYLES[:muted]) : value.to_s
end