---
title: 'Rajzolás és témák'
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/drawing'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/drawing.md'
lang: hu
date: '2026-10-27'
description: 'Hogyan kerül szín és karakter a képernyőre a rubbs-ban: a BBS::Theme (minden preset és minden szerepkulcs), a BBS::Ansi paletta, a BBS::FrameBuffer, amibe az Application rajzol, és amit összehasonlít, a BBS::Renderer a flow-k ERB screenjeihez, a BBS::Banner, és a BBS::Markdown, ami Markdownból ANS…'
taxonomy:
  tag:
    - reference
---

# Rajzolás és témák

> Hogyan kerül szín és karakter a képernyőre a [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs)-ban: a `BBS::Theme` (minden preset és minden szerepkulcs), a `BBS::Ansi` paletta, a `BBS::FrameBuffer`, amibe az Application rajzol, és amit összehasonlít, a `BBS::Renderer` a flow-k ERB screenjeihez, a `BBS::Banner`, és a `BBS::Markdown`, ami Markdownból ANSI-sorokat készít. Az ezekre épülő widgetek a [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) oldalon vannak. Minden a rubbs 1.0.0 forrásával egyeztetve.

# Témák

## Hogyan működik egy téma

A `BBS::Theme` (lib/bbs/theme.rb) egy név és egy Hash, ami **szerepkulcsokat** képez le ANSI SGR-stringekre. A widgetek sosem égetnek be színt: egy szerepet kérnek (`:button_focused`, `:window_title`, `:listbox_selected`…), és azzal az SGR-rel rajzolnak, amit a téma ad. A téma cseréje az egész applicationt átszínezi.

```ruby
BBS::Theme.new(name, styles)
```

| Tag | Mit csinál |
|---|---|
| `Theme.new(name, styles)` | A `name` bármilyen érték (a presetek Symbolt használnak), a `styles` egy Hash szerepkulcsból SGR-Stringbe. A `Theme::ROLES`-on kívüli kulcsra `ArgumentError`-t dob. |
| `Theme.fetch(name)` | Az ilyen nevű (Symbol vagy String) preset a `Theme::PRESETS`-ből; egy `BBS::Theme`-et változatlanul visszaad. Ismeretlen névre `ArgumentError`. |
| `Theme::ROLES` | Az összes szerepkulcs (a `synchronet` preset kulcsai); a `Theme.roles` ugyanezt adja. |
| `name` | A név. |
| `styles` | A Hash, fagyasztva. |
| `style(key)` / `[key]` | Egy szerep SGR-je, vagy `nil`, ha a témában nincs ilyen. |
| `merge(overrides)` | Új téma ugyanazzal a névvel, a megadott kulcsokat lecserélve vagy hozzáadva. Az eredetit nem bántja; a `Theme::ROLES`-on kívüli kulcsra `ArgumentError`-t dob. |

A téma fagyasztott: a `merge`-dzsel változtatsz rajta, ami újat ad. Ha a Hashből kimarad egy szerep, az nem hiba: a cella `nil` stílust kap, a `FrameBuffer#diff` pedig egy sima `\e[0m` reset után írja ki, így a terminál alapszíneivel jelenik meg.

```ruby
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]
```

## Presetek

A presetek adatok: mindegyik egy Hash a `BBS::Theme::PRESETS`-ben (lib/bbs/theme/presets.rb), és a `BBS::Theme.fetch(:név)` épít belőlük témát; az alábbi öt osztálymetódus ennek rövidítése. Ha nincs megadva téma, az application a `synchronet`-et használja (vagy a `BBS.config.theme`-et, ha az be van állítva).

| Preset | Kinézet |
|---|---|
| `BBS::Theme.synchronet` | Fekete képernyő, szürke szöveg. Kék window-k élénkfehér kerettel és sárga címmel; az aktív window címe fekete világosszürkén. A menüsor és a státuszsor fekete világosszürkén, piros hotkeyekkel, a kijelölt menü fehér zölden. A gombok feketék szürkén, fókuszban fehérek zölden; a beviteli mezők fehérek kéken, fókuszban feketék szürkén. |
| `BBS::Theme.nortoncommander` | Norton Commander: kék képernyő és kék window-k cián kerettel, a címek feketék ciánon (az aktív fehér ciánon). A menüsor és a státuszsor fekete ciánon, a legördülő menük feketék világosszürkén. A gombok fehérek zölden, fókuszban fehérek ciánon; a beviteli mezők feketék ciánon. |
| `BBS::Theme.dosnavigator` | DOS Navigator: világosszürke képernyő és window-k fekete szöveggel, a címek ciánon, kék kiemelések. A gombok feketék szürkén, fókuszban fehérek zölden; a lista kijelölése fekete ciánon. |
| `BBS::Theme.neumanntronics` | Visszafogott 256 színű paletta (`38;5;N` / `48;5;N`): majdnem fekete háttér, kékeszöld szöveg, világos kékeszöld kiemelés, borostyánsárga hangsúlyok és hotkeyek, sötét kékeszöld sávok. 256 színű terminál kell hozzá. A window árnyéka a háttér színe, így a window-knak nincs látható árnyéka. |
| `BBS::Theme.mono` | Egyáltalán nincs szín, csak félkövér, halvány, aláhúzott és inverz, a szín nélküli terminálokhoz. A `screen` és a `window_bg` üres string: a terminál saját háttere. |

## Szerepkulcsok

A `Theme::ROLES`: a presetek összes kulcsa (mind az öt mindet definiálja), mi rajzol vele a rubbs 1.0.0-ban, és a `synchronet`-beli értéke. A „a gem nem használja” jelzésű kulcsok a saját widgetjeidnek és screenjeidnek valók. Az 1.0 kidobta azokat a szerepeket, amelyekkel semmi nem rajzolt: a `check_on`, a `check_off`, a `listbox_header`, a `warning` és a `reset` szerepet.

| Kulcs | Mit színez | `synchronet` |
|---|---|---|
| `screen` | Az egész képernyő, minden más előtt kitöltve: az asztal háttere. | `\e[40m` |
| `text` | A `Label` alapértéke; a `Checkbox`; a `ScrollView` és minden Markdown-sor alapja. | `\e[0;37;40m` |
| `text_dim` | A `TextInput` placeholdere; a Markdown dőlt szövege és az idézetjel. | `\e[0;2;37;40m` |
| `text_bright` | A Markdown első szintű címsorai és a félkövér. | `\e[1;37;40m` |
| `label` | A Markdown harmadik és mélyebb szintű címsorai, a beágyazott kód, a kódblokkok, a vízszintes vonalak és a táblázatkeretek; az `Info` window-k elválasztó vonalai. | `\e[0;36;40m` |
| `accent` | A Markdown második szintű címsorai, a linkek, a listajelek és -számok. | `\e[1;33;40m` |
| `success` | A gem nem használja. | `\e[1;32;40m` |
| `error` | A `Form` window státuszsora (validációs hibák). | `\e[1;31;40m` |
| `window_bg` | A window belseje. | `\e[44m` |
| `window_border` | A window kerete. | `\e[1;37;44m` |
| `window_title` | Az inaktív window címe; a bezáró `[■]` és a zoom gomb, a window száma, az átméretező sarok. | `\e[1;33;44m` |
| `window_title_focused` | Az aktív window címe; mozgatás vagy átméretezés közben az egész keret. | `\e[1;30;47m` |
| `window_text` | A dialógusok és az `Info` window-k szövege. | `\e[0;37;44m` |
| `window_shadow` | A window-k és a legördülő menük árnyéka. | `\e[40m` |
| `menubar` | A menüsor. | `\e[1;30;47m` |
| `menubar_hotkey` | A hotkeyek betűi a menüsorban. | `\e[1;31;47m` |
| `menubar_selected` | A nyitott vagy kiemelt menücím. | `\e[1;37;42m` |
| `menubar_sel_hot` | Ennek a címnek a hotkey-betűje. | `\e[1;33;42m` |
| `menu_bg` | A legördülő menü kitöltése. | `\e[1;37;44m` |
| `menu_text` | A menüpontjai. | `\e[0;37;44m` |
| `menu_hotkey` | A menüpontok hotkey-betűi. | `\e[1;33;44m` |
| `menu_selected` | A kiemelt menüpont. | `\e[1;37;46m` |
| `menu_disabled` | A letiltott menüpontok. | `\e[0;2;37;44m` |
| `menu_border` | A legördülő menü kerete és elválasztó vonalai. | `\e[1;37;44m` |
| `statusbar` | A státuszsor. | `\e[1;30;47m` |
| `statusbar_key` | A billentyűnevek a státuszsor tippjeiben. | `\e[1;31;47m` |
| `button` | A `Button`. | `\e[0;30;47m` |
| `button_focused` | A fókuszban lévő `Button`; a fókuszban lévő `Checkbox` is. | `\e[1;37;42m` |
| `button_hotkey` | A `Button` hotkey-betűje. | `\e[1;31;47m` |
| `button_disabled` | A letiltott `Button`. | `\e[0;2;30;47m` |
| `input` | A `TextInput` és a `TextArea`. | `\e[1;37;44m` |
| `input_focused` | Ugyanezek fókuszban. | `\e[1;30;47m` |
| `input_label` | A mezőcímkék a `Form`, `Info` és `MasterDetail` window-kban, és a `Form` tippje. | `\e[0;36;40m` |
| `listbox` | A `ListBox` sorai. | `\e[0;37;44m` |
| `listbox_selected` | A fókusz nélküli `ListBox` kijelölt sora. | `\e[1;37;46m` |
| `listbox_focused` | A fókuszban lévő `ListBox` kijelölt sora. | `\e[1;30;47m` |
| `scrollbar` | A `ScrollView` görgetősávjának pályája. | `\e[0;36;44m` |
| `scrollbar_thumb` | A csúszkája. | `\e[1;37;46m` |

A presetek minden SGR-értéke az előtér mellett a hátteret is megadja (`37;40`, `37;44`), és többségük `0;`-val kezdődik, így egy stílus sosem örökli az előtte rajzolt cella félkövérségét vagy halványságát. A saját értékeidben is így tégy.

## Téma választása az applicationhöz

A `BBS::Application.define` blokkjában a `theme` egy téma-példányt vagy egy preset nevét várja, akár egy felülíró blokkal együtt:

```ruby
require 'bbs'

APP = BBS::Application.define do
  theme BBS::Theme.nortoncommander
end
```

| Alak | Eredmény |
|---|---|
| `theme BBS::Theme.synchronet` | Az a téma. |
| `theme :synchronet` | Az ilyen nevű preset (`Theme.fetch`; ismeretlen névre `ArgumentError`). |
| `theme some_theme.merge(accent: "\e[1;36;40m")` | Bármilyen `BBS::Theme` példány. |
| `theme(:nortoncommander) { ... }` | Az ilyen nevű preset, a blokk felülírásaival. |
| `theme(BBS::Theme.mono) { ... }` | Az a példány, a blokk felülírásaival. |
| `theme { ... }` | A `synchronet`, a blokk felülírásaival. |
| nincs `theme` hívás | `synchronet`. |

A 0.17.0-ban a csupasz Symbol-alak (`theme :synchronet`) magát a Symbolt tárolta el, és az első újrarajzolás `TypeError`-ral elszállt, az argumentum nélküli `APP.theme` pedig visszaállította a tárolt témát; 0.17.1 óta a Symbol a presetet veszi, és a builder argumentum nélküli `theme`-je az eddig beállított témát adja vissza (`nil`-t, ha nem volt `theme` hívás, ami `synchronet`-et jelent).

Az applicationön belül a `theme` a használt `BBS::Theme`-et adja vissza; ezt kapja a `BBS::Markdown.to_lines`, és ebből színezhetsz saját stringet:

```ruby
helpers do
  def warn_line(text) = "#{theme[:error]}#{text}#{theme[:text]}"
end
```

## Kulcsok felülírása

A `theme` blokkban minden egyargumentumos hívás beállítja az adott kulcsot; a `style(key, value)` ennek explicit alakja, az `inherit` pedig lecseréli az alapot, amire a felülírások kerülnek:

```ruby
require 'bbs'

APP = BBS::Application.define do
  theme :nortoncommander do
    inherit :neumanntronics              # opcionális: másik preset legyen az alap
    window_border "\e[1;36;44m"
    accent        "\e[1;33;44m"
    style :screen, "\e[40m"
  end
end
```

| Hívás | Hatás |
|---|---|
| `KULCS érték` | Beállítja a `KULCS`-ot, a `Theme::ROLES` egyik szerepét. Egy elgépelt kulcs `NoMethodError`-t dob. |
| `style(key, value)` | Ugyanez, explicit módon. |
| `inherit(base)` | Másik alap: presetnév (Symbol) vagy `BBS::Theme`. Az előtte és utána beállított felülírások megmaradnak. |

DSL nélkül a `merge` ugyanezt teszi:

```ruby
require 'bbs'

custom = BBS::Theme.synchronet.merge(
  accent:         "\e[1;36;40m",
  button_focused: "\e[1;37;44m"
)
```

## Saját téma

Egy teljes Hashből építs `BBS::Theme`-et. A legegyszerűbben úgy kapod meg az összes kulcsot, ha egy preset Hashéből indulsz a `Theme::PRESETS`-ben:

```ruby
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
```

# A paletta: BBS::Ansi

A témán kívül minden szín egyetlen palettából jön, a `BBS::Ansi`-ból (lib/bbs/ansi.rb): a flow stílusai, egy soralapú board `say`-e, egy FakeOS, egy logsor, egy gyors door. A `BBS::Color` (`Color.c(:yellow, text)`) az 1.0 óta nincs meg; helyette a konstanst és egy resetet írj.

| Konstans | SGR | Konstans | SGR |
|---|---|---|---|
| `GREY` (sima szöveg) | `\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` |

```ruby
require 'bbs'

p "#{BBS::Ansi::CYAN}hello#{BBS::Ansi::RESET}"    # => "\e[0;36mhello\e[0m"
```

A `BBS::Petscii::Colors` ugyanezeknek a szekvenciáknak a C64-es nevei (`WH YE CY LG GR DG LB BL GN RD PU OR`, valamint `RV RO` az inverz be- és kikapcsolásához; lásd [Commodore 64](https://wiki.teletypegames.org/hu/engines/rubbs/commodore)), és egy komponens ezekből a színekből álló szerep-Hasht kap: a `BBS::Selector::COLORS`-t, a Flow `FlowRunner::STYLES`-át, egy FakeOS `colors`-át. A C64 grafikus vásznán ugyanezekből C64-es színkód lesz: `BBS::Petscii::Canvas.color(YE) == Canvas::YELLOW`.

# BBS::FrameBuffer

A `BBS::FrameBuffer` (lib/bbs/frame\_buffer.rb) egy képernyőn kívüli cellarács; minden cella egy karakter és egy SGR-string. Az Application egy egész képkockát rajzol bele, összeveti az előzővel, és csak a különbséget küldi el. A widgetek a `render(frame)`-ben kapnak egyet.

## Dupla pufferelés és delta diff

Az Application minden újrarajzolása (a privát `render_screen`) ezt csinálja:

1. Készít egy üres, terminálméretű `next` képkockát. Megtartja a `committed` képkockát (amit a terminál most mutat); az első rajzoláskor vagy átméretezés után üresről indul, és letörli a képernyőt.
2. Kitölti a `next`-et a téma `screen` stílusával, kirajzolja az asztalt, a window-kat alulról felfelé, a menüsort és a státuszsort.
3. `delta = next.diff(committed)`; ha nem üres, kurzorrejtés és -mutatás (`\e[?25l` … `\e[?25h`) közé csomagolva kiírja, és a `next` lesz az új committed képkocka.

Így egy billentyűleütés után csak a megváltozott cellák mennek le a vonalon: egy listakijelölés mozgatása két sorba kerül, egy ketyegő óra néhány karakterbe. Kézzel is ugyanez működik:

```ruby
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"   csak a megváltozott számjegy
p nxt.diff(nxt)           # => ""
```

## Konstansok

| Konstans | Érték |
|---|---|
| `Cell` | `Struct.new(:char, :sgr)`. |
| `BLANK` | Egy fagyasztott `Cell` szóközzel, stílus nélkül: ezzel van tele egy új vagy törölt képkocka. |
| `BOX_CHARS` | A `box` által használt `:single` (`┌┐└┘─│`) és `:double` (`╔╗╚╝═║`) vonalkészlet. |

## Osztálymetódusok ANSI-stringekhez

Ezek SGR-escape-eket tartalmazó stringeken dolgoznak, és csak a látható karaktereket számolják. Escape-szekvencia az, amire a `BBS::Ansi::CSI_RE` illeszkedik (`/\e\[[\d;?]*[A-Za-z~]/`: SGR, kurzormozgatás, `\e[2~`-féle billentyűk, és 1.0 óta a privát módú szekvenciák is, mint a `\e[?25l`); ez váltja a 0.x `FrameBuffer::ANSI_ESC_RE`-jét.

| Metódus | Mit csinál |
|---|---|
| `FrameBuffer.text(value)` | `value.to_s`, az érvénytelen bájtokat `?`-re cserélve, hogy egy regexp el tudja olvasni. Az összes többi metódus ezen megy át, így a nyers telnet-bájtok nem dönthetik össze a rajzolást. |
| `FrameBuffer.plain(text)` | A szöveg escape-szekvenciák nélkül. |
| `FrameBuffer.visible_width(text)` | A hossz escape-szekvenciák nélkül. |
| `FrameBuffer.clip_ansi(text, max_width)` | `max_width` látható karakterre vágja a szöveget, a vágás előtti escape-eket megtartva. 0 vagy kisebb szélességre `''`. |
| `FrameBuffer.wrap_ansi(text, width)` | A gem egyetlen tördelője (a dialógusok, a window-k és a Markdown-renderelő is ezt használják). Szavanként tördeli legfeljebb `width` széles sorok tömbjébe (a sorvégi szóközöket levágja). A tördelési ponton aktív SGR-t a következő sor elején megismétli, így a szín átível. A szélességnél hosszabb szót nem vágja el; az külön sorba kerül, és kilóg. 0 vagy kisebb szélességre `['']`. |

```ruby
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"
```

## Rajzolás

Minden koordináta 1-től számozott oszlop és sor, mint a terminálban. A képkockán kívül eső részt bármelyik szélen levágja, így nyugodtan rajzolhatsz részben a képernyőn kívülre.

| Metódus | Mit csinál |
|---|---|
| `FrameBuffer.new(cols, rows)` | Üres képkocka. A `cols` és a `rows` olvasható. |
| `clear` | Minden cellát kiürít, és a kurzort a bal felső sarokba teszi. |
| `move(col, row)` | Beállítja az írási pozíciót. |
| `write(text, sgr: nil)` | A pozíciótól kezdve kiírja a `text` karaktereit, mind egy stílusban, és továbblép. Az escape-eket nem értelmezi, karakterként írná ki őket: stílusos stringhez a `write_ansi` való. |
| `write_ansi(text, base_sgr: nil)` | Beágyazott escape-eket tartalmazó stringet ír ki. Lásd lent. |
| `fill(x:, y:, width:, height:, sgr: nil, char: ' ')` | Kitölt egy téglalapot a `char`-ral. |
| `hline(x:, y:, length:, char: '─', sgr: nil)` | Vízszintes vonal. |
| `vline(x:, y:, length:, char: '│', sgr: nil)` | Függőleges vonal. |
| `box(x:, y:, width:, height:, sgr: nil, fill_sgr: nil, style: :single, title: nil, title_sgr: nil, title_indent: 0)` | Keret `sgr` színben; a belseje szóközökkel kitöltve `fill_sgr` színben, ha meg van adva (különben érintetlen); a `style` `:single` vagy `:double`; a `title` a felső élben, `title_sgr` (vagy `sgr`) színben, `title_indent` oszloppal jobbra tolva és a helyre vágva. 2×2 alatt semmit nem rajzol. |
| `shadow(x:, y:, width:, height:, sgr: nil)` | Árnyék a megadott téglalaphoz: két oszlop jobbra, egy sor alul, egy sorral és két oszloppal eltolva. Az `sgr` alapértéke `\e[40m`. |
| `diff(from)` | Azok az ANSI-bájtok, amelyek a `from` képkockából ezt csinálják (lásd fent). |

A `write_ansi` úgy értelmezi az escape-eket, ahogy egy terminál tenné, a képkockán belül:

- Egy SGR-szekvencia (`\e[...m`) hozzáadódik az aktuális stílushoz; egy reset (`\e[0m` vagy `\e[m`) a `base_sgr`-hez tér vissza, nem a „stílus nélkülihez”. Így tartja meg egy kék window-ban egy színes szó utáni szöveg a kék hátteret.
- A `\e[NC` (kurzor előre) N szóközt ír az aktuális stílusban, ahogy az ANSI-art használja.
- Minden más CSI-szekvencia elvész, a kurzormozgatás (`\e[SOR;OSZLOPH`) is, így egy widgeten belüli szöveg nem ugorhat ki belőle; a 0.x `cursor:` opciója megszűnt. A vezérlőkarakterek is elvesznek (a `\n` és a `\r` is): egy hívás egy sort ír.

```ruby
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')                                        # csak a "cli" fér ki
fb.move(1, 2)
fb.write_ansi("a\e[1mb\e[0mc\e[3Cd", base_sgr: "\e[44m")   # stílusok: 44 / 44+bold / 44, három szóköz, d
fb.move(1, 3)
fb.write_ansi("x\e[4;10Hy")                                # a H nem számít: "xy"
```

Az eredmény:

```text
Hello            cli
abc   d
xy

```

Keretes panel mintás háttéren, árnyékkal:

```ruby
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")
```

```text
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░╔═════ Inbox═══════════╗░░░░░░
░░║ 3 new messages       ║  ░░░░
░░║──────────────────────║  ░░░░
░░║           │          ║  ░░░░
░░║           │          ║  ░░░░
░░╚══════════════════════╝  ░░░░
░░░░                        ░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
```

## Karakterszélesség

A framebuffer Ruby-karakterenként egy cellát számol. Ez helyes az ASCII-ra, az előre komponált ékezetes latin betűkre, a keretrajzoló, blokk- és braille-karakterekre, vagyis mindarra, amit maga a gem rajzol. Hibás viszont:

- a széles karaktereknél (CJK, a legtöbb emoji): egy cellában tárolódnak, de a terminál két oszlopon mutatja őket, így a sor maradéka karakterenként eggyel jobbra csúszik;
- a kombináló jeleknél (`e` plusz U+0301): két cellának számítanak, de egy oszlopon látszanak.

A `visible_width`, a `clip_ansi`, a `wrap_ansi` és a Markdown-táblázatok ugyanígy számolnak. Normalizáld a szöveget NFC-re (`text.unicode_normalize(:nfc)`), és ami pontosan egymás alá kell kerüljön, abba ne tegyél széles karaktert.

# BBS::Renderer: ERB screenek

A `BBS::Renderer` (lib/bbs/renderer.rb) egy ERB-sablonból stringet állít elő egy soros, flow-stílusú screenhez. Ezt használja a Flow `screen` lépése (lásd [Flow](https://wiki.teletypegames.org/hu/engines/rubbs/flow)). A framebufferhez nincs köze: a dupla pufferelést a fent leírt `FrameBuffer#diff` végzi.

```ruby
BBS::Renderer.render(name, vars = {}, dir: BBS.config.screens_dir)
```

| Argumentum | Jelentés |
|---|---|
| `name` | A sablon neve: a `dir` mappa `NÉV.erb` fájlja. |
| `vars` | Egy Hash; minden párból példányváltozó lesz a sablonban (a `username:`-ből `@username`). |
| `dir:` | A sablonmappa. `RuntimeError`-t dob („No screens\_dir: …”), ha sem ez, sem a `BBS.config.screens_dir` nincs beállítva. |

Az ERB `trim_mode: '-'`-dal fut (így a `-%>` elnyeli a sortörést), és az eredmény minden `\n`-je `\r\n` lesz a telnetvonal kedvéért. A sablon ezeket a helpereket hívhatja:

| Helper | Kimenet |
|---|---|
| `clr` | `\e[2J\e[H`: képernyőtörlés, kurzor a sarokba. |
| `reset` | `\e[0m` |
| `green`, `yellow`, `cyan`, `white`, `red` | Az élénk szín (`\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)` | Zöld dupla vonalas keret a szöveg körül. |
| `big_banner(text, font: 'slant')` | FIGlet-betűk (az `artii` gemmel) zölden, két szóközzel beljebb. |

`screens/welcome.erb`:

```erb
<%= clr %><%= banner 'Welcome' %>
   <%= green %>Hello <%= @username %>!<%= reset %>
<% if @new_mail > 0 -%>
   <%= yellow %>You have <%= @new_mail %> new message(s).<%= reset %>
<% end -%>
```

```ruby
require 'bbs'

out = BBS::Renderer.render('welcome', { username: 'tasi', new_mail: 2 }, dir: 'screens')
```

A terminálra írva (a keret és az üdvözlés élénkzöld, a levelekről szóló sor élénksárga):

```text
╔═══════════╗
║  Welcome  ║
╚═══════════╝
   Hello tasi!
   You have 2 new message(s).
```

## BBS::Banner

A Flow `banner` és `big_banner` lépése a `BBS::Banner`-t (lib/bbs/banner.rb) használja, ami küldésre kész, `\r\n` sorvégű, két szóközzel behúzott stringeket ad:

| Metódus | Kimenet |
|---|---|
| `Banner.render(text, color: "\e[1;32m", padding: 2)` | Dupla vonalas keret a szöveg körül, mindkét oldalon `padding` szóközzel. |
| `Banner.render_big(text, color: "\e[1;32m", font: 'slant')` | FIGlet-betűk az `artii`-vel; bármely Artii-fontnév jó (`slant`, `big`, `small`…). |

```ruby
require 'bbs'

print BBS::Banner.render_big('BBS', font: 'small').gsub("\r\n", "\n")
```

```text
    ___ ___ ___
   | _ ) _ ) __|
   | _ \ _ \__ \
   |___/___/___/
```

# BBS::Markdown

A `BBS::Markdown` (lib/bbs/markdown.rb) Markdown-szövegből ANSI-val színezett sorok tömbjét készíti, adott szélességre. Az eredményt egy `BBS::Widgets::ScrollView`-nak, egy görgethető `Info` window-nak adhatod, vagy magad írhatod ki a sorokat.

```ruby
BBS::Markdown.to_lines(text, width:, theme:)
```

| Argumentum | Jelentés |
|---|---|
| `text` | A Markdown. Az érvénytelen bájtokból `?` lesz. |
| `width:` | A tördelési szélesség. Ha a sorok `ScrollView`-ba kerülnek, adj meg eggyel kevesebbet a szélességénél, hogy maradjon hely a görgetősávnak. |
| `theme:` | Egy `BBS::Theme` (vagy bármi, ami válaszol a `[key]`-re); a színek a szerepeiből jönnek. |

Applicationön belül a szokásos hívás:

```ruby
lines = BBS::Markdown.to_lines(text, width: width - 1, theme: ctx[:app].theme)
```

Minden stílusos szakasz a téma `text` SGR-jével zárul, így az utána következő szöveg visszakapja a hátterét: színezett window-háttéren ez számít. A sima bekezdéssorok nem kezdődnek saját SGR-rel; a `ScrollView` `text` alapstílussal rajzolja őket. Ha a sorokat közvetlenül egy sessionre írod, mindegyik elé tedd oda a `theme[:text]`-et.

## Amit ért

| Markdown | Így jelenik meg | Szerep |
|---|---|---|
| `# Címsor` | A címsor szövege `#` nélkül (a záró `#`-ek is eltűnnek), tördelve. | `text_bright` |
| `## Címsor` | Ugyanígy. | `accent` |
| `###`-tól `######`-ig | Ugyanígy, minden szint egyformán. | `label` |
| Bekezdés | Az egymást követő sorok egy-egy szóközzel összefűzve, a whitespace összevonva, szavanként tördelve. | `text` |
| Üres sor | Lezárja a bekezdést, és egy üres sort ad. |  |
| `**félkövér**` | A szöveg. | `text_bright` |
| `*dőlt*`, `_dőlt_` | A szöveg. Szavon belül nem: a `snake_case_name` és az `a*b*c` változatlan marad. | `text_dim` |
| ``kód`` \| A szöveg. \| `label` |
| `[szöveg](url)` | Csak a szöveg; az URL elmarad. | `accent` |
| `> idézet` | Minden sor `│` jellel, a szélesség mínusz 2-re tördelve. | jel: `text_dim` |
| `- elem`, `* elem`, `+ elem` | `• elem`, függő behúzással tördelve. A jel előtti szóközök megmaradnak, így készülnek a beágyazott listák. | jel: `accent` |
| `1. elem` | `1. elem`, a szám úgy, ahogy le van írva, függő behúzással. | szám: `accent` |
| Behúzott sor egy listaelem alatt | Az elem szövegét folytatja. |  |
| `---`, `***`, `___` (legalább három) | Teljes szélességű `─` vonal. | `label` |
| Kódblokk (három backtick, bármilyen info stringgel) | A sorok változatlanul, a szélességre vágva (nem tördelve); a beágyazott ANSI átmegy. | `label` |
| GFM-táblázat (fejléc, `---` elválasztó sor, törzssorok) | Keretrajzoló karakterekkel rajzolt táblázat; a fejléccellák félkövérek; `:--`, `:-:`, `--:` igazítás; a hiányzó cellák kitöltve; ha túl széles, a legszélesebb oszloptól kezdve szűkül, aztán a cellák vágódnak. | keret: `label`, fejléc: `text_bright` |
| `\*`, `\_`, `\``, `\#`, `\[`… | A karakter backslash nélkül (lásd a korlátokat). |  |

Egy minta 36-os szélességen, `synchronet`-tel:

```ruby
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) }
```

Színek nélkül (a terminálban: a `Welcome` élénkfehér, a `Rules` sárga, a `Small print` cián, a `nice` élénkfehér, a `kind` és a `brief` halvány, a `the wiki` sárga, a `help` cián, a listajelek és -számok sárgák, a vonal és a táblázatkeret cián):

```text
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 │      │
└──────┴───────┴──────┘
```

Egy el nem férő táblázat szűkül, aztán a cellái vágódnak (30-as szélesség):

```text
┌──────────┬─────────────────┐
│ Door     │ Description     │
├──────────┼─────────────────┤
│ SERMO    │ An AI character │
│ Impostor │ Social deductio │
└──────────┴─────────────────┘
```

## Korlátok

A forrással ellenőrizve; ezek egyikére se építs:

- **A backslash nem állítja meg a kiemelést.** Az escape-ek a kiemelési szabályok után érvényesülnek, így a `\*not italic\*` mégis dőlt lesz, a backslashek pedig megmaradnak: az eredmény egy backslash, egy halvány `not italic`, majd még egy backslash. Ugyanez a helyzet a `\_x\_`-szel. Ahol nem illeszkedik kiemelési szabály, ott működik az escape: a `2\*3\*4`-ből `2*3*4`, a `\# no heading`-ből `# no heading` lesz.
- A kódszakaszokon belül is eltűnnek az escape-ek: a ``a\*b`` `a*b`-t mutat.
- Nincs `__félkövér__`, `~~áthúzás~~`, HTML vagy entitás (úgy jelennek meg, ahogy le vannak írva), setext-címsor (`===` egy sor alatt), behúzott kódblokk, kemény sortörés (két sorvégi szóköz) és feladatlista (a `- [ ]` megtartja a szögletes zárójelet).
- Képek: a `![alt](src)` egy `!`-et és az alt szöveget mutatja a link színében.
- Beágyazott idézet: a `> > x` egy jelet és egy szó szerinti `> x`-et ad.
- A harmadiktól a hatodik szintig a címsorok egyformák.
- A kódblokkok vágódnak, nem tördelődnek; a szélességnél hosszabb szót nem vágja el.
- A széles karakterek egy oszlopnak számítanak (lásd fent, Karakterszélesség).

# Lásd még

- [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) — áttekintés
- [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) — ami ezekkel a szerepekkel rajzol
- [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) — a `theme` DSL a helyén, a futási ciklus
- [Rajzok és grafikus képernyők](https://wiki.teletypegames.org/hu/engines/rubbs/art) — rajzok, a C64 grafikus vászna
- [Flow](https://wiki.teletypegames.org/hu/engines/rubbs/flow) — a `screen`, `banner` és `big_banner` lépések

'; 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: [RuBBS](https://wiki.teletypegames.org/hu/engines/rubbs.md)
- Previous: [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors.md)
- Next: [Flow](https://wiki.teletypegames.org/hu/engines/rubbs/flow.md)
