EN /HU | Login

Flow

The Flow layer of rubbs: a line-oriented, declarative dialogue that every board starts with. This page lists every step of BBS::Flow with its signature, defaults and an example, explains how steps share data (the context), how a flow stops, how ERB screens and banners work, and ends with a complete login-and-menu board and the real Impostor BBS. Written against rubbs 1.0.0.

Overview

RUBY
require 'bbs'

DEMO = BBS::Flow.define do
  banner 'WELCOME'
  ask :name, prompt: 'Your name'
  text { |ctx| "Hello, #{ctx[:name]}!" }
end

BBS.configure { |c| c.flow = DEMO }
BBS.start

BBS::Flow.define(screens_dir: nil) { … } evaluates the block on a new Flow and returns it frozen. Each DSL call in the block appends one step to flow.steps, a small object of BBS::Flow::Steps with a run(runner) method; nothing is sent to anybody yet. A flow is a template, built once at load time and shared by every caller.

When a caller connects, BBS::FlowRunner runs the steps from top to bottom on that caller's line. Steps that print write ANSI text, steps that ask read a line from the caller. Text scrolls like on a teletype; nothing is redrawn in place. For a full screen there is the Application (windows, menus, mouse) and, for a line-by-line board, the 40×25 BBS::Petscii::Screen with its BBS::Selector menus (see Commodore 64).

Most boards use a flow as glue only: the terminal question, perhaps a login, then a call step that runs the board and one for the goodbye. The steps below are all there is; anything else is Ruby in a call.

Every example on this page is a constant like DEMO above; to try one, put it in c.flow and start the server as in Getting started.

The context

Each run of a flow has its own context, a Hash with Symbol keys, called ctx in blocks. ask and your call blocks write it, blocks read it, persist saves fields from it, screen hands it to the ERB template.

Key Set by Value
:session_id the runner, always the caller's random 16-hex-digit session id
any name ask NAME, your call blocks whatever you put there

Sub-flows (menu options, confirm blocks) share the same context. A flow started with BBS::FlowRunner.new(session, session_id, flow, context: {...}) begins with those keys (this is how a door passes the caller's name in); session_id is always set over them.

How a flow ends

The runner executes steps in order until one of these:

  • The steps run out. Back in the caller of this flow; for config.flow that means the server hangs up.
  • A step returns :halt. The whole flow stops, including every enclosing menu and sub-flow. :halt comes from: the caller hanging up during any read, a failed ask validation, a failed gate, a "no" to a confirm without a block, a call block returning :halt.
  • exit_menu leaves the innermost menu only, and the flow continues after it.

Errors: a call block's error is logged through BBS.log (flow call: …), the caller sees "Something went wrong.", and the flow goes on; a dropped line inside it halts. Any other step's error (for example in a text or gate block, an unknown banner font, a missing ERB file) ends the session.

Step reference

Step Signature Does
say say(text, style: :muted) prints one line between blank lines
text text(content = nil, style: :muted) { … } prints one line, fixed or computed
banner banner(text, style: :success) a double-line framed title
big_banner big_banner(text, style: :success, font: 'slant') a FIGlet title
screen screen(name, **vars) renders an ERB file
ask ask(name, prompt:, transform: nil, validate: nil) reads a line into the context
pause pause(message, seconds: 1.0) prints and sleeps
confirm confirm(message, denied: nil) or with a block a yes/no question
gate gate(denied:) { … } stops the flow unless the block is truthy
menu menu(prompt, loop: false) { option … } a numbered BBS::Selector of sub-flows
exit_menu exit_menu leaves the innermost menu
persist persist(*fields, store:) saves context fields into a BBS::Store
call call { … } runs any Ruby code with the line
app app(definition) runs a BBS::Application

The steps set, line, section, rows, table, wait_enter, fetch, pick, body, output, os and tui are gone since 1.0: each is a few lines in a call (the runner's line, ask and select cover most of them), and a BBS::FakeOS machine runs as a BBS::Door (see Doors).

Styles

style: takes a name from BBS::FlowRunner::STYLES, colours of the one palette (BBS::Ansi):

Name Colour
:success bright green
:muted grey (plain text)
:error bright red
:info yellow
:prompt white

Any other value (an unknown Symbol, a raw ANSI string) prints in :muted. The colour names of 0.x (:confirm, :cyan, :magenta, :blue, :yellow, :white, :green, :red) are gone.

Output steps

say

say(text, style: :muted) prints text indented by two spaces, with a blank line before and after.

RUBY
DEMO = BBS::Flow.define do
  say 'Welcome to the board.'
  say 'Login successful.', style: :success
  say 'The file area is closed tonight.', style: :error
end

text

text(content = nil, style: :muted) prints one indented line with no blank lines around it. With a block, the block gets the context and its return value is printed; a nil result prints nothing.

RUBY
DEMO = BBS::Flow.define do
  ask :name, prompt: 'Name'
  text 'Your account:', style: :prompt
  text { |ctx| "  name:    #{ctx[:name]}" }
  text { |ctx| "  session: #{ctx[:session_id]}" }
  text(style: :info) { |ctx| 'sysop!' if ctx[:name] == 'sysop' }
end

An exception in the block is not caught and ends the session; compute risky values in a call first.

banner

banner(text, style: :success) prints text in a double-line box (╔═╗ ║ ╚═╝) with two spaces of padding.

RUBY
DEMO = BBS::Flow.define do
  banner 'IDENTITY VERIFICATION', style: :info
end
TEXT
  ╔═════════════════════════╗
  ║  IDENTITY VERIFICATION  ║
  ╚═════════════════════════╝

big_banner

big_banner(text, style: :success, font: 'slant') renders text with FIGlet through the artii gem. Any font name artii knows works (slant, standard, small, big, doom, banner, …; bundle exec artii --list prints them all). An unknown font raises and ends the session, so try new fonts locally first.

RUBY
DEMO = BBS::Flow.define do
  big_banner 'HARBOR'
  big_banner 'BBS', font: 'doom', style: :info
end

screen

screen(name, **vars) renders the ERB file NAME.erb and writes it. See Screens (ERB) below.

RUBY
DEMO = BBS::Flow.define(screens_dir: File.join(__dir__, 'screens')) do
  screen :welcome, year: Time.now.year, node: :session_id
end

Input steps

ask

ask(name, prompt:, transform: nil, validate: nil) prints prompt: in :prompt style, reads one line, strips surrounding whitespace and stores the result in ctx[name]. On a modern terminal the server echoes the characters; on a PETSCII line the answer is typed in a BBS::Field, which edits with the cursor keys.

Option Values Effect
prompt: String the text before the colon; required
transform: :upcase, :downcase, a Proc applied to the stripped answer before validation; a Proc gets the String and returns the new value
validate: :non_empty, :email, a Proc or Method checked after the transform; a Proc or Method gets the (transformed) value, truthy means valid

A failed validation prints a message and halts the whole flow: :email says "That doesn't look like a valid email address. Farewell.", the others "Invalid input. Farewell.". It does not ask again. Any other value given to validate: is never checked (everything passes). To ask again, or to print your own message, use call (see Asking again and passwords).

RUBY
DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle', transform: :downcase, validate: :non_empty
  ask :email,  prompt: 'Email',  validate: :email
  ask :age,    prompt: 'Age',    transform: ->(answer) { answer.to_i }
  text { |ctx| "#{ctx[:handle]}, #{ctx[:age]}, #{ctx[:email]}" }
end

Your own rule as a lambda; abc becomes 0 by the transform and fails:

RUBY
ADULTS = BBS::Flow.define do
  ask :age, prompt: 'Age', transform: ->(answer) { answer.to_i }, validate: ->(age) { age >= 18 }
  say 'Welcome in.'
end

There is no mask: on ask; a password prompt is a call step with runner.readline(mask: '*').

pause

pause(message, seconds: 1.0) prints message in :info style and sleeps. The caller cannot skip it; keep it short.

RUBY
DEMO = BBS::Flow.define do
  pause 'Verifying identity...', seconds: 1.2
  say 'Verified.', style: :success
end

confirm

A yes/no question with a [yes/no] prompt; yes or y (any case) is yes, anything else is no.

Without a block, confirm(message, denied: nil) is a checkpoint: on yes the flow continues, on no it prints denied (if given) and halts.

With a block, confirm(message, denied: nil) { … } runs the block as a sub-flow on yes; on no it prints denied (if given) and the flow continues after the block.

RUBY
DEMO = BBS::Flow.define do
  confirm 'Are you over 18?', denied: 'Come back later.'
  say 'Welcome to the adult area.'

  confirm 'Subscribe to the newsletter?', denied: 'Maybe next time.' do
    ask :email, prompt: 'Email', validate: :email
    say 'Subscribed.', style: :success
  end

  say 'Main menu next.'
end

Control steps

gate

gate(denied:) { |ctx| … } calls the block with the context; if the result is falsy it prints denied in :error and halts the flow.

RUBY
BANNED = %w[spammer troll].freeze

DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle', transform: :downcase
  gate(denied: 'This handle is banned.') { |ctx| !BANNED.include?(ctx[:handle]) }
  say 'Come in.'
end

menu

menu(prompt, loop: false) { option … } is a numbered BBS::Selector of the options, 38 columns wide so it fits a C64, with prompt on its status row. The cursor keys and the digits light an option, RETURN takes it, and Esc (on a C64 RUN/STOP or ←) leaves the menu without one. Each option(label) { … } is a sub-flow, run with the shared context when it is taken. An option without a block does nothing.

loop: After the option's steps
false the menu is done; the flow continues after it
true the menu is shown again, until an option runs exit_menu or the caller presses Esc
RUBY
DEMO = BBS::Flow.define do
  menu 'Main menu', loop: true do
    option 'Bulletins' do
      say 'Nothing new.'
    end
    option 'Settings' do
      menu 'Settings' do
        option('Colours on')  { say 'Colours on.' }
        option('Colours off') { say 'Colours off.' }
      end
    end
    option 'Log off' do
      exit_menu
    end
  end
  say 'Goodbye!'
end

:halt inside an option (a failed gate, a call returning :halt, a hang-up) stops the whole flow, not just the menu.

exit_menu

exit_menu leaves the innermost enclosing menu; the flow continues after it. It works from nested sub-flows too (a confirm block inside an option). Outside any menu it ends the flow. A call block cannot return it (only :halt passes through a call).

RUBY
DEMO = BBS::Flow.define do
  menu 'Pick', loop: true do
    option('Again') { say 'Once more.' }
    option('Done') do
      confirm 'Really leave the menu?' do
        exit_menu
      end
    end
  end
  say 'Out of the menu.'
end

Note the difference: answering "no" to that confirm (with a block) just continues and the menu loops again.

persist

persist(*fields, store:) saves the named context fields into a BBS::Store, keyed by the session id: the first persist of a call adds a row, later ones update it. Fields missing from the context are skipped; if none are present, nothing is written. List only fields that are columns (headers:) of the store. A timestamp column, if the store has one, is filled with the time the row was created.

Since 0.33 the store is a table in a SQLite file: a .csv path means the .sqlite3 file beside it (data/guestbook.sqlite3), and a CSV file of an earlier version is imported once, when the table is empty. See Data and network for the store itself.

RUBY
GUESTBOOK = BBS::Store.new(path: File.join(__dir__, 'data/guestbook.csv'),
                           headers: %w[session_id timestamp name city message])

DEMO = BBS::Flow.define do
  ask :name, prompt: 'Name'
  ask :city, prompt: 'City'
  persist :name, :city, store: GUESTBOOK
  ask :message, prompt: 'Leave a message'
  persist :message, store: GUESTBOOK
  say 'Thanks for signing the guestbook.'
end

GUESTBOOK.all
# => [{ "session_id" => "3f9c2a1be07d4c11", "timestamp" => "2026-10-27T20:15:02Z",
#       "name" => "Zsolt", "city" => "Budapest", "message" => "Hello from the Harbor" }]

Code steps

call

call { |ctx, runner| … } runs any Ruby code. The block gets the context (read and write it freely) and the FlowRunner, which gives you the line:

runner.
write(text) writes to the caller (use \r\n line endings)
line(text, style: :muted) writes one line indented by two spaces, in a style's colour
ask(prompt) writes prompt and reads the answer, stripped (in a BBS::Field on a C64); nil when the caller hung up
readline(mask: nil) reads one line; mask: '*' echoes * instead of the characters; nil when the caller hung up
select(options, hint: nil) a numbered BBS::Selector of [label, value] pairs; returns the value taken, nil for Esc
context the context Hash (the block's ctx)
session the BBS::Session: readkey, term_cols, term_rows, petscii?, enable_mouse, … (see Telnet and Store)
session_id the session id

Return :halt to stop the flow; any other return value continues it. Inside the block use next (not return) to leave early. An exception is logged, the caller sees "Something went wrong.", and the flow continues; a lost connection halts.

RUBY
DEMO = BBS::Flow.define do
  call do |ctx, runner|
    runner.line "Your terminal is #{runner.session.term_cols}x#{runner.session.term_rows}."
    ctx[:wide] = runner.session.term_cols >= 100
    nil
  end
  text { |ctx| ctx[:wide] ? 'Wide screen layout.' : 'Narrow screen layout.' }
end

What the removed steps did is a few lines here. A list to choose from, a wait for RETURN, a value loaded with a line on screen:

RUBY
Post = Struct.new(:title, :body)
POSTS = [Post.new('Welcome', 'Hello all.'), Post.new('Rules', 'Be nice.')].freeze

DEMO = BBS::Flow.define do
  call do |_ctx, runner|
    post = runner.select(POSTS.map { |p| [p.title, p] }, hint: 'Read which post?')
    next nil unless post

    runner.line post.title, style: :prompt
    runner.line post.body
    runner.ask('Press RETURN')
    nil
  end
end

call is also how a flow runs things that are not steps: an Application with code around it (APP.run(...), see app), another flow (BBS::FlowRunner.new(runner.session, ctx[:session_id], OTHER, context: ctx).run), a door (BBS::Door.new(:machine, MACHINE).run(runner.session, ctx), a FakeOS machine too), a ZMODEM download, a board's own BBS::Petscii::Screen.

Asking again and passwords

ask halts on a bad answer; a call loop asks until it gets a good one, and can mask the input:

RUBY
PINS = { 'sysop' => '1234' }.freeze

DEMO = BBS::Flow.define do
  call do |ctx, runner|
    loop do
      answer = runner.ask('  Handle (3-12 letters): ')
      break :halt if answer.nil?

      answer = answer.downcase
      if answer.match?(/\A[a-z]{3,12}\z/)
        ctx[:handle] = answer
        break nil
      end
      runner.line 'Letters only, 3 to 12 of them.', style: :error
    end
  end

  call do |ctx, runner|
    next nil unless PINS.key?(ctx[:handle])

    result = :halt
    3.times do
      runner.write '  PIN: '
      pin = runner.readline(mask: '*')
      break if pin.nil?

      if pin.strip == PINS[ctx[:handle]]
        ctx[:sysop] = true
        result = nil
        break
      end
      runner.line 'Wrong PIN.', style: :error
    end
    result
  end

  text { |ctx| "Welcome, #{ctx[:handle]}#{' (sysop)' if ctx[:sysop]}." }
end

Inside loop, times or each blocks, next only skips to the next iteration; leave them with break VALUE, and make that value (or the block's last expression) the call's result. A hang-up (ask or readline returning nil) must end the loop, otherwise it spins forever.

app

app(definition) hands the line to a BBS::Application (see Application): definition.run(session:, session_id:, context: ctx), until it quits, through its doors too (the chosen door runs and the Application is rebuilt after it). The Application's context is this same Hash, so changes are visible to later steps; a dropped line halts the flow.

RUBY
DESKTOP = BBS::Application.define do
  status_hint('F10', 'Back') { :halt }
end

DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle'
  app DESKTOP
  say 'Back in the flow.'
end

The same from a call step, the form every board in our network uses, leaves room for code around the run:

RUBY
DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle'
  call do |ctx, runner|
    DESKTOP.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
    nil
  end
  say 'Thanks for calling.'
end

Screens (ERB)

A screen step renders an ERB template and writes it. Screens are where hand-drawn ANSI title pages live.

Where the file is. screen :welcome reads welcome.erb from the flow's own folder, BBS::Flow.define(screens_dir: DIR), or, if the flow has none, from BBS.config.screens_dir. If neither is set, the step raises (No screens_dir: …) and the session ends. A flow shipped in a gem names its own folder, so it renders its screens inside any board.

Variables. Inside the template every context key is an instance variable (@session_id, @name, …), plus the step's vars. A vars value that is a Symbol is looked up in the context (node: :session_id gives @node the session id); anything else is used as is.

Helpers available in a template:

Helper Gives
clr clear screen and home the cursor (\e[2J\e[H)
reset \e[0m
green, yellow, cyan, white, red bright colours of the palette (BBS::Ansi)
gray, dim_green plain grey and normal green
banner(text, padding: 2) a green double-line box, as the banner step
big_banner(text, font: 'slant') green FIGlet art, as the big_banner step

ERB runs with trim_mode: '-', so -%> swallows the line break after a tag; every \n of the result is sent as \r\n.

screens/welcome.erb:

ERB
<%= clr -%>
<%= big_banner('Harbor') %>

  <%= cyan %>The Harbor BBS<%= reset %>  <%= gray %>node <%= @node %>, <%= @year %><%= reset %>
<% if @name -%>
  <%= white %>Welcome back, <%= @name %>.<%= reset %>
<% end -%>

The flow:

RUBY
DEMO = BBS::Flow.define(screens_dir: File.join(__dir__, 'screens')) do
  screen :welcome, year: Time.now.year, node: :session_id
  ask :name, prompt: 'Name'
  screen :welcome, year: Time.now.year, node: :session_id
end

The same renderer is callable directly, e.g. from a call or a test: BBS::Renderer.render('welcome', { year: 2026 }, dir: 'screens') returns the String.

Banners

The banner and big_banner steps and the template helpers all come from BBS::Banner, which you can call yourself (in a call, a door):

Method Returns
BBS::Banner.render(text, color: BBS::Ansi::BRIGHT_GREEN, padding: 2) a double-line box around text, indented by two spaces, \r\n line endings, a blank line before and after
BBS::Banner.render_big(text, color: BBS::Ansi::BRIGHT_GREEN, font: 'slant') FIGlet art of text (artii font font), each line indented by two spaces and coloured, \r\n line endings

color is an ANSI SGR sequence, usually a colour of the palette (BBS::Ansi::CYAN); BBS::FlowRunner::STYLES[:info] gives a style's colour.

RUBY
require 'bbs'

puts BBS::Banner.render('HELLO', color: BBS::FlowRunner::STYLES[:info])
puts BBS::Banner.render_big('BBS', font: 'small')

DEMO = BBS::Flow.define do
  call { |_ctx, runner| runner.write BBS::Banner.render_big('NEWS', color: BBS::Ansi::BRIGHT_CYAN, font: 'standard') }
end
TEXT
  ╔═════════╗
  ║  HELLO  ║
  ╚═════════╝

   ___ ___ ___
  | _ ) _ ) __|
  | _ \ _ \__ \
  |___/___/___/

A complete board: login and menu

A board with a welcome screen, a login with a sysop PIN, a user store, a message reader and an account page. Folder:

TEXT
harbor/
├── bbs.rb
├── screens/welcome.erb   # the template from "Screens (ERB)" above
└── data/                 # users.sqlite3 appears here

bbs.rb:

RUBY
require 'bbs'

USERS = BBS::Store.new(path: File.join(__dir__, 'data/users.csv'),
                       headers: %w[session_id timestamp handle email])

Post = Struct.new(:title, :author, :body)
POSTS = [
  Post.new('Welcome aboard', 'sysop', 'This is the Harbor. Be nice, keep it short.'),
  Post.new('Door games', 'sysop', 'Three doors are coming next month.')
].freeze

PINS = { 'sysop' => '1234' }.freeze

READ_POSTS = lambda do |_ctx, runner|
  loop do
    post = runner.select(POSTS.map { |p| ["#{p.title} (#{p.author})", p] }, hint: 'Read which post?')
    break nil unless post

    runner.line post.title, style: :prompt
    runner.line post.body
    break :halt if runner.ask('  RETURN for the list: ').nil?
  end
end

MAIN = BBS::Flow.define(screens_dir: File.join(__dir__, 'screens')) do
  screen :welcome, year: Time.now.year, node: :session_id

  banner 'LOGIN', style: :info
  ask :handle, prompt: 'Handle', transform: :downcase, validate: :non_empty

  call do |ctx, runner|
    next nil unless PINS.key?(ctx[:handle])

    runner.write '  PIN: '
    pin = runner.readline(mask: '*')
    next :halt if pin.nil?

    ctx[:sysop] = pin.strip == PINS[ctx[:handle]]
    nil
  end

  gate(denied: 'Wrong PIN.') { |ctx| !PINS.key?(ctx[:handle]) || ctx[:sysop] }
  persist :handle, store: USERS
  banner 'ACCESS GRANTED'

  menu 'Main menu', loop: true do
    option('Read posts') { call(&READ_POSTS) }

    option 'Your account' do
      text { |ctx| "Handle:  #{ctx[:handle]}" }
      text { |ctx| "Session: #{ctx[:session_id]}" }
      text { |ctx| "Sysop:   #{ctx[:sysop] ? 'yes' : 'no'}" }
      confirm 'Subscribe to the newsletter?', denied: 'Maybe next time.' do
        ask :email, prompt: 'Email', validate: :email
        persist :email, store: USERS
        say 'Subscribed.', style: :success
      end
    end

    option 'Log off' do
      exit_menu
    end
  end

  say 'Fair winds!', style: :info
end

BBS.configure do |c|
  c.flow = MAIN
end

BBS.start

After a caller subscribed, USERS.find(session_id) gives { "session_id" => "3f9c2a1be07d4c11", "timestamp" => "2026-10-27T22:17:56Z", "handle" => "sysop", "email" => "a@b.hu" }; the row is in data/users.sqlite3.

A real board: Impostor

The Impostor BBS is the debrief terminal of the game Definitely not an Impostor. Its whole dialogue is one Flow in a gem (lib/impostor.rb), with its own screens/ folder, so the same Flow runs as a standalone board and as a door in another board (door :impostor, Impostor::Modern.flow(sessions: store), label: '&Impostor'). The two faces are two modules, Impostor::Modern and Impostor::Petscii, each with its screens (screens/modern/, screens/petscii/, the latter drawn for 40 columns).

RUBY
require 'bbs'
require_relative 'impostor/code_generator'

module Impostor
  SCREENS = File.expand_path('../screens', __dir__)

  SESSION_HEADERS = %w[session_id timestamp player_name code feedback email].freeze

  def self.sessions_store(path) = BBS::Store.new(path: path, headers: SESSION_HEADERS)

  # The flow both faces share; +screens_dir+ holds the face's screens.
  def self.debrief(sessions:, screens_dir:)
    BBS::Flow.define(screens_dir: screens_dir) do
      screen :welcome

      banner "IDENTITY VERIFICATION", style: :info

      ask :player_name, prompt: "Player Name", transform: :upcase
      ask :code,        prompt: "Code"
      persist :player_name, :code, store: sessions

      pause "Verifying identity...", seconds: 1.2

      gate denied: "Access denied. Code mismatch." do |ctx|
        CodeGenerator.decrypt(ctx[:code]) == ctx[:player_name][0, CodeGenerator::NAME_LEN]
      end

      confirm "Norman, is that you?", denied: "Very well. Goodbye." do
        screen :congrats

        menu ">", loop: true do
          option "Leave feedback" do
            ask :feedback, prompt: "Feedback"
            persist :feedback, store: sessions
            say "Feedback recorded. Thank you.", style: :success
          end

          option "Subscribe to updates" do
            ask :email, prompt: "Email address", validate: :email
            persist :email, store: sessions
            say "Perfect. We'll be in touch.", style: :success
          end

          option "Exit" do
            exit_menu
          end
        end
      end
    end
  end

  module Modern
    def self.flow(sessions:) = Impostor.debrief(sessions: sessions, screens_dir: File.join(SCREENS, 'modern'))
  end

  module Petscii
    def self.flow(sessions:) = Impostor.debrief(sessions: sessions, screens_dir: File.join(SCREENS, 'petscii'))
  end
end

Things to notice: the flow is built by a method, so the store and the face are parameters, not globals; the code is persisted before the gate, so failed attempts are on record too; the confirm block holds the rest of the board, so a "no" ends it politely; and the board's bbs.rb only wires it up:

RUBY
require 'bundler/setup'
require_relative 'lib/impostor'

sessions = Impostor.sessions_store(File.expand_path('data/sessions.csv', __dir__))

BBS.configure do |c|
  c.flow = BBS::Terminal.flow(title: 'Impostor BBS',
                              modern: Impostor::Modern.flow(sessions: sessions),
                              petscii: Impostor::Petscii.flow(sessions: sessions))
end

BBS.start

Running a flow yourself

BBS::FlowRunner.new(session, session_id, flow, context: {}).run runs any flow on a session and returns when it ends; the return value is :halt if it halted, otherwise nil. This is how BBS::Terminal.flow runs the face the caller picked, and how a flow door runs inside an Application. From a call step:

RUBY
RULES = BBS::Flow.define do
  banner 'RULES', style: :info
  text '1. Be nice.'
  text '2. No spam.'
  pause 'Back to the board...', seconds: 2
end

DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle'
  call do |ctx, runner|
    BBS::FlowRunner.new(runner.session, ctx[:session_id], RULES, context: ctx).run
  end
  say 'Rules read.'
end

Returning the runner's result from the call passes a :halt of the inner flow on to the outer one. The inner flow gets its own context, seeded from ctx (a copy of the keys: what the inner flow asks does not appear in the outer ctx).

See also

  • rubbs — the main page
  • Getting started — installation, configuration, project layout
  • Application — the widget framework, entered with app or APP.run
  • Doors — flows as doors, BBS::FakeOS machines
  • Commodore — BBS::Terminal.flow, PETSCII faces, BBS::Petscii::Screen and BBS::Selector
  • Data and network — BBS::Store, BBS::SQLite, BBS.log
  • Telnet and Store — runner.session, keys