Initial commit: extracted from impostor-bbs gems/bbs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 22:18:57 +02:00
commit 4690ade510
11 changed files with 583 additions and 0 deletions

190
lib/bbs/flow_runner.rb Normal file
View File

@@ -0,0 +1,190 @@
# frozen_string_literal: true
module BBS
class FlowRunner
STYLES = {
success: "\e[1;32m",
muted: "\e[0;37m",
error: "\e[1;31m",
info: "\e[0;33m",
prompt: "\e[1;37m",
confirm: "\e[1;36m",
}.freeze
def initialize(session, session_id, flow)
@session = session
@session_id = session_id
@flow = flow
@ctx = {}
end
def run
execute(@flow.steps)
end
private
def execute(steps)
steps.each do |step|
result = dispatch(step)
return result if result == :halt || result == :exit_menu
end
nil
end
def dispatch(step)
case step[:type]
when :screen then run_screen(step)
when :banner then run_banner(step)
when :big_banner then run_big_banner(step)
when :say then run_say(step)
when :set then run_set(step)
when :ask then run_ask(step)
when :persist then run_persist(step)
when :pause then run_pause(step)
when :gate then run_gate(step)
when :confirm_halt then run_confirm_halt(step)
when :confirm_block then run_confirm_block(step)
when :menu then run_menu(step)
when :exit_menu then :exit_menu
end
end
# ── step handlers ──────────────────────────────────────────────────────────
def run_screen(step)
vars = step[:vars].transform_values { |v| v.is_a?(Symbol) ? @ctx[v] : v }
write Renderer.render(step[:name].to_s, @ctx.merge(vars))
end
def run_banner(step)
color = STYLES.fetch(step[:style], STYLES[:success])
write Banner.render(step[:text], color: color)
end
def run_big_banner(step)
color = STYLES.fetch(step[:style], STYLES[:success])
write Banner.render_big(step[:text], color: color, font: step[:font])
end
def run_say(step)
color = STYLES.fetch(step[:style], STYLES[:muted])
write "\r\n #{color}#{step[:text]}\e[0m\r\n\r\n"
end
def run_set(step)
@ctx[step[:name]] = step[:value]
nil
end
def run_ask(step)
write " #{STYLES[:prompt]}#{step[:prompt]}\e[0m: "
value = readline&.strip
return :halt if value.nil?
value = apply_transform(value, step[:transform])
if step[:validate] && !valid?(value, step[:validate])
write "\r\n #{STYLES[:error]}#{validation_message(step[:validate])}\e[0m\r\n\r\n"
return :halt
end
@ctx[step[:name]] = value
nil
end
def run_persist(step)
fields = step[:fields].filter_map { |f| [f, @ctx[f]] if @ctx.key?(f) }.to_h
step[:store].upsert(session_id: @session_id, **fields) unless fields.empty?
nil
end
def run_pause(step)
write "\r\n #{STYLES[:info]}#{step[:message]}\e[0m\r\n"
sleep step[:seconds]
nil
end
def run_gate(step)
return nil if step[:check].call(@ctx)
write " #{STYLES[:error]}#{step[:denied]}\e[0m\r\n\r\n"
:halt
end
def run_confirm_halt(step)
write "\r\n #{STYLES[:confirm]}#{step[:message]}\e[0m [yes/no]: "
answer = readline&.strip&.downcase
return :halt if answer.nil?
return nil if %w[yes y].include?(answer)
write "\r\n #{STYLES[:muted]}#{step[:denied]}\e[0m\r\n\r\n" if step[:denied]
:halt
end
def run_confirm_block(step)
write "\r\n #{STYLES[:prompt]}#{step[:message]}\e[0m [yes/no]: "
answer = readline&.strip&.downcase
return :halt if answer.nil?
if %w[yes y].include?(answer)
execute(step[:sub_steps])
else
write "\r\n #{STYLES[:muted]}#{step[:denied]}\e[0m\r\n\r\n" if step[:denied]
nil
end
end
def run_menu(step)
loop do
write "\r\n"
step[:options].each_with_index do |opt, i|
write " #{STYLES[:muted]}#{i + 1}. #{opt[:label]}\e[0m\r\n"
end
write "\r\n #{STYLES[:prompt]}#{step[:prompt]}\e[0m "
input = readline&.strip
return :halt if input.nil?
index = input.to_i - 1
unless (0...step[:options].length).include?(index)
write "\r\n #{STYLES[:error]}Enter a number between 1 and #{step[:options].length}.\e[0m\r\n"
next
end
result = execute(step[:options][index][:steps])
return :halt if result == :halt
return nil if result == :exit_menu
break unless step[:loop]
end
nil
end
# ── helpers ────────────────────────────────────────────────────────────────
def apply_transform(value, transform)
case transform
when :upcase then value.upcase
when :downcase then value.downcase
else value
end
end
def valid?(value, validator)
case validator
when :email then value.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
when :non_empty then !value.empty?
else true
end
end
def validation_message(validator)
case validator
when :email then "That doesn't look like a valid email address. Farewell."
else "Invalid input. Farewell."
end
end
def write(data) = @session.write(data)
def readline = @session.readline
end
end