Files
rubbs/lib/bbs/flow.rb
Zsolt Tasnadi 4d38046c70 Add FakeOS: configurable character-mode OS simulator
BBS::FakeOS is a generic, teletype-style engine for simulating retro
operating systems: a scripted boot sequence (BIOS POST, memory test,
banners, typewriter output) followed by an interactive command shell.

Each simulated OS (MS-DOS, CP/M, AmigaDOS CLI, ...) is just a
FakeOS.define { ... } config — prompt, colors, boot block, command table
and an on_unknown fallback — so new OSes are added as configuration, not
code. on_unknown is the natural hook for a conversational/LLM-backed
shell. Exposed as a Flow step (`os <definition>`) alongside tui/app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:04:59 +02:00

149 lines
3.7 KiB
Ruby

# frozen_string_literal: true
module BBS
class Flow
attr_reader :steps
def self.define(&block)
flow = new
flow.instance_eval(&block)
flow.freeze
end
def initialize
@steps = []
end
def screen(name, **vars)
@steps << { type: :screen, name: name, vars: vars }
end
def banner(text, style: :success)
@steps << { type: :banner, text: text, style: style }
end
def big_banner(text, style: :success, font: 'slant')
@steps << { type: :big_banner, text: text, style: style, font: font }
end
def say(text, style: :muted)
@steps << { type: :say, text: text, style: style }
end
def set(name, value)
@steps << { type: :set, name: name, value: value }
end
def ask(name, prompt:, transform: nil, validate: nil)
@steps << { type: :ask, name: name, prompt: prompt,
transform: transform, validate: validate }
end
def persist(*fields, store:)
@steps << { type: :persist, fields: fields.flatten, store: store }
end
def pause(message, seconds: 1.0)
@steps << { type: :pause, message: message, seconds: seconds }
end
def gate(denied:, &check)
@steps << { type: :gate, check: check, denied: denied }
end
def confirm(message, denied: nil, &block)
step = { message: message, denied: denied }
if block_given?
sub = Flow.new
sub.instance_eval(&block)
@steps << step.merge(type: :confirm_block, sub_steps: sub.steps)
else
@steps << step.merge(type: :confirm_halt)
end
end
def menu(prompt, loop: false, &block)
builder = MenuBuilder.new
builder.instance_eval(&block)
@steps << { type: :menu, prompt: prompt, loop: loop, options: builder.options }
end
def exit_menu
@steps << { type: :exit_menu }
end
def call(&block)
@steps << { type: :call, block: block }
end
def line(color: :muted)
@steps << { type: :line, color: color }
end
def section(title, color: :confirm)
@steps << { type: :section, title: title, color: color }
end
def text(content = nil, style: :muted, &block)
@steps << { type: :text, text: content, block: block, style: style }
end
def rows(empty: 'No items.', &block)
@steps << { type: :rows, block: block, empty: empty }
end
def table(&block)
@steps << { type: :table, block: block }
end
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
def tui(definition)
@steps << { type: :tui, definition: definition }
end
def app(definition)
@steps << { type: :app, definition: definition }
end
def os(definition)
@steps << { type: :os, definition: definition }
end
end
class MenuBuilder
attr_reader :options
def initialize
@options = []
end
def option(label, &block)
sub = Flow.new
sub.instance_eval(&block) if block_given?
@options << { label: label, steps: sub.steps }
end
end
end