A Commodore 64 can call a rubbs board.
session.petscii!switches the line to PETSCII (40×25, the C64's colours and keys) while the code above keeps writing UTF-8 with ANSI;BBS::Terminalasks the caller which machine is calling; and a two-faced board serves a modern face and a C64 face on the same port. This page covers all three, with Feedia and Alexandria BBS as worked examples, and how a real or emulated C64 gets on the line. Available since 0.15.0 (BBS::Petscii) and 0.16.0 (BBS::Terminal).
A Commodore 64 has no network port. It reaches a telnet board through a terminal program and a modem:
| Part | What callers use |
|---|---|
| Terminal program | CCGMS (CCGMS Future), StrikeTerm, or SyncTERM in C64 mode on a PC. It must be in PETSCII mode (no ASCII translation), in the lowercase character set. |
| Modem | A WiFi modem (Zimodem, WiModem232, and the like) on the user port or behind a SwiftLink/Turbo232 cartridge, typically 38400 baud with RTS/CTS on a SwiftLink. It is dialled with ATDT host:port, e.g. ATDT teletypegames.org:2323. |
What the board sees:
That is why rubbs guesses "C64" when no window size comes, and why the PETSCII line turns everything into single bytes rather than escape sequences.
The VICE emulator (x64sc) can play the C64, the SwiftLink and the modem line: VICE's RS-232 device connects straight to a TCP host (+rsdev1ip232 means raw TCP, no IP232 protocol), and CCGMS on a disk image is the terminal program.
# A C64 with a SwiftLink at $DE00 (NMI), its line connected to a board over TCP,
# CCGMS autostarted from a disk image; CCGMS's saved config sets the SwiftLink to 38400.
x64sc -silent +cart \
-acia1 -acia1base 0xDE00 -acia1irq 1 -acia1mode 1 -myaciadev 0 \
-rsdev1 127.0.0.1:2323 -rsdev1baud 38400 +rsdev1ip232 \
-autostart ccgms.d64
With -rsdev1 the connection is already open when CCGMS starts, so no ATDT is needed; the board asks its first question, and RETURN takes the guess (C64). To check bytes without an emulator, connect with a small script, send no NAWS, and look at what comes back (see the example under BBS::Terminal below).
session.petscii! and session.petscii?| Method | What it does |
|---|---|
session.petscii! |
From now on the session talks to a C64. It sets term_cols / term_rows to 40×25, switches mouse reporting off for good (enable_mouse becomes a no-op), sends LOWERCASE, LOCK_CASE and CLEAR raw (the lowercase character set, C= + Shift locked, a clear screen), and puts a BBS::Petscii::Encoder in front of write. Calling it again does nothing. |
session.petscii? |
true once petscii! has run. |
After petscii!:
session.write goes through the Encoder, so a Flow, a FakeOS, an Application or a door keeps writing UTF-8 text with ANSI colours and cursor moves;readkey and readline decode the C64's keys (see The C64's keys below);A Flow, a FakeOS or a door drawn for 40 columns runs on a C64 unchanged; one drawn for 80 columns wraps.
require 'bbs'
BBS.configure do |c|
c.flow = BBS::Flow.define do
call do |_ctx, runner|
session = runner.session
session.petscii!
session.write "\e[1;33mREADY.\e[0m\r\n"
session.write "#{session.term_cols}x#{session.term_rows}, petscii: #{session.petscii?}\r\n"
nil
end
end
end
BBS.start
BBS::Petscii constantsThe constants are the PETSCII codes as one-character UTF-8 strings (the C0/C1 code points), so they can be written straight into a string: "#{BBS::Petscii::YELLOW}READY." is yellow on the C64.
| Constant | Code | Meaning |
|---|---|---|
COLS, ROWS |
40, 25 | The C64's text screen. |
CLEAR |
0x93 |
Clear the screen, cursor home. |
HOME |
0x13 |
Cursor to the top left corner. |
UP, DOWN, LEFT, RIGHT |
0x91, 0x11, 0x9d, 0x1d |
Cursor moves. |
RVS_ON, RVS_OFF |
0x12, 0x92 |
Reverse video on and off. RETURN also ends reverse mode. |
DEL |
0x14 |
Delete (backspace) on the C64. |
BELL |
0x07 |
Bell. |
LOWERCASE, UPPERCASE |
0x0e, 0x8e |
Switch to the lowercase (text) or the uppercase (graphics) character set. |
LOCK_CASE |
0x08 |
Lock the character set, so C= + Shift no longer switches it. This is ASCII's backspace, and the Encoder turns 0x08 into a cursor-left: send it raw (petscii! does). |
CONTROLS |
array | The control bytes the Encoder passes through to the C64 as they are. |
DEFAULT |
LIGHT_GREY |
The colour of plain text (\e[0m). |
The C64 has 16 text colours. All of them can be written directly; most are also reachable from ANSI.
| Constant | Code | ANSI that gives it |
|---|---|---|
WHITE |
0x05 |
\e[1;37m, \e[97m |
RED |
0x1c |
\e[31m |
GREEN |
0x1e |
\e[32m |
BLUE |
0x1f |
\e[34m |
ORANGE |
0x81 |
\e[33m (dark yellow) |
BLACK |
0x90 |
(none: write the constant) |
BROWN |
0x95 |
(none: write the constant) |
LIGHT_RED |
0x96 |
\e[1;31m, \e[91m |
DARK_GREY |
0x97 |
\e[30m, \e[1;30m, \e[90m (black is dark grey: a black-on-grey door must stay readable on the C64's black screen) |
GREY |
0x98 |
\e[2m, \e[2;37m (dim) |
LIGHT_GREEN |
0x99 |
\e[1;32m, \e[92m |
LIGHT_BLUE |
0x9a |
\e[1;34m, \e[94m |
LIGHT_GREY |
0x9b |
\e[37m, \e[0m (the default) |
PURPLE |
0x9c |
\e[35m, \e[1;35m |
YELLOW |
0x9e |
\e[1;33m, \e[93m |
CYAN |
0x9f |
\e[36m, \e[1;36m |
COLORS lists all 16; ANSI_DARK and ANSI_BRIGHT are the two rows of the ANSI mapping (index 0–7 = black, red, green, yellow, blue, magenta, cyan, white). Bold (1) picks the bright row. Background colours (40–47) and 256/true colours (38;5;n, 38;2;r;g;b, 48;…) are dropped: the background of a C64 terminal is the terminal program's.
BBS::Petscii::Encoder#encode(data) turns a string into PETSCII bytes (a binary string). The session keeps one Encoder per call, because the Encoder has state: the cursor column, the current colour, reverse mode, and an escape sequence cut in two by separate writes.
| Input | Output on the C64 |
|---|---|
A–Z / a–z |
Swapped the PETSCII way: A → 0xc1, a → 0x41, so they show in the right case in the lowercase set. |
Digits, space, !"#$%&'()*+,-./:;<=>?@[] |
As they are. |
Box drawing ─│┌┐└┘├┤┬┴┼ (single, double, heavy, rounded) |
The C64's line glyphs. The lowercase set has no rounded corners, so ╭╮╰╯ show as the square ┌┐└┘; the uppercase (graphics) set has them (Petscii::ROUNDED_CORNERS, drawn by a Petscii::Canvas). |
█ |
A reversed space. ▀ ▐ are reversed half blocks, ▄ ▌ ▁ ▔ the C64's own; ░▒▓ a checker glyph, ▖▗▘▝▚ the C64's quarter blocks. |
£ ← ↑ ✓ |
The C64's own £, ←, ↑, check mark. |
\ { } ~ ^ _ \| and ` | /, (, ), -, ↑, an underline glyph, a vertical line, '. |
|
Typography – — ‘ ’ “ ” … · • ● ◄ ► → × ° |
The nearest ASCII: -, ', ", ..., ., *, <, >, x, o. |
Accented letters á é ő ű Ä |
The letter without its accent (Unicode NFKD, marks removed): á → a, Ő → O. ß → ss, π → pi. |
Anything else (€, emoji, CJK) |
?. |
\r\n |
One RETURN (0x0d). |
\n alone |
One RETURN. |
\r alone |
Back to the start of the same line (RETURN + cursor up). |
\t |
Two spaces. |
\b (0x08) |
Cursor left. |
\f (0x0c), \e[2J |
CLR. |
\e[H, \e[ROW;COLH, \e[…f |
HOME, then that many DOWNs and RIGHTs. |
\e[nA … \e[nD |
n × UP, DOWN, RIGHT, LEFT. |
\e[nG |
RETURN, UP, then n−1 RIGHTs (column n of the same line). |
\e[…m |
Colour and reverse (7 / 27), see the colour table. |
Other escape sequences (\e[K, \e[?25l, \e(B, …) |
Dropped, even when a write cuts one in two. |
| PETSCII control codes (the constants) | Passed through. |
Other C0/C1 bytes, DEL (0x7f) |
Dropped. |
The 40-column wrap. The Encoder counts columns. A line of exactly 40 characters makes the C64 wrap by itself; the \r\n after it is then swallowed, so there is no empty line under it.
Invalid UTF-8 is scrubbed to ? before encoding.
Real bytes, from ruby -Ilib on rubbs 1.0.0:
require 'bbs'
enc = BBS::Petscii::Encoder.new
hex = ->(s) { s.bytes.map { |b| format('%02x', b) }.join(' ') }
puts hex.(enc.encode("Hello, World!\r\n"))
# c8 45 4c 4c 4f 2c 20 d7 4f 52 4c 44 21 0d
puts hex.(enc.encode("\e[1;33mREADY.\e[0m\r\n"))
# 9e d2 c5 c1 c4 d9 2e 9b 0d (yellow, READY., light grey)
puts hex.(enc.encode("┌──┐\r\n│ok│\r\n└──┘\r\n"))
# b0 c0 c0 ae 0d dd 4f 4b dd 0d ad c0 c0 bd 0d
puts hex.(enc.encode("Árvíztűrő — 3…\r\n"))
# c1 52 56 49 5a 54 55 52 4f 20 2d 20 33 2e 2e 2e 0d
puts hex.(enc.encode("\e[2J\e[3;5Hx"))
# 93 13 11 11 1d 1d 1d 1d 58 (CLR, HOME, 2 x DOWN, 4 x RIGHT, x)
puts hex.(enc.encode("€ ☺"))
# 3f 20 3f
puts hex.(BBS::Petscii::Encoder.new.encode('a' * 40 + "\r\nb"))
# 41 x 40, then 42: no RETURN after the full line
e2 = BBS::Petscii::Encoder.new
puts hex.(e2.encode("\e[1;3")) + ' | ' + hex.(e2.encode("2mGo"))
# | 99 c7 4f (the sequence finished in the second write: light green)
BBS::Petscii.bytes and BBS::Petscii.key| Method | What it does |
|---|---|
BBS::Petscii.bytes(codes) |
The bytes of a string of PETSCII code points, e.g. of the constants, as a binary string. Use it to send PETSCII past the Encoder. |
BBS::Petscii.key(byte) |
One byte from a C64 as a key, the way readkey returns keys: a Symbol, a one-character String, or nil for a byte that is not a key. readkey and readline use it on a PETSCII line. The key bytes are one table, Petscii::KEYS (byte → key); the old KEY_* constants are gone in 1.0. |
BBS::Petscii.bytes("#{BBS::Petscii::LOWERCASE}#{BBS::Petscii::LOCK_CASE}#{BBS::Petscii::CLEAR}")
# => "\x0E\b\x93" (0e 08 93: what petscii! sends first)
[0x41, 0xc1, 0x0d, 0x14, 0x03, 0x5f, 0x85, 0x8c, 0x5c, 0xa0, 0x93].map { |b| BBS::Petscii.key(b) }
# => ["a", "A", :enter, :backspace, :escape, :escape, :f1, :f8, "£", " ", nil]
To send raw PETSCII on a switched session (skipping the Encoder), write to the socket: session.line.write(BBS::Petscii.bytes(BBS::Petscii::LOCK_CASE)).
On a PETSCII line readkey returns:
| C64 key | Byte | readkey |
|---|---|---|
| A letter | 0x41–0x5a |
lowercase String ("a") |
| Shift + letter | 0xc1–0xda (or 0x61–0x7a) |
uppercase String ("A") |
| Digits, space, punctuation | 0x20–0x40, [, ] |
that character |
£ |
0x5c |
"£" |
↑ |
0x5e |
"^" |
| Shift + space | 0xa0 |
" " |
| RETURN | 0x0d |
:enter |
| INST/DEL | 0x14 |
:backspace |
| Cursor down / up | 0x11 / 0x91 |
:down / :up |
| Cursor right / left | 0x1d / 0x9d |
:right / :left |
| CLR/HOME | 0x13 |
:home |
| RUN/STOP | 0x03 |
:escape |
← (top left key) |
0x5f |
:escape |
| F1 … F8 | 0x85, 0x89, 0x86, 0x8a, 0x87, 0x8b, 0x88, 0x8c |
:f1 … :f8 |
| Anything else (colour keys, CLR, …) | skipped |
There is no :tab, no :ctrl_… and no mouse on a C64. A board that wants to work on both machines should use RETURN, the cursor keys, letters and digits, and treat :escape as "back" (RUN/STOP and ← both give it).
readline(mask:) on a PETSCII line takes the same keys, echoes in PETSCII (or the mask), and INST/DEL removes a whole character. It returns the line as UTF-8 on RETURN.
A board with two faces must know, before its first screen, whether to speak ANSI or PETSCII. BBS::Terminal (lib/bbs/terminal.rb) is that first question, shared by every board with a C64 face.
MY BOARD
1 - MODERN (ANSI/XTERM, 256 COLORS)
2 - COMMODORE 64 (PETSCII, 40 COLUMNS)
YOUR TERMINAL [2]:
:ctrl_t before the line is switched) clears the pick.| Constant | Value | Meaning |
|---|---|---|
Terminal::SETTLE |
1.5 |
Seconds guess waits for the telnet answers. |
Terminal::WAIT |
60 |
Seconds the question waits for a key before taking the default. |
Terminal::FACES |
hash | Per face: the digit, the label, and the keys that pick it. |
| Face | Digit | Keys that pick it |
|---|---|---|
:modern |
1 |
1, a, A, m, M |
:petscii |
2 |
2, c, C, p, P |
Terminal.guess(session)BBS::Terminal.guess(session) # => :modern or :petscii
Calls session.settle(timeout: 1.5): waits for the client's window size and drops whatever it types meanwhile. A reported window wider than 40 columns is :modern; 40 columns or less, or no report at all (a C64 behind a WiFi modem talks plain TCP), is :petscii.
Terminal.flow(title:, modern:, petscii:)BBS::Terminal.flow(title: 'My Board', modern: MODERN_FLOW, petscii: PETSCII_FLOW) # => a BBS::Flow
The whole first step as a Flow, to put into config.flow:
Terminal::Question, private since 1.0: Terminal.flow is the one way to ask); a dropped line ends the call;:petscii calls session.petscii!; on :modern calls session.enable_mouse, but only when BBS.config.mouse is on (since 0.16.1);ctx[:session_id] and anything set before).Mouse switching. Mouse reporting starts with an escape sequence (\e[?1002h\e[?1006h) that would be garbage on a C64. A two-faced board therefore sets config.mouse = true (the modern face wants a mouse) and config.mouse_on_connect = false (do not switch it on before the question); Terminal.flow switches it on after a modern pick.
BBS.configure do |config|
config.mouse = true
config.mouse_on_connect = false # the mouse's escape sequence is garbage on a C64
config.flow = BBS::Terminal.flow(title: 'My Board', modern: MODERN, petscii: PETSCII)
end
What the line carries (a script connected without NAWS, then RETURN):
board → caller ff fb 03 ff fb 01 ff fd 03 ff fd 1f telnet: WILL SGA, WILL ECHO, DO SGA, DO NAWS
...1.5 s, no NAWS from the caller...
board → caller "\r\n\r\nMY BOARD\r\n\r\n1 - MODERN ...\r\n2 - COMMODORE 64 ...\r\n\r\nYOUR TERMINAL [2]: "
caller → board 0d RETURN takes the guess
board → caller 32 0d 0a "2", new line
board → caller 0e 08 93 petscii!: LOWERCASE, LOCK_CASE, CLEAR
...the C64 face, in PETSCII...
One port, two faces: the caller answers Terminal's question and gets either
Application: windows, menubar, mouse, 80 or more columns, 256 colours; orThe two faces share the domain code (the data, the services) and nothing of the UI. The house layout, used by Feedia BBS, Alexandria BBS and the Teletype BBS:
bbs.rb BBS.configure + BBS::Terminal.flow(modern:, petscii:)
lib/domain/… the data: feeds, Wikipedia, … (no UI)
lib/ui/modern.rb UI::Modern.flow(...) → a BBS::Flow that runs an Application
lib/ui/petscii.rb UI::Petscii.flow(...) → a BBS::Flow with one call step
lib/ui/petscii/screen.rb UI::Petscii::Screen → the 40×25 screen helpers
bbs/feedia-bbs/bbs.rb (an RSS reader):
require 'bbs'
require_relative 'lib/domain/feeds'
require_relative 'lib/domain/inbox'
require_relative 'lib/ui/modern'
require_relative 'lib/ui/petscii'
feeds = Feeds.load(ENV.fetch('FEEDIA_FEEDS', File.join(__dir__, 'config/feeds.yml')))
inbox = Inbox.new(path: ENV.fetch('FEEDIA_INBOX', File.join(__dir__, 'data/inbox.jsonl')),
webhook: ENV.fetch('FEEDIA_DISCORD_WEBHOOK', ''))
BBS.configure do |config|
config.mouse = true
config.mouse_on_connect = false
config.idle_seconds = 600
config.flow = BBS::Terminal.flow(title: 'Feedia', modern: UI::Modern.flow(feeds, inbox),
petscii: UI::Petscii.flow(feeds, inbox))
end
BBS.start
Alexandria BBS (bbs/alexandria-bbs/bbs.rb, Wikipedia on a telnet line) is the same with Wikipedia.new(...) in place of feeds and inbox, and title: 'Alexandria BBS'.
BBS::Petscii::ScreenThe PETSCII face does not use Application: it writes lines, the way a C64 board did. The screen of such a board is part of rubbs, BBS::Petscii::Screen (since 0.29); each board subclasses it in lib/ui/petscii/screen.rb for methods of its own. Its ideas:
Colours as ANSI. The screen writes ANSI colours, which the PETSCII line turns into the C64's. rubbs' width helpers (FrameBuffer.wrap_ansi, the one word-wrap, and clip_ansi) know ANSI and would count PETSCII control codes as text, so ANSI keeps wrapping right. BBS::Petscii::Colors names the colours the C64 way: WH YE CY LG GR DG LB BL GN RD PU OR, and RV/RO for reverse on and off. They are the C64 names of the one palette, BBS::Ansi (YE is BBS::Ansi::BRIGHT_YELLOW, LG is BBS::Ansi::GREY, the text colour).
module UI
module Petscii
BBS::Petscii::Colors.install(self) # bare WH, YE, LG … in every class nested here
class Screen < BBS::Petscii::Screen
def banner(text) = puts(" #{YE}#{text}#{LG}") # the board's own methods
end
end
end
Its methods, each a C64-board habit (40×25 by default; Screen.new(session, cols: 80, rows: 24) serves a modern terminal too):
| Method | What it does |
|---|---|
cls |
Clears the screen: the C64's CLR, ANSI on a modern terminal. |
title(text, right = '') |
A clear screen under a reversed cyan title bar, text on the left and right (a count, a language) on the right, 39 columns wide. |
say(text, color: LG) |
Text wrapped to 38 columns, one space in. The colour is a keyword: say('Posted.', color: GN). |
rule(color: DG) |
A horizontal line, 39 columns. |
key(timeout:) |
readkey, raising IOError on hang-up (so a call step ends quietly). |
choose(prompt, keys, back: 'q') |
A one-key prompt: waits for one of keys (lowercase letters), RUN/STOP and ← give back, echoes the key in capitals. |
ask(prompt, text:, mask:, max:) |
A line typed in a BBS::Field, stripped. |
edit(text = '', title:, single_line: false, max: nil) |
The full-screen BBS::Editor at the screen's size: the text when saved, nil when the caller quit. |
pause(text = 'return: back') |
Waits for any key. |
page(heading, lines, right:, wait:, numbers:) |
Lines a screenful (20 lines) at a time under the title bar; RETURN for more, q or RUN/STOP leaves early; numbers: true shows 2/5 in the bar. |
pick(heading, items, empty:, right:, commands:) { label } |
A numbered BBS::Selector.list under the title bar: the cursor keys or the number, then RETURN; nil for q, ← or RUN/STOP. |
menu(items, back:) |
A BBS::Selector at the cursor. |
actions(*pairs) |
A row of keys as a cursor menu: screen.actions(%w[r read], %w[q back]). |
plain(text) |
On a C64, BBS::Petscii.plain(text): typography and accents as ASCII, so a line wraps where it is counted. |
Every menu of the screen is a BBS::Selector, in its default colours, BBS::Selector::COLORS (the roles text key bar heading about hint, in the palette's C64 names). A board that wants another footer merges into them: BBS::Selector::COLORS.merge(hint: "#{CY}#{RV}").
From 0.x to 1.0 a C64 face changes in three places: say(text, GN) and rule(DG) become say(text, color: GN) and rule(color: DG); editor(max, title) becomes edit(title: title, max: max).to_s (it returns nil when the caller quits); and Petscii::Screen::MENU is gone, so colors: MENU is dropped and MENU.merge(…) becomes BBS::Selector::COLORS.merge(…). Petscii::Screen.plain is BBS::Petscii.plain. The full list is in the gem's UPGRADING.md.
The face's flow is one call step that runs the whole call, and a second one that says goodbye; the goodbye rescues the hang-up errors, because a caller who drops during the menu should not leave a backtrace:
def self.flow(feeds, inbox)
BBS::Flow.define do
call do |ctx, runner|
Reader.new(feeds, inbox, Screen.new(runner.session), ctx).run
nil
end
call do |_ctx, runner|
C64.goodbye(Screen.new(runner.session))
nil
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
nil
end
end
end
The Reader is the board itself: a masthead, a menu of F/S/W/Q, the feeds through pick, an item through page (its Markdown rendered with BBS::Markdown.to_lines(body, width: 38, theme: {...}) in the screen's colours). Alexandria's Proxy is the same shape: a prompt that takes a title, results through pick, an article's sections through page.
Drawing for 40 columns. Logos are drawn from reversed spaces (RV + spaces + RO), which show the same on both machines; box drawing and █ are fine too, the Encoder maps them. The screen keeps its title bar and rules to 39 columns and its text to 38; a line of exactly 40 also works, since the Encoder swallows the RETURN after the C64's own wrap.
A small, runnable board: news items, an Application on modern terminals, rubbs' BBS::Petscii::Screen on a C64.
require 'bbs'
NEWS = [
['Board opens', 'The board is open for callers from today. Call often, and tell your friends.'],
['PETSCII face', 'A Commodore 64 gets the same news on 40 columns, in PETSCII.']
].freeze
# The modern face: an Application with a mouse.
MODERN_APP = BBS::Application.define do
theme BBS::Theme.synchronet
body do
NEWS.each_with_index do |(title, text), i|
add BBS::Widgets::Label.new(text: "#{title}: #{text}", style_key: :text,
bounds: BBS::Widget::Rect.new(3, 3 + i, 74, 1))
end
end
status_hint('F10', 'Log off') { :halt }
end
MODERN = BBS::Flow.define do
app MODERN_APP
end
# The PETSCII face: a 40x25 screen, the news as a numbered cursor menu.
PETSCII = BBS::Flow.define do
call do |_ctx, runner|
screen = BBS::Petscii::Screen.new(runner.session)
while (item = screen.pick('my board', NEWS, right: 'q: log off', &:first))
title, text = item
screen.title(title)
screen.say(text)
screen.pause
end
nil
end
end
BBS.configure do |config|
config.mouse = true
config.mouse_on_connect = false
config.idle_seconds = 600
config.flow = BBS::Terminal.flow(title: 'My Board', modern: MODERN, petscii: PETSCII)
end
BBS.start
On the C64 side, the first screen after RETURN starts like this (rubbs 1.0):
0e 08 93 petscii!: lowercase set, case locked, clear
93 9f 12 20 4d 59 20 42 4f 41 52 44 20 … 51 3a 20 4c 4f 47 20 4f 46 46 20 92 9b
CLEAR, cyan, RVS ON, " my board … q: log off ", RVS OFF, light grey
9f 12 20 31 20 c2 4f 41 52 44 20 4f 50 45 4e 53 20 … 92 9b 0d
the lit item: cyan, RVS ON, " 1 Board opens", RVS OFF
9e 20 32 20 9b d0 c5 d4 d3 c3 c9 c9 20 46 41 43 45 … 0d
yellow " 2 ", light grey "PETSCII face"
BBS::Flow on both faces, the PETSCII face with its ERB screens drawn for 40 columns in screens/petscii/: Impostor::Petscii.flow(sessions: store).BBS::FakeOS whose screens fit 40 columns runs on a PETSCII line unchanged.BBS::Gateway passes a PETSCII caller on to another board, and since it reports no window size, a two-faced board on the other side guesses C64 (see Doors).readkey, readline, settle, NAWScall step and the contextBBS::Gateway