How rubbs puts colour and characters on the screen:
BBS::Theme(every preset and every role key), theBBS::Ansipalette, theBBS::FrameBufferthat the Application draws into and diffs,BBS::Rendererfor ERB screens in a flow,BBS::Banner, andBBS::Markdown, which turns Markdown into ANSI lines. Widgets that use all this are on Widgets. Everything here is checked against rubbs 1.0.0.
A BBS::Theme (lib/bbs/theme.rb) is a name and a Hash that maps role keys to ANSI SGR strings. Widgets never hard-code colours; they ask for a role (:button_focused, :window_title, :listbox_selected…) and draw with whatever SGR the theme gives. Switching the theme recolours the whole application.
BBS::Theme.new(name, styles)
| Member | What it does |
|---|---|
Theme.new(name, styles) |
name is any value (the presets use a Symbol), styles a Hash of role key to SGR String. A key outside Theme::ROLES raises ArgumentError. |
Theme.fetch(name) |
The preset of that name (Symbol or String) from Theme::PRESETS; a BBS::Theme is returned as it is. An unknown name raises ArgumentError. |
Theme::ROLES |
Every role key (the keys of the synchronet preset); Theme.roles returns the same. |
name |
The name. |
styles |
The Hash, frozen. |
style(key) / [key] |
The SGR for a role, or nil when the theme does not have it. |
merge(overrides) |
A new theme with the same name and the given keys replaced or added. The original is untouched; a key outside Theme::ROLES raises ArgumentError. |
A theme is frozen: change it with merge, which gives a new one. A role the Hash leaves out is not an error: the cell gets a nil style, and FrameBuffer#diff writes it after a plain \e[0m reset, so it shows in the terminal's default colours.
require 'bbs'
t = BBS::Theme.synchronet
p t[:accent] # => "\e[1;33;40m"
p t.style(:no_such_role) # => nil
cyan = t.merge(accent: "\e[1;36;40m")
p [cyan.name, cyan[:accent]] # => [:synchronet, "\e[1;36;40m"]
p t.frozen? # => true
t.merge(check_on: "\e[1;32m") # ArgumentError: unknown theme roles: [:check_on]
The presets are data: one Hash each in BBS::Theme::PRESETS (lib/bbs/theme/presets.rb), and BBS::Theme.fetch(:name) builds one; the five class methods below are shorthands for it. The application uses synchronet when none is given (or BBS.config.theme, when set).
| Preset | Looks like |
|---|---|
BBS::Theme.synchronet |
Black screen, grey text. Blue windows with a bright white frame and yellow titles; the active window's title black on light grey. Menubar and statusbar black on light grey with red hotkeys, the selected menu white on green. Buttons black on grey, focused white on green; inputs white on blue, focused black on grey. |
BBS::Theme.nortoncommander |
Norton Commander: blue screen and blue windows with cyan frames, titles black on cyan (active: white on cyan). Menubar and statusbar black on cyan, drop-down menus black on light grey. Buttons white on green, focused white on cyan; inputs black on cyan. |
BBS::Theme.dosnavigator |
DOS Navigator: light grey screen and windows with black text, titles on cyan, blue accents. Buttons black on grey, focused white on green; list selection black on cyan. |
BBS::Theme.neumanntronics |
A muted 256-colour palette (38;5;N / 48;5;N): near-black background, teal text, light teal highlights, amber accents and hotkeys, dark teal bars. Needs a 256-colour terminal. The window shadow is the background colour, so windows have no visible shadow. |
BBS::Theme.mono |
No colour at all, only bold, dim, underline and reverse video, for terminals without colour. screen and window_bg are empty strings: the terminal's own background. |
Theme::ROLES: every key the presets define (all five define all of them), what draws with it in rubbs 1.0.0, and its synchronet value. Keys marked "not used by the gem" are there for your own widgets and screens. 1.0 dropped the roles nothing drew with: check_on, check_off, listbox_header, warning and reset.
| Key | What it colours | synchronet |
|---|---|---|
screen |
The whole screen, filled before anything else is drawn: the desktop background. | \e[40m |
text |
Default for Label; Checkbox; the base of ScrollView and every Markdown line. |
\e[0;37;40m |
text_dim |
TextInput placeholder; Markdown italics and the quote gutter. |
\e[0;2;37;40m |
text_bright |
Markdown level-1 headings and bold. | \e[1;37;40m |
label |
Markdown level-3+ headings, inline code, code blocks, rules and table borders; separator lines in Info windows. |
\e[0;36;40m |
accent |
Markdown level-2 headings, links, list bullets and numbers. | \e[1;33;40m |
success |
Not used by the gem. | \e[1;32;40m |
error |
The status line of a Form window (validation errors). |
\e[1;31;40m |
window_bg |
The inside of a window. | \e[44m |
window_border |
The frame of a window. | \e[1;37;44m |
window_title |
The title of an inactive window; the close [■] and zoom gadgets, the window number, the resize corner. |
\e[1;33;44m |
window_title_focused |
The title of the active window; the whole frame while a window is being moved or sized. | \e[1;30;47m |
window_text |
Text in dialogs and Info windows. |
\e[0;37;44m |
window_shadow |
The drop shadow of windows and drop-down menus. | \e[40m |
menubar |
The menubar row. | \e[1;30;47m |
menubar_hotkey |
Hotkey letters in the menubar. | \e[1;31;47m |
menubar_selected |
The open or highlighted menu title. | \e[1;37;42m |
menubar_sel_hot |
The hotkey letter of that title. | \e[1;33;42m |
menu_bg |
The fill of a drop-down menu. | \e[1;37;44m |
menu_text |
Its items. | \e[0;37;44m |
menu_hotkey |
Hotkey letters of the items. | \e[1;33;44m |
menu_selected |
The highlighted item. | \e[1;37;46m |
menu_disabled |
Disabled items. | \e[0;2;37;44m |
menu_border |
The frame of the drop-down and its separator lines. | \e[1;37;44m |
statusbar |
The statusbar row. | \e[1;30;47m |
statusbar_key |
The key names in the statusbar hints. | \e[1;31;47m |
button |
A Button. |
\e[0;30;47m |
button_focused |
A focused Button; also a focused Checkbox. |
\e[1;37;42m |
button_hotkey |
The hotkey letter of a Button. |
\e[1;31;47m |
button_disabled |
A disabled Button. |
\e[0;2;30;47m |
input |
TextInput and TextArea. |
\e[1;37;44m |
input_focused |
The same, focused. | \e[1;30;47m |
input_label |
Field labels in Form, Info and MasterDetail windows, and the hint of a Form. |
\e[0;36;40m |
listbox |
ListBox rows. |
\e[0;37;44m |
listbox_selected |
The selected row of an unfocused ListBox. |
\e[1;37;46m |
listbox_focused |
The selected row of a focused ListBox. |
\e[1;30;47m |
scrollbar |
The ScrollView scrollbar track. |
\e[0;36;44m |
scrollbar_thumb |
Its thumb. | \e[1;37;46m |
Each SGR value in the presets starts with the background as well as the foreground (37;40, 37;44), and most start with 0;, so a style never inherits bold or dim from the cell drawn before it. Do the same in your own values.
In the BBS::Application.define block, theme takes a theme instance or a preset name, or either of them plus a block of overrides:
require 'bbs'
APP = BBS::Application.define do
theme BBS::Theme.nortoncommander
end
| Form | Result |
|---|---|
theme BBS::Theme.synchronet |
That theme. |
theme :synchronet |
The preset of that name (Theme.fetch; an unknown name raises ArgumentError). |
theme some_theme.merge(accent: "\e[1;36;40m") |
Any BBS::Theme instance. |
theme(:nortoncommander) { ... } |
The preset of that name, with the block's overrides. |
theme(BBS::Theme.mono) { ... } |
That instance, with the block's overrides. |
theme { ... } |
synchronet, with the block's overrides. |
no theme call |
synchronet. |
In 0.17.0 the bare Symbol form (theme :synchronet) stored the Symbol itself and crashed the first redraw with a TypeError, and APP.theme without arguments reset the stored theme; since 0.17.1 the Symbol takes the preset, and theme without arguments on the builder returns the theme set so far (nil when there was no theme call, which means synchronet).
Inside the application, theme returns the BBS::Theme in use, which is what you pass to BBS::Markdown.to_lines or use for a coloured string of your own:
helpers do
def warn_line(text) = "#{theme[:error]}#{text}#{theme[:text]}"
end
Inside a theme block every call with one argument sets that key; style(key, value) is the explicit form, and inherit switches the base the overrides are applied to:
require 'bbs'
APP = BBS::Application.define do
theme :nortoncommander do
inherit :neumanntronics # optional: take another preset as the base
window_border "\e[1;36;44m"
accent "\e[1;33;44m"
style :screen, "\e[40m"
end
end
| Call | Effect |
|---|---|
KEY value |
Sets KEY, a role of Theme::ROLES. A misspelled key raises NoMethodError. |
style(key, value) |
The same, explicitly. |
inherit(base) |
Use another base: a preset name (Symbol) or a BBS::Theme. Overrides set before and after it are kept. |
Without the DSL, merge does the same:
require 'bbs'
custom = BBS::Theme.synchronet.merge(
accent: "\e[1;36;40m",
button_focused: "\e[1;37;44m"
)
Build a BBS::Theme from a full Hash. The simplest way to get every key is to start from a preset's Hash in Theme::PRESETS:
require 'bbs'
AMBER = BBS::Theme.new(:amber, BBS::Theme::PRESETS[:mono].merge(
screen: "\e[40m",
text: "\e[0;33;40m",
text_dim: "\e[0;2;33;40m",
text_bright: "\e[1;33;40m",
label: "\e[0;33;40m",
accent: "\e[1;33;40m",
window_bg: "\e[40m",
window_border: "\e[0;33;40m",
window_title: "\e[1;33;40m",
window_title_focused: "\e[0;30;43m",
window_text: "\e[0;33;40m",
button: "\e[0;30;43m",
button_focused: "\e[1;37;43m",
input: "\e[0;33;40m",
input_focused: "\e[0;30;43m",
listbox: "\e[0;33;40m",
listbox_selected: "\e[0;30;43m",
listbox_focused: "\e[1;37;43m"
))
APP = BBS::Application.define do
theme AMBER
end
Every colour outside a theme comes from one palette, BBS::Ansi (lib/bbs/ansi.rb): a flow's styles, a line-by-line board's say, a FakeOS, a log line, a quick door. BBS::Color (Color.c(:yellow, text)) is gone since 1.0; write the constant and a reset instead.
| Constant | SGR | Constant | SGR |
|---|---|---|---|
GREY (plain text) |
\e[0;37m |
BRIGHT_YELLOW |
\e[1;33m |
DIM_GREY |
\e[2;37m |
BLUE |
\e[0;34m |
DARK_GREY |
\e[1;30m |
BRIGHT_BLUE |
\e[1;34m |
WHITE |
\e[1;37m |
MAGENTA |
\e[0;35m |
BRIGHT_RED |
\e[1;31m |
CYAN |
\e[0;36m |
GREEN |
\e[0;32m |
BRIGHT_CYAN |
\e[1;36m |
BRIGHT_GREEN |
\e[1;32m |
REVERSE / REVERSE_OFF |
\e[7m / \e[27m |
YELLOW |
\e[0;33m |
RESET |
\e[0m |
require 'bbs'
p "#{BBS::Ansi::CYAN}hello#{BBS::Ansi::RESET}" # => "\e[0;36mhello\e[0m"
BBS::Petscii::Colors are the C64 names of the same sequences (WH YE CY LG GR DG LB BL GN RD PU OR, RV RO for reverse on and off; see Commodore 64), and a component takes a Hash of roles in these colours: BBS::Selector::COLORS, the Flow's FlowRunner::STYLES, a FakeOS's colors. On a C64 graphics canvas the same colours turn into C64 codes: BBS::Petscii::Canvas.color(YE) == Canvas::YELLOW.
BBS::FrameBuffer (lib/bbs/frame_buffer.rb) is an off-screen grid of cells, each a character and an SGR string. The Application draws a whole frame into one, compares it with the previous frame, and sends only the difference. Widgets get one in render(frame).
Every redraw of the Application (the private render_screen) does this:
next frame of the terminal's size. Keep the committed frame (what the terminal shows now); on the first draw or after a resize, start from an empty one and clear the screen.next with the theme's screen style, draw the desktop, the windows bottom to top, the menubar and the statusbar.delta = next.diff(committed); if it is not empty, write it wrapped in cursor hide/show (\e[?25l … \e[?25h) and make next the committed frame.So after a key press only the cells that changed travel down the line: moving a list selection costs two rows, a ticking clock a few characters. The same works by hand:
require 'bbs'
committed = BBS::FrameBuffer.new(20, 2)
nxt = BBS::FrameBuffer.new(20, 2)
nxt.move(1, 1)
nxt.write('Time: 12:00', sgr: "\e[1;33m")
p nxt.diff(committed) # => "\e[1;1H\e[0m\e[1;33mTime: 12:00\e[0m"
committed = nxt
nxt = BBS::FrameBuffer.new(20, 2)
nxt.move(1, 1)
nxt.write('Time: 12:01', sgr: "\e[1;33m")
p nxt.diff(committed) # => "\e[1;11H\e[0m\e[1;33m1\e[0m" only the changed digit
p nxt.diff(nxt) # => ""
| Constant | Value |
|---|---|
Cell |
Struct.new(:char, :sgr). |
BLANK |
A frozen Cell with a space and no style: what a new or cleared frame is filled with. |
BOX_CHARS |
The :single (┌┐└┘─│) and :double (╔╗╚╝═║) line sets used by box. |
These work on strings that contain SGR escapes, counting only the visible characters. An escape sequence is what BBS::Ansi::CSI_RE matches (/\e\[[\d;?]*[A-Za-z~]/: SGR, cursor moves, \e[2~-style keys, and since 1.0 private-mode sequences such as \e[?25l); it replaces the FrameBuffer::ANSI_ESC_RE of 0.x.
| Method | What it does |
|---|---|
FrameBuffer.text(value) |
value.to_s, with invalid bytes replaced by ? so a regexp can read it. Every other method here goes through it, so raw telnet bytes cannot crash a render. |
FrameBuffer.plain(text) |
The text without escape sequences. |
FrameBuffer.visible_width(text) |
The length without escape sequences. |
FrameBuffer.clip_ansi(text, max_width) |
Cuts the text to max_width visible characters, keeping the escapes before the cut. '' for a width of 0 or less. |
FrameBuffer.wrap_ansi(text, width) |
The one word-wrap of the gem (dialogs, windows and the Markdown renderer use it too). Word-wraps into an Array of lines no wider than width (trailing spaces trimmed). The active SGR at a wrap point is repeated at the start of the next line, so colour carries over. A word longer than the width is not broken; it gets a line of its own and overflows. [''] for a width of 0 or less. |
require 'bbs'
fb = BBS::FrameBuffer
p fb.plain("\e[?25l\e[1;33mbright\e[0m") # => "bright"
p fb.visible_width("\e[1;33mbright\e[0m normal") # => 13
p fb.clip_ansi("\e[1;33mbright\e[0m normal", 4) # => "\e[1;33mbrig"
p fb.wrap_ansi("\e[1;32mthe quick brown fox jumps\e[0m over", 10)
# => ["\e[1;32mthe quick", "\e[1;32mbrown fox", "\e[1;32mjumps\e[0m over"]
p fb.wrap_ansi('anextremelylongword here', 8) # => ["anextremelylongword", "here"]
p fb.text("ok\xFFok") # => "ok?ok"
All coordinates are 1-based columns and rows, like the terminal's. Anything outside the frame is clipped, at any edge, so you can draw partly off-screen safely.
| Method | What it does |
|---|---|
FrameBuffer.new(cols, rows) |
An empty frame. cols and rows are readable. |
clear |
Blanks every cell and moves the cursor to the top left. |
move(col, row) |
Sets the write position. |
write(text, sgr: nil) |
Writes the characters of text from the position, all in one style, and advances. Escapes are not interpreted, they would be written as characters: use write_ansi for styled strings. |
write_ansi(text, base_sgr: nil) |
Writes a string with embedded escapes. See below. |
fill(x:, y:, width:, height:, sgr: nil, char: ' ') |
Fills a rectangle with char. |
hline(x:, y:, length:, char: '─', sgr: nil) |
A horizontal line. |
vline(x:, y:, length:, char: '│', sgr: nil) |
A vertical line. |
box(x:, y:, width:, height:, sgr: nil, fill_sgr: nil, style: :single, title: nil, title_sgr: nil, title_indent: 0) |
A frame in sgr; the inside filled with spaces in fill_sgr when given (else left alone); style is :single or :double; title drawn in the top edge in title_sgr (or sgr), shifted right by title_indent columns and clipped to fit. Nothing is drawn below 2×2. |
shadow(x:, y:, width:, height:, sgr: nil) |
A drop shadow for the given rectangle: two columns at the right, one row below, offset by one row and two columns. sgr defaults to \e[40m. |
diff(from) |
The ANSI bytes that turn the frame from into this one (see above). |
write_ansi interprets the escapes the way a terminal would, inside the frame:
\e[...m) adds to the current style; a reset (\e[0m or \e[m) returns to base_sgr, not to "no style". This is how a coloured word inside a blue window keeps the blue background after it.\e[NC (cursor forward) writes N spaces in the current style, as ANSI art uses it.\e[ROW;COLH) included, so text inside a widget cannot jump out of it; the cursor: option of 0.x is gone. Control characters are dropped too (including \n and \r): one call writes one line.require 'bbs'
fb = BBS::FrameBuffer.new(20, 4)
fb.move(1, 1)
fb.write('Hello', sgr: "\e[1;33m")
fb.move(18, 1)
fb.write('clipped') # only "cli" fits
fb.move(1, 2)
fb.write_ansi("a\e[1mb\e[0mc\e[3Cd", base_sgr: "\e[44m") # styles: 44 / 44+bold / 44, three spaces, d
fb.move(1, 3)
fb.write_ansi("x\e[4;10Hy") # H ignored: "xy"
The resulting frame:
Hello cli
abc d
xy
A framed panel on a patterned background, with its shadow:
require 'bbs'
fb = BBS::FrameBuffer.new(32, 9)
fb.fill(x: 1, y: 1, width: 32, height: 9, sgr: "\e[44m", char: '░')
fb.box(x: 3, y: 2, width: 24, height: 6, sgr: "\e[1;37;44m", fill_sgr: "\e[44m",
style: :double, title: 'Inbox', title_sgr: "\e[1;33;44m", title_indent: 4)
fb.shadow(x: 3, y: 2, width: 24, height: 6)
fb.hline(x: 4, y: 4, length: 22)
fb.vline(x: 15, y: 5, length: 2)
fb.move(5, 3)
fb.write('3 new messages', sgr: "\e[0;37;44m")
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░╔═════ Inbox═══════════╗░░░░░░
░░║ 3 new messages ║ ░░░░
░░║──────────────────────║ ░░░░
░░║ │ ║ ░░░░
░░║ │ ║ ░░░░
░░╚══════════════════════╝ ░░░░
░░░░ ░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
The frame buffer counts one cell per Ruby character. That is right for ASCII, Latin letters with precomposed accents, box drawing, block and braille characters, which is everything the gem itself draws. It is wrong for:
e plus U+0301): counted as two cells but shown in one.visible_width, clip_ansi, wrap_ansi and Markdown tables count the same way. Normalise text to NFC (text.unicode_normalize(:nfc)) and keep wide characters out of anything that has to line up.
BBS::Renderer (lib/bbs/renderer.rb) renders an ERB template into a string for a sequential, flow-style screen. It is what the Flow screen step uses (see Flow). It has nothing to do with the frame buffer: the double buffering is done by FrameBuffer#diff, described above.
BBS::Renderer.render(name, vars = {}, dir: BBS.config.screens_dir)
| Argument | Meaning |
|---|---|
name |
The template name: the file NAME.erb in dir. |
vars |
A Hash; each pair becomes an instance variable in the template (username: becomes @username). |
dir: |
The template directory. Raises RuntimeError ("No screens_dir: …") when neither this nor BBS.config.screens_dir is set. |
ERB runs with trim_mode: '-' (so -%> swallows the newline), and every \n in the result is turned into \r\n for the telnet line. The template can call these helpers:
| Helper | Output |
|---|---|
clr |
\e[2J\e[H: clear the screen, cursor home. |
reset |
\e[0m |
green, yellow, cyan, white, red |
The bright colour (\e[1;32m, \e[1;33m, \e[1;36m, \e[1;37m, \e[1;31m). |
gray, dim_green |
\e[0;37m, \e[0;32m |
banner(text, padding: 2) |
A green double-line box around the text. |
big_banner(text, font: 'slant') |
FIGlet lettering (the artii gem) in green, indented by two spaces. |
screens/welcome.erb:
<%= clr %><%= banner 'Welcome' %>
<%= green %>Hello <%= @username %>!<%= reset %>
<% if @new_mail > 0 -%>
<%= yellow %>You have <%= @new_mail %> new message(s).<%= reset %>
<% end -%>
require 'bbs'
out = BBS::Renderer.render('welcome', { username: 'tasi', new_mail: 2 }, dir: 'screens')
Written to the terminal (the box and greeting bright green, the mail line bright yellow):
╔═══════════╗
║ Welcome ║
╚═══════════╝
Hello tasi!
You have 2 new message(s).
The Flow banner and big_banner steps use BBS::Banner (lib/bbs/banner.rb), which returns ready-to-send strings with \r\n line ends and a two-space indent:
| Method | Output |
|---|---|
Banner.render(text, color: "\e[1;32m", padding: 2) |
A double-line box around the text, with padding spaces on each side. |
Banner.render_big(text, color: "\e[1;32m", font: 'slant') |
FIGlet lettering via artii; any Artii font name (slant, big, small…). |
require 'bbs'
print BBS::Banner.render_big('BBS', font: 'small').gsub("\r\n", "\n")
___ ___ ___
| _ ) _ ) __|
| _ \ _ \__ \
|___/___/___/
BBS::Markdown (lib/bbs/markdown.rb) turns Markdown text into an Array of ANSI-coloured lines that fit a given width. Feed the result to a BBS::Widgets::ScrollView, an Info window with scrolling, or write the lines out yourself.
BBS::Markdown.to_lines(text, width:, theme:)
| Argument | Meaning |
|---|---|
text |
The Markdown. Invalid bytes become ?. |
width: |
The width to wrap to. When the lines go into a ScrollView, pass one less than its width to leave room for the scrollbar. |
theme: |
A BBS::Theme (or anything that answers [key]); the colours come from its roles. |
Inside an application, the usual call is:
lines = BBS::Markdown.to_lines(text, width: width - 1, theme: ctx[:app].theme)
Each styled span ends with the theme's text SGR, so the text after it gets its background back: this matters on a tinted window background. Plain paragraph lines do not start with an SGR of their own; the ScrollView draws them with text as the base style. When you write the lines straight to a session, put theme[:text] in front of each.
| Markdown | Renders as | Role |
|---|---|---|
# Heading |
The heading text, no # (trailing #s dropped too), wrapped. |
text_bright |
## Heading |
Same. | accent |
### to ###### |
Same, all levels alike. | label |
| Paragraph | Consecutive lines joined with single spaces, whitespace collapsed, word-wrapped. | text |
| Blank line | Ends the paragraph and gives one empty line. | |
**bold** |
The text. | text_bright |
*italic*, _italic_ |
The text. Not inside words: snake_case_name and a*b*c stay as they are. |
text_dim |
`code` | The text. | label |
||
[text](url) |
Only the text; the URL is dropped. | accent |
> quote |
Each line with a │ gutter, wrapped to the width minus 2. |
gutter text_dim |
- item, * item, + item |
• item, wrapped with a hanging indent. Leading spaces of the marker are kept, which gives nested lists. |
bullet accent |
1. item |
1. item, the number as written, hanging indent. |
number accent |
| Indented line under a list item | Continues the item's text. | |
---, ***, ___ (three or more) |
A ─ rule across the full width. |
label |
| Code fence (three backticks, any info string) | The lines verbatim, clipped (not wrapped) to the width; embedded ANSI passes through. | label |
GFM table (header row, --- delimiter row, body rows) |
A box-drawn table; header cells bold; :--, :-:, --: alignment; missing cells filled; columns shrunk from the widest when the table is too wide, cells then clipped. |
borders label, header text_bright |
\*, \_, \`, \#, \[… |
The character without the backslash (see the limits). |
A sample, rendered at width 36 with synchronet:
require 'bbs'
md = <<~MD
# Welcome
## Rules
### Small print
Be **nice**, *kind* and _brief_. See [the wiki](https://wiki.example) or run `help`.
This line joins the paragraph above and wraps at the width.
> Quoted text gets a gutter
> on every line.
- first item
- second item that is long enough to wrap onto a second line
continued here
- nested
1. one
2. two
---
| Name | Calls | Note |
|:-----|------:|:----:|
| ra | 12 | ok |
| **zed** | 3 | |
MD
lines = BBS::Markdown.to_lines(md, width: 36, theme: BBS::Theme.synchronet)
puts lines.map { |l| BBS::FrameBuffer.plain(l) }
Without the colours (in the terminal: Welcome bright white, Rules yellow, Small print cyan, nice bright white, kind and brief dim, the wiki yellow, help cyan, the bullets and numbers yellow, the rule and the table frame cyan):
Welcome
Rules
Small print
Be nice, kind and brief. See the
wiki or run help. This line joins
the paragraph above and wraps at the
width.
│ Quoted text gets a gutter
│ on every line.
• first item
• second item that is long enough to
wrap onto a second line continued
here
• nested
1. one
2. two
────────────────────────────────────
┌──────┬───────┬──────┐
│ Name │ Calls │ Note │
├──────┼───────┼──────┤
│ ra │ 12 │ ok │
│ zed │ 3 │ │
└──────┴───────┴──────┘
A table that does not fit is shrunk, then its cells are clipped (width 30):
┌──────────┬─────────────────┐
│ Door │ Description │
├──────────┼─────────────────┤
│ SERMO │ An AI character │
│ Impostor │ Social deductio │
└──────────┴─────────────────┘
Checked against the source; rely on none of these working:
\*not italic\* still becomes italic and the backslashes stay: the result is a backslash, a dim not italic, and a backslash. The same goes for \_x\_. An escape does work where no emphasis rule matches, e.g. 2\*3\*4 gives 2*3*4 and \# no heading gives # no heading.`a\*b` shows a*b.__bold__, ~~strike~~, HTML or entities (shown as written), setext headings (=== under a line), indented code blocks, hard line breaks (two trailing spaces), or task lists (- [ ] keeps the brackets). shows ! and the alt text in the link colour.> > x gives one gutter and a literal > x.theme DSL in context, the run loopscreen, banner and big_banner steps