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:
@@ -99,6 +99,25 @@ module BBS
|
||||
def wait_enter(prompt: 'Press ENTER to continue...')
|
||||
@steps << { type: :wait_enter, prompt: prompt }
|
||||
end
|
||||
|
||||
def fetch(name, loading: 'Loading...', &block)
|
||||
@steps << { type: :fetch, name: name, loading: loading, block: block }
|
||||
end
|
||||
|
||||
def pick(from:, prompt:, empty: 'No items.', item:, hint: nil, &block)
|
||||
sub = Flow.new
|
||||
sub.instance_eval(&block) if block_given?
|
||||
@steps << { type: :pick, from: from, prompt: prompt, empty: empty,
|
||||
item: item, hint: hint, sub_steps: sub.steps }
|
||||
end
|
||||
|
||||
def body(width: 66, indent: 4, &block)
|
||||
@steps << { type: :body, block: block, width: width, indent: indent }
|
||||
end
|
||||
|
||||
def output(&block)
|
||||
@steps << { type: :output, block: block }
|
||||
end
|
||||
end
|
||||
|
||||
class MenuBuilder
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user