rubbs

A Ruby gem for building telnet BBS servers with a declarative conversation flow DSL.

Installation

# Gemfile
gem 'bbs', git: 'https://git.teletype.hu/tools/rubbs.git'

Quick start

require 'bbs'

sessions = BBS::Store.new(path: 'data/sessions.csv', headers: %w[session_id timestamp name])

BBS.configure do |c|
  c.screens_dir = 'screens'
  c.stores      = [sessions]
  c.flow        = BBS::Flow.define do
    screen :welcome
    ask :name, prompt: "What is your name?"
    say "Hello, %{name}!", style: :success
    persist :name, store: sessions
  end
end

BBS::Server.start

Connect with telnet localhost 2323. The port defaults to 2323 and can be overridden with the BBS_PORT environment variable.

Configuration

BBS.configure yields a BBS::Config object:

Option Default Description
screens_dir "screens" Directory containing ERB screen files
stores [] List of BBS::Store instances
flow A BBS::Flow object (required)
port ENV["BBS_PORT"] || 2323 TCP port to listen on

Flow DSL

The entire dialogue is defined as a declarative BBS::Flow.define block.

Primitives

Output

Primitive Description
screen :name, **vars Render an ERB screen from screens/; vars override/extend the context
banner "text", style: Print a box-drawing ASCII banner
big_banner "text", style:, font: Print a large FIGlet ASCII-art banner (default font: slant)
say "text", style: Print a single styled line
text "content", style: Print a line; accepts a block { |ctx| string } for dynamic content
section "title", color: Print a decorated section header with box-drawing
line color: Print a horizontal rule
rows(empty: "…") { |ctx| array } Print a list of lines; shows empty when the array is empty
table { |ctx| hash } Print a two-column key/value table
body(width:, indent:) { |ctx| string } Word-wrap and print a block of text (strips basic Markdown)
output { |ctx| string } Write a raw string returned by the block verbatim

Input & control

Primitive Description
ask :field, prompt:, transform:, validate: Read input and store it in the session context
set :field, value Set a context variable directly from code
persist :field, …, store: Write named context fields to a BBS::Store
pause "message", seconds: Display a message and sleep
wait_enter prompt: Print a prompt and block until the user presses Enter
gate denied: "…" { |ctx| bool } Halt the flow unless the block returns true
confirm "message", denied: "…" Yes/no gate — halts on no
confirm "message", denied: "…" { } Yes/no branch — runs the block on yes, skips on no
call { |ctx| } Run an arbitrary block; return :halt to end the session
menu "prompt", loop: bool { } Numbered option menu; loops back after each selection when loop: true
exit_menu Break out of the enclosing menu loop and continue the parent flow

Data fetching

Primitive Description
fetch :key, loading: "…" { |ctx| value } Show a loading message, call the block, store the result in ctx[:key], erase the loading line
pick from:, prompt:, empty:, item:, hint: { sub-flow } Show a numbered list from ctx[from]; on selection stores the item in ctx[:picked] and runs the sub-flow

ask options

Option Description
prompt: The string printed before the cursor
transform: A proc applied to the raw input before storing (e.g. transform: :upcase.to_proc)
validate: A proc that returns true if the value is acceptable; the question is repeated on failure

Banner styles

:success, :info, :error, :warning, :muted — each maps to an ANSI colour.

fetch + pick example

option 'Blog Posts' do
  section 'Blog Posts', color: :blue
  fetch :pages, loading: 'Loading...' do |ctx|
    WIKI.list('blog')
  end
  pick from: :pages,
       empty: 'No results.',
       prompt: 'Enter number to read (blank to go back)',
       item:  ->(p, i) { "#{i}. #{p.title}" },
       hint:  ->(p, i) { p.description.to_s[0...65] } do
    fetch :body, loading: 'Loading page...' do |ctx|
      WIKI.content(ctx[:picked].id)
    end
    body { |ctx| ctx[:body] }
    wait_enter
  end
end

Menu example

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

  option "Exit" do
    exit_menu
  end
end

say "Goodbye!", style: :muted

Screens

ERB files in screens/ have access to ANSI colour helpers and a banner helper. Context variables set by ask, set, or passed explicitly to screen are available as @field.

<%= banner("MISSION COMPLETE") %>

  <%= white %>Congratulations!<%= reset %> You are <%= yellow %><%= @name %><%= reset %>.

ANSI helpers

black, red, green, yellow, blue, magenta, cyan, white, reset, bold, dim

Stores

Each persist step writes to a BBS::Store (a thread-safe, append-and-upsert CSV file). session_id and timestamp are always written automatically.

identities = BBS::Store.new(path: 'data/identities.csv', headers: %w[session_id timestamp name code])
signups    = BBS::Store.new(path: 'data/signups.csv',    headers: %w[session_id timestamp email])

persist :name, :code, store: identities
persist :email,       store: signups

Rows are keyed by session_id: the first persist for a session creates the row; subsequent ones update it in place.

Colors

BBS::Color is a module_function module you can include to get the c helper anywhere — top-level, inside lambdas, and inside instance_eval blocks.

include BBS::Color

c(:green,  "[▶ Play]")      # => "\e[1;32m[▶ Play]\e[0m"
c(:gray,   "some text")
c(:yellow, msg.timestamp)

Available color symbols: :reset, :gray, :yellow, :white, :blue, :cyan, :green, :magenta, :red.

Raises KeyError on unknown symbols.

Architecture

File Responsibility
bbs/server.rb TCP accept loop, spawns one thread per client
bbs/session.rb Negotiates telnet options, runs the flow
bbs/telnet.rb Telnet protocol (IAC handling, echo control, readline)
bbs/flow.rb Flow DSL builder
bbs/flow_runner.rb Executes a Flow step by step against a session context
bbs/color.rb BBS::Color module with ANSI color map and c() helper
bbs/config.rb Configuration object
bbs/banner.rb Box-drawing and FIGlet banner renderer
bbs/renderer.rb ERB screen loader with ANSI colour helpers
bbs/store.rb Thread-safe CSV persistence
Description
No description provided
Readme 51 KiB
Languages
Ruby 100%