---
title: 'Pixelia BBS'
url: 'https://wiki.teletypegames.org/services/pixelia-bbs'
markdown: 'https://wiki.teletypegames.org/services/pixelia-bbs.md'
lang: en
date: '2026-11-01'
description: 'The art board of the BBS network: classic ANSI art from the 16colo.rs archive, a shared 40×25 canvas that every caller paints on at the same time, and the Wall of the works saved from it. Live at teletypegames.org:2332, in the browser on bbsweb, and through TTG Gate BBS Overview A Ruby app on the…'
taxonomy:
  tag:
    - howto
---

# Pixelia BBS

> The art board of the BBS network: classic ANSI art from the 16colo.rs archive, a shared 40×25 canvas that every caller paints on at the same time, and the Wall of the works saved from it.
> 
> **Live at `teletypegames.org:2332`**, in the browser on [bbsweb](https://bbsweb.teletypegames.org), and through [TTG Gate BBS](https://wiki.teletypegames.org/services/ttg-gate-bbs)

# Overview

A Ruby app on the [**rubbs**](https://git.teletypegames.org/engines/rubbs) gem, like the [Teletype BBS](https://wiki.teletypegames.org/services/teletype-bbs), [Scriptorium BBS](https://wiki.teletypegames.org/services/scriptorium-bbs) and [Commodex BBS](https://wiki.teletypegames.org/services/commodex-bbs).

It has two faces on one port:

- **Modern**: ANSI colours, windows, menus and the mouse (SyncTERM, NetRunner, PuTTY, bbsweb). A piece of art on the whole screen, the canvas in a window with a clickable palette.
- **Commodore 64**: PETSCII on a 40×25 screen, for a C64 terminal program (CCGMS, Novaterm, UltimateTerm) behind a WiFi modem. The canvas is the C64 screen itself, and an ANSI piece is shown 40 columns at a time.

A call starts with rubbs' `BBS::Terminal` question: RETURN takes the guess, `1` picks the modern face, `2` the C64.

# The gallery

**16colo.rs.** The board browses the [16colo.rs](https://16colo.rs) archive through its public API: a year, a pack, a file. It lists the text-art files (`.ans`, `.asc`, `.diz`, `.nfo`, `.txt`, `.ice`, and any file whose SAUCE says ASCII, ANSI or ANSiMation) with their title, artist, group and 16colo.rs address. Nothing is mirrored: a file is fetched when someone opens it (at most 512 KB) and kept in a 100 MB disk cache; the listings stay an hour in memory.

The board renders the art itself: it reads the SAUCE record, CP437, the ANSI.SYS escapes and iCE colours, wraps at the SAUCE width, and stops at 1000 rows. On a modern terminal the piece scrolls on the whole screen. On a C64 it comes in pages of 40 columns: the CGA colours go to the nearest C64 colour, a coloured background becomes reversed video, and `R` or CRSR right steps across a wider piece.

**The Wall** holds the works saved from the canvas, newest first, with title, handle and date, drawn natively on both faces.

# The canvas

One shared 40×25 canvas, the size of the C64 screen, so both faces show it cell for cell. Everyone on it sees the others' strokes within a few seconds. A cell holds one character in one of the 16 C64 colours, as ink on black or reversed; that is what a C64 cell can show, so the picture looks the same everywhere. The characters are the ones both a Unicode terminal and PETSCII have: letters, digits, most signs, box lines, blocks and the half blocks.

**On a modern terminal:**

| Key | Action |
|---|---|
| F3 | Open the canvas |
| Arrows, typing | Move, paint the character |
| F7, F8 | Previous / next ink (or click a swatch) |
| F9 or Ctrl-R | Reverse on and off |
| Insert or Ctrl-G | Glyph mode: letters paint the brush glyphs |
| Mouse | A click places the cursor, a drag or a right click paints |
| Ctrl-Z or Ctrl-U | Undo your own last cells |
| Ctrl-S | Save to the Wall (Alt-S in the form) |
| F2 | 16colo.rs by year |
| F4 | The Wall |
| Alt-X | Log off |

**On a Commodore 64:** CRSR moves, typing paints, DEL rubs out, F3/F4 change the ink, F5 turns reverse on and off, F6 switches glyph mode (the letters are laid out like the keyboard: `Q W E / A S D / Z X C` are the box corners, tees and the cross, `G` is a full block, `T` and `B` the top and bottom halves). RETURN, RUN/STOP or ← opens the command row: ink, reverse, glyphs, save, help, quit. F1 and F7 are left alone, UltimateTerm keeps them. The board sends only the cells that changed, a couple of bytes each, so the canvas stays quick at 1200 baud.

Limits: 20 cells a second per call, one Wall save every five minutes per call, a title of 32 and a handle of 16 characters.

# Configuration and data

| Variable | Default |  |
|---|---|---|
| `PIXELIA_BBS_DATA` | `data` | the canvas, the Wall, the cache |
| `PIXELIA_BBS_API_URL` | `https://api.16colo.rs` | the 16colo.rs API |
| `PIXELIA_BBS_SITE_URL` | `https://16colo.rs` | the files and the links shown |
| `PIXELIA_BBS_USER_AGENT` | `PixeliaBBS/1.0 (+https://teletypegames.org; …)` | sent to 16colo.rs |
| `PIXELIA_BBS_API_TTL` | `3600` | seconds a listing is kept |
| `PIXELIA_BBS_MAX_FILE_KB` | `512` | the largest art file fetched |
| `PIXELIA_BBS_CACHE_MB` | `100` | the art cache's size |
| `PIXELIA_BBS_MAX_ROWS` | `1000` | rows rendered of a piece |
| `PIXELIA_BBS_PAINT_RATE` | `20` | cells a second per call |
| `PIXELIA_BBS_WALL_COOLDOWN` | `300` | seconds between two saves of a call |

The canvas is `data/canvas.json` (written every two seconds), the Wall one JSON file a work in `data/works/`. The admins moderate on the server:

```sh
docker exec pixelia-bbs bin/works list           # the Wall
docker exec pixelia-bbs bin/works show ID
docker exec pixelia-bbs bin/works remove ID
docker exec pixelia-bbs bin/canvas clear         # archives the canvas to data/archive/, then blanks it
```

The board has no secrets.

# Under the hood

A push to master deploys it from Woodpecker (RuboCop, compose check, `ci-deploy pixelia-bbs`); the container is `pixelia-bbs`, in `/srv/stacks/pixelia-bbs`, and `data/` stays between deploys. The entrypoint updates rubbs on start.

The code follows the house layering: `lib/domain` (the colour, glyph, canvas, work, SAUCE and art models; the 16colo.rs, art-cache, canvas and work repositories; `AnsiRenderService`, `GalleryService`, `CanvasService`, `WallService`; the composition root `PixeliaBBS`) knows nothing of telnet, and `lib/ui` holds `UI::Modern` with its canvas widget and viewer, and `UI::Petscii` with a screen copy that sends only the differences.

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [Services](https://wiki.teletypegames.org/services.md)
- Previous: [Impostor BBS](https://wiki.teletypegames.org/services/impostor-bbs.md)
- Next: [Public site](https://wiki.teletypegames.org/services/public-site.md)
