---
title: Window-k
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/windows'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/windows.md'
lang: hu
date: '2026-10-27'
description: 'A window-k egy rubbs-application munkafelületei: keretesek, számozottak, válthatók, mozgathatók, és ha bármekkora méretre ki tudják rakni magukat, méretezhetők és nagyíthatók is, Turbo Vision-stílusban. Ez az oldal a BBS::Window-ról, a window DSL-ről (BBS::WindowSpec), a négy kész window-típusról …'
taxonomy:
  tag:
    - reference
---

# Window-k

> A window-k egy [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs)-[application](https://wiki.teletypegames.org/hu/engines/rubbs/application) munkafelületei: keretesek, számozottak, válthatók, mozgathatók, és ha bármekkora méretre ki tudják rakni magukat, méretezhetők és nagyíthatók is, Turbo Vision-stílusban. Ez az oldal a `BBS::Window`-ról, a window DSL-ről (`BBS::WindowSpec`), a négy kész window-típusról (`Info`, `Form`, `MasterDetail`, `Stream`), a `ButtonBar` segédről és a `BBS::Dialogs`-ról szól. A window-kban lévő vezérlők a [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) oldalon vannak.

# Window háromféleképpen

| Mód | Mikor | Példa |
|---|---|---|
| kész típus, imperatívan | kódból nyitott egyszeri window-k | `BBS::Windows::Form.open(app, title: ..., fields: [...], on_submit: ...)` |
| kész típus, a window DSL-lel | egy application név szerint nyitott window-i | `window :profile, type: :form do \\|w\\| ... end`, aztán `open_window(:profile)` |
| `BBS::Window` kézzel, vagy egy leszármazott | amit a típusok nem fednek le | `BBS::Window.new(title: ...)`, `layout`, `add`, `open_window(win)` |

A kész típusok a `BBS::Window` leszármazottai: az első fejezetben leírtak rájuk is érvényesek.

Az oldal minden példája teljes program vagy teljes definíció: a definíciót egy flow `call` lépéséből indítsd, ahogy az [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) oldalon látható, vagy a saját szerveredből.

# BBS::Window

A `BBS::Window` (`lib/bbs/window.rb`) egy `BBS::Container` kerettel, címmel, árnyékkal, bezárógombbal és saját fókuszkezelővel. Az application veremben tartja a nyitott window-kat; az utolsó az aktív, az kapja elsőként a bemenetet.

## Konstruktor és attribútumok

```text
BBS::Window.new(title: nil, modal: false, shadow: true, on_close: nil,
                close_key: :escape, maximizable: false, movable: true)
```

| Attribútum | Alapérték | Jelentés |
|---|---|---|
| `title` | `nil` | szöveg a felső keretben; szóközzel érdemes körülvenni (`' Inbox '`). Levágódik, ha a gomboknak kell a hely |
| `modal` | `false` | a modális window elöl marad, amíg be nem zárják: amíg nyitva van, nincs váltás, nincs száma, mindig dupla a kerete. A dialógusok modálisak, a kész típusok nem |
| `shadow` | `true` | a vetett árnyék (nagyítva sosem rajzolódik) |
| `on_close` | `nil` | a window-val hívódik, amikor az application bezárja. `false` eltünteti a `[■]` bezárógombot (az Esc ettől még bezár) |
| `close_key` | `:escape` | a billentyű, ami bezárja a window-t, ha egy widget sem vette el; `nil`: nincs ilyen |
| `maximizable` | `false` | nagyítógombot ad egy `resizable?` window-nak |
| `movable` | `true` | a hívó mozgathatja (és ha `resizable?`, méretezheti) |
| `number` | az `open_window` adja | 1–9, a felső keret jobb oldalán; az `Alt-N` ezt a window-t választja |

Mind írható-olvasható attribútum (`win.title = ' Új cím '`). Az `app=`-et az `open_window` állítja be.

## Kézzel épített window

A window helyét a `layout(x, y, width, height)` adja meg (1-től számozott képernyőcellák, a kerettel együtt), a gyerekeit képernyő-koordinátákban helyezd el benne, add hozzá őket, aztán `reset_focus`, és nyisd meg:

```ruby
require 'bbs'

def open_hello(app)
  win = BBS::Window.new(title: ' Hello ', on_close: ->(_w) { app.context[:closed] = true })
  win.layout(20, 6, 40, 7)
  label = BBS::Widgets::Label.new(text: 'A window built by hand.', style_key: :window_text)
  label.layout(22, 8, 36, 1)
  win.add(label)
  ok = BBS::Widgets::Button.new(label: '&OK', on_click: ->(_b) { app.close_window(win) })
  ok.layout(36, 10, 8, 1)
  win.add(ok)
  win.reset_focus
  app.open_window(win)
end

APP = BBS::Application.define do
  status_hint('F2', 'Hello') { open_hello(self) }
  status_hint('Alt-X', 'Bye') { :halt }
end
```

A `BBS::Dialogs.centred_window(app, width:, height:, title:)` egy középre tett, kirakott window-t ad kiindulásnak (modálisat; állítsd `modal = false`-ra, ha váltható legyen).

## Metódusok

| Metódus | Mit csinál |
|---|---|
| `focus_manager` | a window `BBS::FocusManager`-e, első használatkor jön létre |
| `reset_focus` | új fókuszkezelő az első fókuszálható gyereken; widgetek hozzáadása után hívd |
| `close` | `:close_window`-t ad vissza; az ezt visszaadó kezelő hatására az application bezárja a window-t (és egyszer meghívja az `on_close`-t) |
| `tick(app)` | nagyjából másodpercenként hívódik, amíg a hívó tétlen; üres, lekérdezéshez írd felül |
| `relayout(app)` | a terminál átméretezése után hívódik (lásd lent) |
| `shift(dx, dy)` | eltolja a window-t az összes leszármazottjával |
| `move_to(x, y, width = a mostani, height = a mostani)` | áthelyezi (és ha `resizable?`, átméretezi) a window-t, az asztalon belül és legalább 24×7-esre; ettől kezdve a window ezt a helyet tartja |
| `toggle_maximize` | nagyítás vagy visszaállítás, ha `zoomable?` |
| `maximized?`, `busy?` | nagyítva van; épp mozgatják vagy méretezik |
| `resizable?` | itt `false`; azokban a window-kban igaz, amik bármekkora méretre kirakhatók |
| `zoomable?`, `movable?`, `sizable?` | `maximizable && resizable?`; `movable` és nincs nagyítva; mozgatható és átméretezhető |
| `normal_rect`, `place(x, y, w, h)` | az átméretezhető window-k elrendező horgonyai (lásd lent) |

## Hogyan kezeli a window a bemenetet

A `Window#handle_event` sorrendje:

1. Billentyűzetes mozgatás vagy méretezés közben minden billentyű ahhoz tartozik.
2. A címsor vagy a sarok egérrel húzása követi az egeret.
3. Az **F5** vagy az **Alt-Enter** nagyít vagy visszaállít (ha `zoomable?`).
4. A **Ctrl-F5** vagy az **F6** elindítja a billentyűzetes mozgató/méretező módot (ha `movable?`). Az F6 csak akkor jut ide, ha nem váltott window-t, vagyis ha ez az egyetlen váltható window.
5. Bal kattintás: a `[■]`-on bezár, a `[↑]`/`[↕]`-on vagy dupla kattintással a címsoron nagyít/visszaállít, a `┘` sarkon méretezésbe, a címsoron mozgatásba kezd; máshol a mutató alatti widgetre teszi a fókuszt.
6. Az egérgörgő a mutató alatti widgethez megy.
7. A Tab és a Shift-Tab a fókuszt mozgatja; utána a fókuszált widget kapja az eseményt.
8. Egy Shift/Ctrl/Alt + nyíl, Home, End, PgUp vagy PgDn, amit senki nem vett el, sima billentyűként újra próbálkozik (a Shift-Le egy listában lefelé lép).
9. Egy betű vagy Alt+betű, amit senki nem vett el, megnyomja azt az engedélyezett gombot, aminek az a gyorsbillentyűje (`'y'` vagy `Alt-Y` a `&Yes`-re).
10. A `close_key` bezárja a window-t.

A 9. pont magyarázza az űrlapok gyorsbillentyű-szabályát: szövegmezőben a betű begépelődik, így csak az Alt+betű jut el a gombokig; jelölőnégyzeten vagy gombon a sima betű is megnyomja őket.

## Keret és gombok

```text
╔═[■]═  Notes ═══════════════════════════1══[↑]═╗   aktív: dupla keret
║                                               ║
║                                               ║
╚═══════════════════════════════════════════════┘   ┘ = méretező sarok
┌─[■]─  Planets ─────────────────────2──[↑]─┐       inaktív: szimpla keret
```

| Elem | Mikor látszik | Kattintás |
|---|---|---|
| `[■]` bezárógomb, bal oldalt | az `on_close` nem `false`, és a window legalább 6 széles | bezár |
| cím | mindig | húzás mozgat, dupla kattintás nagyít |
| szám | ha a window-nak van, és legalább 20 széles | — |
| `[↑]` / `[↕]` nagyítógomb, jobb oldalt | `zoomable?` és legalább 14 széles | nagyítás / visszaállítás |
| `┘` sarok | `sizable?` | húzás méretez |

Az aktív window-nak és minden modális window-nak dupla kerete és fókuszált címstílusa van, a többinek szimpla kerete. Mozgatás vagy méretezés közben a keret kiemelt, és a cím a billentyűket mutatja.

## Nagyítás

Az a `maximizable` window, ami `resizable?`, nagyítógombot kap. A `[↑]`, az **F5**, az **Alt-Enter** vagy egy dupla kattintás a címsoron kitölti vele az asztalt a menüsor és a státuszsor között (`app.desktop_rect`); a `[↕]` és ugyanezek a billentyűk visszaállítják. A nagyított window nagyított marad a terminál átméretezésekor, és se nem mozgatható, se nem méretezhető.

Átméretezhető típusok: `MasterDetail`, `Stream`, `Form` (egyetlen, saját `:rows` nélküli textarea kapja a maradék magasságot), és a `scroll: true`-s `Info`. Egy statikus `Info` nem rakható ki más méretre, így rajta a `maximizable`-nek nincs hatása. Sok olvasnivalót tartalmazó window-khoz való, nem kis dialógusokhoz.

## Mozgatás és méretezés

A `movable?` (mozgatható és nem nagyított) window mozgatható; ha `resizable?` is, méretezhető.

- **Egér:** a címsor húzása mozgat, a `┘` sarok húzása méretez.
- **Billentyűzet:** a Ctrl-F5 (vagy az F6, ha ez az egyetlen window) mozgató módba lép. A nyilak egy cellát mozgatnak, a Shift+nyilak méreteznek, a Tab vált a mozgatás és a méretezés között, az Enter, az Esc vagy a Ctrl-F5 befejezi.
- Kódból: `win.move_to(x, y, width, height)`.

A window az asztalon marad, és legalább 24×7-es (vagy akkora, mint az asztal, ha az kisebb). Mozgatás vagy méretezés után ezt a téglalapot tartja: a terminál átméretezésekor szükség esetén visszahúzódik a képernyőre, és a nagyításból visszaállítva is ide tér vissza.

## Window-k váltása

Egyszerre több window is nyitva lehet; abban a sorrendben takarják egymást, ahogy utoljára használták őket.

- Minden nem modális window megnyitáskor a legkisebb szabad számot kapja 1 és 9 között; a tizedik már nem kap.
- Az **F6** előrehozza a következő window-t (szám szerint), a **Shift-F6** az előzőt, az **Alt-1** … **Alt-9** az adott számút.
- Az aktív mögötti window-ra kattintás előrehozza, és a kattintás tovább is megy hozzá: egy gomb, a bezárógomb vagy a címsor húzása azonnal reagál.
- Az aktív window bezárásakor az alatta lévő lesz aktív.
- Amíg modális window van nyitva, nincs váltás: előbb a kérdésre kell válaszolni.
- Kódból: `app.raise_window(win)`, `app.cycle_windows(1)` / `(-1)`, `app.switchable_windows`.

## A terminál átméretezése

Átméretezéskor az application minden window-n meghívja a `relayout(app)`-ot:

- a mozgatott vagy méretezett window visszakerül a saját téglalapjára, a képernyőre húzva;
- az átméretezhető window nagyítva az `app.desktop_rect`-re, egyébként a `normal_rect`-jére kerül;
- minden más window megtartja a méretét, és újra középre kerül.

## Saját, átméretezhető window

Egy leszármazott három metódussal kapcsolódik be a nagyításba, a méretezésbe és az átméretezés kezelésébe: `resizable?` (igaz), `normal_rect` (a szokásos `[x, y, width, height]`-je az adott terminálon) és `place(x, y, width, height)` (kirakja magát és a gyerekeit arra a téglalapra). A `tick` szívverést ad neki:

```ruby
require 'bbs'

class ClockWindow < BBS::Window
  def initialize(app)
    super(title: ' Clock ', maximizable: true)
    self.theme = app.theme
    @app   = app
    @label = BBS::Widgets::Label.new(text: now, style_key: :text_bright, align: :center)
    add(@label)
    place(*normal_rect)
  end

  def resizable? = true

  def normal_rect
    w = 30
    h = 5
    [(@app.cols - w) / 2 + 1, (@app.rows - h) / 2 + 1, w, h]
  end

  def place(x, y, width, height)
    layout(x, y, width, height)
    @label.layout(x + 2, y + height / 2, width - 4, 1)
  end

  def tick(_app) = (@label.text = now)

  private

  def now = Time.now.strftime('%H:%M:%S')
end

APP = BBS::Application.define do
  status_hint('F2', 'Clock') { open_window(ClockWindow.new(self)) }
  status_hint('Alt-X', 'Bye') { :halt }
end
```

Az óra középen nyílik, az F5 kitölti vele a képernyőt, a Ctrl-F5 és a Shift+nyilak nagyobbá teszik, és minden tétlen másodpercben frissül.

# A window DSL

Az `Application.define`-on belül a `window(name, type:) { |w| ... }` egy `BBS::WindowSpec`-et regisztrál (`lib/bbs/window_spec.rb`); az `app.open_window(name, **extras)` nyitja meg (a `WindowSpec#open(app, **extras)`-en át, ez a spec egyetlen megnyitási módja). A spec minden megnyitáskor újra kiértékeli az attribútumait, így azok függhetnek a hívótól, a képernyőmérettől vagy az extráktól.

```ruby
require 'bbs'

Board = Struct.new(:id, :title)

APP = BBS::Application.define do
  window :compose, type: :form do |w|
    w.title { |ctx| " Post to #{ctx[:board].title} " }
    w.width 70
    w.field :body, type: :textarea, max_length: 1000
    w.on_submit do |values, form, ctx|
      if values[:body].strip.empty?
        form.error('The message is empty.')
        :keep
      else
        ctx[:app].context[:last_post] = [ctx[:board].id, values[:body]]
        :close
      end
    end
  end

  status_hint('F3', 'Post') { open_window(:compose, board: Board.new(1, 'General')) }
end
```

## A ctx

A spec minden blokkja `ctx`-et kap:

```text
ctx = { app: app, **app.context, **app.services, **extras }
```

Azonos kulcsnál a későbbi forrás nyer: az extra felülírja a service-t, a service a context-kulcsot. A `ctx` minden megnyitáskor új; a maradandó állapotot a `ctx[:app].context`-be írd.

## Értékek és blokkok

Minden attribútum értéket vagy blokkot kap:

```text
w.width 70                              # érték
w.width { |ctx| ctx[:app].cols - 10 }   # minden megnyitáskor kiértékelve, ctx-szel
w.width = 70                            # a skalár attribútumoknak setterük is van
```

A blokkal megadott érték-attribútumok csak a `ctx`-et kapják. A callback-attribútumok (`item_label`, `render_detail`, `on_submit`, ...) később hívódnak, a saját argumentumaikkal, utolsóként a `ctx`-szel.

## Attribútumok típusonként

| Attribútum | info | form | master\_detail | stream | Megjegyzés |
|---|---|---|---|---|---|
| `title` | ✓ | ✓ | ✓ | ✓ |  |
| `width`, `height` | ✓ | ✓ | ✓ | ✓ | az alapérték típusonként más |
| `maximizable` | ✓ | ✓ | ✓ | ✓ | nagyítógomb; statikus infón hatástalan |
| `body` | ✓ |  |  |  | a törzs elemei |
| `scroll` | ✓ |  |  |  | egyetlen görgethető törzs |
| `padding` | ✓ |  |  |  | az űrlapnak nem adódik át |
| `button(**opts) { \\|win, ctx\\| }` | ✓ |  |  |  | hívásonként egy gomb |
| `field(key, **opts)` |  | ✓ |  |  | hívásonként egy mező |
| `prefill { \\|ctx\\| hash }` |  | ✓ |  |  | értékek a saját `value:` nélküli mezőknek |
| `hint`, `submit_label`, `cancel_label` |  | ✓ |  |  |  |
| `on_submit` |  | `\\|values, form, ctx\\|` |  | `\\|text, app, ctx\\|` | elhagyható; nélküle az űrlap Save gombja csak bezár |
| `on_cancel { \\|form, ctx\\| }` |  | ✓ |  |  |  |
| `items { \\|ctx\\| }` |  |  | ✓ |  | a lista |
| `item_label { \\|item, ctx\\| }` |  |  | ✓ |  |  |
| `render_detail { \\|item, width, ctx\\| }` |  |  | ✓ |  |  |
| `bottom_meta { \\|item, ctx\\| }` |  |  | ✓ |  |  |
| `on_activate { \\|item, win, ctx\\| }` |  |  | ✓ |  | Enter egy elemen |
| `list_width`, `empty_message` |  |  | ✓ |  |  |
| `action(**opts) { \\|win, ctx\\| }` |  |  | ✓ |  | hívásonként egy gomb |
| `close_button`, `close_label` |  |  | ✓ | ✓ |  |
| `initial_lines { \\|ctx\\| }` |  |  |  | ✓ |  |
| `placeholder`, `buffer_size` |  |  |  | ✓ |  |
| `on_poll { \\|app, ctx\\| }` |  |  |  | ✓ |  |

Az olyan attribútumot, amit az adott típus nem használ, a spec csendben figyelmen kívül hagyja. Az 1.0 óta a `type: :form` specnek nem kell `on_submit`: ha nincs, a Save egyszerűen bezárja az űrlapot.

Lent minden típusnál az imperatív forma mellett ott a DSL-es is. A négy típus a kötelező kulcsszavait kéri (`app:`, `title:`, az űrlap `fields:`-ét, a master-detail `items:`, `item_label:` és `render_detail:` kulcsszavát), a többit opcióként, amelyeket a típus `OPTIONS` konstansa ellenőriz; az alapértékek a lenti táblázatokban vannak, ismeretlen opcióra `ArgumentError` jön.

# BBS::Windows::Info

Középre tett window üzenetekhez, nyitóképernyőkhöz és kulcs–érték összefoglalókhoz (`lib/bbs/windows/info.rb`).

```text
BBS::Windows::Info.open(app, title:, body: [], buttons: nil, width: 60, height: nil,
                        scroll: false, padding: nil, maximizable: false)
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `title` | — (kötelező) | a cím |
| `body` | `[]` | a törzs elemei, lásd lent |
| `buttons` | `nil` | gombleírók (lásd lent: BBS::Windows::ButtonBar); `nil` egy középre tett `[ Close ]`-t ad, `[]` egyet sem |
| `width` | `60` | legfeljebb a képernyő szélessége mínusz 4 |
| `height` | természetes | padding + a törzs sorai + 2 a gomboknak; legfeljebb a képernyő magassága mínusz 4; `scroll: true` esetén a törzs 10 sornak számít |
| `scroll` | `false` | `true`: az egész törzs egyetlen `ScrollView` lesz (átméretezhető, nagyítható) |
| `padding` | `{ sides: 2, top: 1, bottom: 2 }` | bármelyik a háromból felülírható |
| `maximizable` | `false` | csak `scroll: true` mellett |

A törzs elemei:

| Elem | Mit mutat |
|---|---|
| `'szöveg'` | egy sor `:window_text` stílusban (görgetett módban a string ANSI-t is tartalmazhat) |
| `:spacer`, `''`, `nil` | üres sor |
| `:rule` | vízszintes vonal |
| `{ text:, style:, align: }` | egy sor egy témastílusban (`:accent`, `:text_dim`, ...), `align`: `:left` / `:center` / `:right` (görgetett módban nem) |
| `{ kv: [[kulcs, érték], ...], label_width: }` | kulcs–érték sorok; a kulcsoszlop alapértelmezése a leghosszabb kulcs + 1, 8 és 16 között |
| `{ widget:, height: }` | bármilyen widget, `height` sorban (görgetett módban nem) |
| egy `BBS::Widget` | a widget úgy, ahogy van, a saját magasságával (görgetett módban nem) |

Görgetett módban a widget-elemek `ArgumentError`-t dobnak. A fókusz a `primary:` gombon indul, ha van ilyen. A `scroll_view` görgetett módban a `ScrollView`-t adja vissza.

```ruby
require 'bbs'

def open_info(app)
  BBS::Windows::Info.open(app,
    title: ' System ',
    width: 50,
    body: [
      { text: 'Notes desk', style: :accent, align: :center },
      :spacer,
      { kv: [['Ruby', RUBY_VERSION], ['Screen', "#{app.cols}x#{app.rows}"]] },
      :rule,
      'Plain text line.'
    ],
    buttons: [{ label: '&OK', primary: true }])
end

APP = BBS::Application.define do
  status_hint('F2', 'Info') { open_info(self) }
end
```

```text
               ╔═[■]═  System ═══════════════════════════════1══╗
               ║                   Notes desk                   ║
               ║                                                ║
               ║ Ruby      3.3.1                                ║
               ║ Screen    80x25                                ║
               ║ ────────────────────────────────────────────── ║
               ║ Plain text line.                               ║
               ║                                                ║
               ║                                                ║
               ║                     [ OK ]                     ║
               ╚════════════════════════════════════════════════╝
```

Ugyanez a DSL-lel, plusz egy hosszú, görgethető:

```ruby
require 'bbs'

APP = BBS::Application.define do
  window :system, type: :info do |w|
    w.title { |ctx| " #{ctx[:app].cols}x#{ctx[:app].rows} " }
    w.width 44
    w.body do |ctx|
      [{ text: 'Notes desk', style: :accent, align: :center }, :spacer,
       { kv: [['Caller', ctx[:username]], ['Ruby', RUBY_VERSION]] }]
    end
    w.button(label: '&OK', primary: true)
    w.button(label: '&Rules') { |_win, ctx| ctx[:app].open_window(:rules) }
  end

  window :rules, type: :info do |w|
    w.title       ' House rules '
    w.scroll      true
    w.maximizable true
    w.height      12
    w.body        { |_ctx| (1..30).map { |i| "#{i}. Be nice." } }
  end

  status_hint('F2', 'System') { open_window(:system) }
  status_hint('F3', 'Rules') { open_window(:rules) }
end
```

A `&Rules` bezárja a rendszer-window-t, és megnyitja a szabályokat: a gomb bezárja a window-ját, kivéve, ha a blokkja `:keep`-et ad vissza, vagy a gombon `keep_open: true` áll.

# BBS::Windows::Form

Űrlap: egymás alatti, címkézett mezők, fölöttük egy opcionális tippsor, egy státuszsor az ellenőrzési üzeneteknek, és egy Save / Cancel gombpár (`lib/bbs/windows/form.rb`).

```text
BBS::Windows::Form.open(app, title:, fields:, on_submit: nil, hint: nil, width: 60, height: nil,
                        submit_label: '&Save', cancel_label: '&Cancel',
                        padding: nil, on_cancel: nil, maximizable: false)
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `title` | — (kötelező) | a cím |
| `fields` | — (kötelező) | mezőleírók, lásd lent |
| `on_submit` | `nil` | `->(values, form) { ... }`; `:keep` esetén nyitva marad, bármi más bezárja. `nil`: a Save csak bezár |
| `on_cancel` | `nil` | `->(form) { ... }`, a Cancel gomb futtatja a window bezárása előtt |
| `hint` | `nil` | egy sor a mezők fölött, `:input_label` stílusban |
| `width` | `60` | legfeljebb a képernyő szélessége mínusz 4 |
| `height` | természetes | a mezők magasságának összege + a fix sorok; explicit magasságnál egyetlen `:rows` nélküli textarea nő |
| `submit_label`, `cancel_label` | `'&Save'`, `'&Cancel'` | a gombok felirata; a `&` jelöli az Alt-gyorsbillentyűt |
| `padding` | `{ sides: 2, top: 1, bottom: 2 }` | belső margók |
| `maximizable` | `false` | a nagyítógomb (az űrlapok átméretezhetők) |

A mezőleíró kulcsai:

| Kulcs | Típusok | Alapérték | Jelentés |
|---|---|---|---|
| `key` | mind | — | a kulcs a `values`-ban |
| `type` | — | `:input` | `:input`, `:textarea` vagy `:checkbox` |
| `label` | input, checkbox | `nil` | input: a címke balra (az oszlop a leghosszabb címke + 1, 8 és 16 között); checkbox: a szöveg az `[x]` után; textareánál figyelmen kívül marad |
| `value` | mind | `''` / nincs bejelölve | a kezdőérték; checkboxnál bármely igaz érték bejelöli |
| `max_length` | input, textarea | 256 / 8000 | a karakterek maximális száma |
| `placeholder` | input | `''` | szürke szöveg, amíg üres |
| `password` | input | `false` | a karakterek helyett `*`-ot mutat |
| `rows` | textarea | 6 | a textarea magassága |

A `values` mezőkulcs szerinti Hash: inputnál és textareánál string (a sorok `\n`-nel összefűzve), checkboxnál `true`/`false`.

| Metódus | Mit csinál |
|---|---|
| `form.values` | a pillanatnyi értékek |
| `form.error(text)` | a státuszsor `:error` stílusban |
| `form.info(text)` | a státuszsor `:input_label` stílusban |
| `form.info('')` | kiüríti a státuszsort |
| `form.status_label` | a státuszsor `Label` widgete |

**Billentyűk az űrlapon.** A fókusz az első mezőn indul, a Tab és a Shift-Tab lép a mezők és a gombok között. **Az Enter szövegmezőben nem küldi el az űrlapot** (nem csinál semmit; textareában új sort kezd): a hívó a Save gombbal küld, vagyis `&Save` esetén **Alt-S**-sel, vagy Tabbal odalép és Entert üt. Az Alt-C vagy az Esc megszakít; az Esc az `on_cancel` meghívása nélkül zár be. Jelölőnégyzeten a Szóköz vagy az Enter átbillenti, és egy sima `s` már megnyomja a Save-et. Írd bele a `hint`-be („Alt-S saves.”).

```ruby
require 'bbs'

def open_profile(app)
  BBS::Windows::Form.open(app,
    title: ' Profile ',
    width: 56,
    hint: 'Alt-S saves, Esc closes.',
    fields: [
      { key: :name,   label: 'Name',     value: app.context[:username], max_length: 20 },
      { key: :pin,    label: 'PIN',      password: true, max_length: 4, placeholder: '4 digits' },
      { key: :public, label: 'Show me in Who is online', type: :checkbox, value: true },
      { key: :bio,    type: :textarea, rows: 4, max_length: 500 }
    ],
    on_submit: lambda do |values, form|
      unless values[:pin].match?(/\A\d{4}\z/)
        form.error('The PIN is 4 digits.')
        return :keep
      end
      app.context[:profile] = values
      :close
    end,
    on_cancel: ->(_form) { app.context[:profile_cancelled] = true })
end

APP = BBS::Application.define do
  status_hint('F2', 'Profile') { open_profile(self) }
end
```

Ha a PIN-be `12`-t írunk, és Alt-S-t nyomunk:

```text
            ╔═[■]═  Profile ════════════════════════════════════1══╗
            ║ Alt-S saves, Esc closes.                             ║
            ║                                                      ║
            ║ Name      Ann                                        ║
            ║                                                      ║
            ║ PIN       **_                                        ║
            ║                                                      ║
            ║ [x] Show me in Who is online                         ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║ The PIN is 4 digits.                                 ║
            ║                                   [ Save ] [ Cancel ]║
            ╚══════════════════════════════════════════════════════┘
```

A DSL-es forma. A `prefill` azoknak a mezőknek ad értéket, amiknek nincs saját `value:`-juk; egy mező `value:`-ja `ctx`-et kapó blokk is lehet:

```ruby
require 'bbs'

APP = BBS::Application.define do
  window :profile, type: :form do |w|
    w.title        ' Profile '
    w.width        56
    w.hint         'Alt-S saves, Esc closes.'
    w.field :name, label: 'Name', max_length: 20
    w.field :pin,  label: 'PIN', password: true, max_length: 4
    w.field :public, label: 'Show me in Who is online', type: :checkbox
    w.field :since, label: 'Since', value: ->(ctx) { ctx[:app].context[:since] || Time.now.year.to_s }
    w.prefill { |ctx| { name: ctx[:username], public: true } }
    w.submit_label '&Save'
    w.on_submit do |values, form, ctx|
      if values[:pin].match?(/\A\d{4}\z/)
        ctx[:app].context[:profile] = values
        :close
      else
        form.error('The PIN is 4 digits.')
        :keep
      end
    end
    w.on_cancel { |_form, ctx| ctx[:app].context[:profile_cancelled] = true }
  end

  status_hint('F2', 'Profile') { open_window(:profile) }
end
```

# BBS::Windows::MasterDetail

Kétpaneles böngésző: balra egy lista, jobbra a kijelölt elem részletei, opcionálisan egy meta-sor és egy gombsor (`lib/bbs/windows/master_detail.rb`). A listában lépkedve a részletek újrarajzolódnak.

```text
BBS::Windows::MasterDetail.open(app, title:, items:, item_label:, render_detail:,
                                list_width: nil, bottom_meta: nil, actions: nil, on_activate: nil,
                                close_button: true, close_label: '&Close',
                                empty_message: '  (empty)', width: nil, height: nil, maximizable: false)
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `title` | — (kötelező) | a cím |
| `items` | — (kötelező) | bármiből álló Array |
| `item_label` | — (kötelező) | `->(item) { szöveg }`, egy listasor |
| `render_detail` | — (kötelező) | `->(item, width) { sorok }`: stringek Array-e (ANSI megengedett) a részletpanelnek, a `width` a használható szélesség. Megnyitáskor, minden kijelölés-váltáskor és átméretezés után hívódik |
| `list_width` | a window harmada, 20–40 | a listaoszlop, legfeljebb a window fele |
| `bottom_meta` | `nil` | `->(item) { szöveg }`: egy sor a panelek alatt |
| `actions` | `nil` | további gombleírók (lásd lent: BBS::Windows::ButtonBar), a Close elé kerülnek |
| `on_activate` | `nil` | `->(item, window) { ... }`, Enterre hívódik a listában |
| `close_button` | `true` | egy Close gomb a végére |
| `close_label` | `'&Close'` | a felirata |
| `empty_message` | `'  (empty)'` | a részletpanel szövege, ha üres a lista |
| `width`, `height` | képernyő − 4, képernyő − 6 | a méret |
| `maximizable` | `false` | a nagyítógomb (a master-detail window-k átméretezhetők) |

| Tag | Mit csinál |
|---|---|
| `replace_items(items, title: nil)` | helyben lecseréli a listát, kijelöli az első elemet, és ha kell, a címet is átírja: navigáció egy fájlböngészőben vagy egy wikiben |
| `reload_detail(keep_scroll: false)` | újrarajzolja a kijelölt elem részleteit |
| `list_widget` | a `ListBox`; a `selected_item` a kijelölt elem |
| `detail_view` | a jobb oldali `ScrollView` |
| `meta_label` | a meta-sor `Label`-je, vagy `nil` |

A fókusz a listában indul. A Fel/Le, a PgUp/PgDn, a Home/End és a görgő a listában mozog; a Tab a gombokra lép. Több gomb jobbra igazodik, a magában álló Close középre.

```ruby
require 'bbs'

Planet = Struct.new(:name, :moons, :about)
PLANETS = [Planet.new('Mercury', 0, 'Closest to the Sun.'),
           Planet.new('Earth', 1, 'Home.'),
           Planet.new('Mars', 2, 'Red and dusty.')].freeze

def open_planets(app)
  BBS::Windows::MasterDetail.open(app,
    title: ' Planets ',
    items: PLANETS,
    item_label: ->(p) { p.name },
    render_detail: ->(p, width) { [p.name.upcase, '', *BBS::FrameBuffer.wrap_ansi(p.about, width)] },
    bottom_meta: ->(p) { "#{p.moons} moon(s)" },
    list_width: 20,
    width: 60, height: 14,
    maximizable: true,
    on_activate: ->(p, _win) { BBS::Dialogs.message(app, "You picked #{p.name}.") },
    actions: [
      { label: '&Rocky only', keep_open: true,
        on_click: ->(win) { win.replace_items(PLANETS.reject { |p| p.name == 'Earth' }, title: ' Rocky ') } }
    ])
end

APP = BBS::Application.define do
  status_hint('F2', 'Planets') { open_planets(self) }
end
```

```text
          ╔═[■]═  Planets ════════════════════════════════════1══[↑]═╗
          ║                                                          ║
          ║   Mercury             EARTH                              ║
          ║   Earth                                                  ║
          ║   Mars                Home.                              ║
          ║                                                          ║
          ...
          ║ 1 moon(s)                                                ║
          ║                                  [ Rocky only ] [ Close ]║
          ╚══════════════════════════════════════════════════════════╝
```

A DSL-es forma; az `items` megnyitáskor értékelődik ki, a callbackek utolsóként a `ctx`-et kapják:

```ruby
require 'bbs'

Planet = Struct.new(:name, :moons, :about)

APP = BBS::Application.define do
  services planets: [Planet.new('Mercury', 0, 'Closest to the Sun.'),
                     Planet.new('Earth', 1, 'Home.'),
                     Planet.new('Mars', 2, 'Red and dusty.')]

  window :planets, type: :master_detail do |w|
    w.title         ' Planets '
    w.maximizable   true
    w.width         60
    w.height        14
    w.list_width    20
    w.items         { |ctx| ctx[:planets] }
    w.item_label    { |p, _ctx| p.name }
    w.render_detail { |p, width, _ctx| [p.name.upcase, '', *BBS::FrameBuffer.wrap_ansi(p.about, width)] }
    w.bottom_meta   { |p, _ctx| "#{p.moons} moon(s)" }
    w.empty_message '  (no planets)'
    w.on_activate   { |p, _win, ctx| BBS::Dialogs.message(ctx[:app], "You picked #{p.name}.") }
    w.action(label: '&Rocky only', keep_open: true) do |win, ctx|
      win.replace_items(ctx[:planets].reject { |p| p.name == 'Earth' }, title: ' Rocky ')
    end
  end

  status_hint('F2', 'Planets') { open_window(:planets) }
end
```

A `render_detail` megkapja a panel szélességét; a szöveget a `BBS::FrameBuffer.wrap_ansi(text, width)`-del tördeld rá, vagy rendereld Markdownként a `BBS::Markdown.to_lines(text, width: width, theme: theme)`-mel (lásd [Rajzolás](https://wiki.teletypegames.org/hu/engines/rubbs/drawing)).

# BBS::Windows::Stream

Élő nézet: görgethető előzmények, alattuk egy beviteli sor, és lekérdezés minden tétlen tickben (`lib/bbs/windows/stream.rb`). Chat- és log-window-khoz készült.

```text
BBS::Windows::Stream.open(app, title:, initial_lines: [], placeholder: '',
                          on_submit: nil, on_poll: nil, buffer_size: 500,
                          width: nil, height: nil, close_button: true, close_label: '&Close',
                          maximizable: false)
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `title` | — (kötelező) | a cím |
| `initial_lines` | `[]` | a kezdő előzmények (ANSI megengedett) |
| `placeholder` | `''` | szürke szöveg az üres beviteli sorban |
| `on_submit` | `nil` | `->(text, app) { ... }` Enterre, nem üres sornál; a beviteli sor kiürül, hacsak `:keep`-et nem ad vissza |
| `on_poll` | `nil` | `->(app) { sorok }` minden tétlen tickben; a visszaadott sorok (vagy `nil` / `[]`) a végére kerülnek, és az előzmények a végére görgetnek |
| `buffer_size` | `500` | ennyi sort tart meg legfeljebb |
| `width`, `height` | képernyő − 8, képernyő − 6 | a méret |
| `close_button`, `close_label` | `true`, `'&Close'` | középre tett Close gomb |
| `maximizable` | `false` | a nagyítógomb (a streamek átméretezhetők) |

A `history` a `ScrollView`-t, az `input` a `TextInput`-ot adja vissza. A fókusz a beviteli sorban indul.

Az `on_submit` **nem** teszi be a sort az előzmények közé: a window csak azt mutatja, amit az `initial_lines` és az `on_poll` ad neki. Küldd el a sort a service-nek, és a következő lekérdezés hozza vissza (így a hívó ugyanabban a sorrendben látja, mint mindenki más), vagy fűzd hozzá magad: `win.history.lines += [line]`.

```ruby
require 'bbs'

def open_log(app)
  BBS::Windows::Stream.open(app,
    title: ' Log ',
    initial_lines: ['Log opened.'],
    placeholder: 'Type a line, Enter adds it',
    buffer_size: 200,
    on_submit: ->(text, _app) { (app.context[:log] ||= []) << text },
    on_poll: ->(_app) { (app.context[:log] || []).shift(10).map { |l| "> #{l}" } })
end

APP = BBS::Application.define do
  status_hint('F2', 'Log') { open_log(self) }
end
```

Egy chat a DSL-lel, közös service-szel. Az `initial_lines` feljegyzi, meddig olvasott a hívó, az `on_poll` hozza, ami azóta jött:

```ruby
require 'bbs'

class Chat
  def initialize
    @lines = []
    @mutex = Mutex.new
  end

  def history = @mutex.synchronize { @lines.last(50) }
  def say(who, text) = @mutex.synchronize { @lines << "#{who}: #{text}" }
  def since(n) = @mutex.synchronize { @lines.drop(n) }
  def size = @mutex.synchronize { @lines.size }
end

APP = BBS::Application.define do
  services chat: Chat.new

  window :chat, type: :stream do |w|
    w.title         ' Chat '
    w.maximizable   true
    w.initial_lines do |ctx|
      ctx[:app].context[:chat_seen] = ctx[:chat].size
      ctx[:chat].history
    end
    w.placeholder   'Say something, Enter sends'
    w.buffer_size   300
    w.on_submit     { |text, _app, ctx| ctx[:chat].say(ctx[:username], text) }
    w.on_poll do |_app, ctx|
      seen  = ctx[:app].context[:chat_seen]
      fresh = ctx[:chat].since(seen)
      ctx[:app].context[:chat_seen] = seen + fresh.size
      fresh
    end
  end

  status_hint('F4', 'Chat') { open_window(:chat) }
end
```

Lekérdezés csak akkor fut, ha a hívó tétlen, nagyjából másodpercenként; aki gépel, az az új sorokat a szünetekben látja.

# BBS::Windows::ButtonBar

Ez a segéd rakja ki a kész window-k gombsorait, és a saját window-kon is használható (`lib/bbs/windows/button_bar.rb`). A gombok az alsó keret fölötti sorba kerülnek.

| Függvény | Mit csinál |
|---|---|
| `ButtonBar.attach(window, app, descriptors)` | létrehozza, elhelyezi és a window-hoz adja a gombokat; `{ widgets:, primary:, by_key: }`-t ad vissza |
| `ButtonBar.place(window, widgets, descriptors)` | a meglévő gombokat újra elhelyezi a window aktuális határaihoz (a `place`-ből hívd) |

A gombleíró (ugyanaz az `Info` `buttons:`-ában, a `MasterDetail` `actions:`-ében, a `w.button`-ben és a `w.action`-ben):

| Kulcs | Alapérték | Jelentés |
|---|---|---|
| `label` | — | a szöveg; a `&` jelöli a gyorsbillentyűt (betű vagy Alt+betű) |
| `on_click` | `nil` | `->(window) { ... }`; a DSL-ben a blokk, `\\|win, ctx\\|` |
| `keep_open` | `false` | `true`: kattintás után nyitva marad a window |
| `primary` | `false` | ez a gomb kapja a fókuszt, amikor egy `Info` megnyílik (az `attach` `primary:`-ként adja vissza) |
| `cancel` | `false` | csak tájékoztató; az Esc úgyis bezár a window `close_key`-jén át |
| `align` | egy gombnál `:center`, többnél `:right` | `:left` a bal széltől sorakoztat, `:right` a jobb szélhez simít deklarálási sorrendben, `:center` középre tesz |
| `key` | `nil` | név a `by_key`-hez |

Az `on_click` után a window bezárul, hacsak nincs `keep_open`, vagy a blokk nem `:keep`-et adott. A visszatérési érték máshová nem jut el: egy gomb `:halt`-ja nem fejezi be az applicationt (arra az `app.stop!` való), az `app.open_door(name)` viszont egy gombból is kilép a dooron át, mert maga állítja le a ciklust.

```ruby
require 'bbs'

def open_vote(app)
  win = BBS::Dialogs.centred_window(app, width: 44, height: 7, title: ' Vote ')
  win.modal = false
  question = BBS::Widgets::Label.new(text: 'Pineapple on pizza?', style_key: :window_text)
  question.layout(win.bounds.x + 2, win.bounds.y + 2, 40, 1)
  win.add(question)
  bar = BBS::Windows::ButtonBar.attach(win, app, [
    { key: :help, label: '&Help', align: :left, keep_open: true,
      on_click: ->(_w) { BBS::Dialogs.message(app, 'Pick one.') } },
    { key: :yes, label: '&Yes', primary: true, on_click: ->(_w) { app.context[:vote] = :yes } },
    { key: :no,  label: '&No',  on_click: ->(_w) { app.context[:vote] = :no } }
  ])
  win.reset_focus
  win.focus_manager.focus(bar[:primary])
  app.open_window(win)
end

APP = BBS::Application.define do
  status_hint('F2', 'Vote') { open_vote(self) }
end
```

```text
                  ╔═[■]═  Vote ═══════════════════════════1══╗
                  ║                                          ║
                  ║ Pineapple on pizza?                      ║
                  ║                                          ║
                  ║                                          ║
                  ║ [ Help ]                   [ Yes ] [ No ]║
                  ╚══════════════════════════════════════════╝
```

# BBS::Dialogs

Kis modális window-k a gyakori kérdésekhez (`lib/bbs/dialogs.rb`). Mind azonnal megnyílik, és a window-t adja vissza; egyik sem blokkol: a válasz egy callbackben érkezik. Amíg egy dialógus nyitva van, máshová nem lehet váltani.

| Függvény | Alapérték | Mit csinál |
|---|---|---|
| `Dialogs.message(app, text, title: ' Message ', width: nil)` | szélesség: a szöveg 50-re tördelve, legalább 30 | a szöveg és egy OK gomb |
| `Dialogs.confirm(app, text, title: ' Confirm ', on_yes: nil, on_no: nil)` | legalább 36 széles | a szöveg, Yes és No |
| `Dialogs.input(app, prompt:, title: ' Input ', value: '', width: 50, on_submit: nil)` |  | egy kérdés, egy szövegmező, OK és Cancel |
| `Dialogs.centred_window(app, width:, height:, title: nil)` |  | egy modális, középre tett, kirakott `Window`, még nem megnyitva |

| Dialógus | Válaszok |
|---|---|
| message | az OK, az `o`, az Alt-O, Enter az OK-n vagy az Esc bezárja |
| confirm | a Yes (`y`, Alt-Y) bezár és meghívja az `on_yes`-t; a No (`n`, Alt-N) bezár és meghívja az `on_no`-t; az Esc **egyiket sem** hívja meg |
| input | Enter a mezőben vagy az OK bezár és meghívja az `on_submit`-ot a szöveggel; a Cancel vagy az Esc hívás nélkül zár be |

A confirm callbackjeinek visszatérési értéke eljut a futási ciklusig, így egy helperből az `on_yes: -> { open_door(:x) }` kilép a dooron át. A hosszú szövegek tördelődnek; `width:` megadásakor a `message` `width - 6`-ra tördel. Saját szöveget a `BBS::FrameBuffer.wrap_ansi(text, width)`-del tördelj: ez a rubbs egyetlen sortördelője, és ismeri az ANSI-t.

```ruby
require 'bbs'

def ask_delete(app)
  BBS::Dialogs.confirm(app, 'Delete every note?',
    on_yes: -> { BBS::Dialogs.message(app, 'Deleted.', title: ' Done ') },
    on_no:  -> { app.context[:kept] = true })
end

def ask_name(app)
  BBS::Dialogs.input(app, prompt: 'New name:', value: app.context[:username].to_s,
    on_submit: ->(value) { app.context[:username] = value })
end

APP = BBS::Application.define do
  status_hint('F2', 'Delete') { ask_delete(self) }
  status_hint('F3', 'Rename') { ask_name(self) }
  status_hint('F4', 'Hello') { BBS::Dialogs.message(self, "Hello, #{context[:username]}!", width: 40) }
end
```

```text
                         ╔═[■]═  Done ════════════════╗
                         ║ Deleted.                   ║
                         ║                            ║
                         ║                            ║
                         ║           [ OK ]           ║
                         ╚════════════════════════════╝
```

Egynél több mezőhöz használj `Form`-ot.

# Billentyűk és egér a window-kban

| Billentyű / művelet | Mit csinál |
|---|---|
| **Tab / Shift-Tab** | következő / előző vezérlő |
| **Enter / Szóköz** | megnyomja a fókuszált gombot; átbillenti a jelölőnégyzetet |
| **Enter** listában | egy MasterDetail `on_activate`-je |
| **Enter** szövegmezőben | Formban semmi; Streamben és a `Dialogs.input`-ban elküld |
| **Alt+betű** / **betű** | megnyomja az azzal jelölt gombot (a sima betű csak akkor, ha a fókuszált vezérlő nem fogad betűt) |
| **Esc** | bezárja a window-t (`close_key`) |
| **F5**, **Alt-Enter** | nagyítás és vissza |
| **Ctrl-F5** | mozgató mód: a nyilak mozgatnak, a Shift+nyilak méreteznek, a Tab vált, az Enter / Esc befejezi |
| **F6 / Shift-F6**, **Alt-1…9** | window-váltás |
| kattintás a `[■]`-ra / a `[↑]` `[↕]`-ra | bezárás / nagyítás, visszaállítás |
| a cím / a `┘` húzása | mozgatás / méretezés |
| dupla kattintás a címen | nagyítás és vissza |
| görgő | görgeti a mutató alatti listát vagy szöveget |

# Lásd még

- [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) — `open_window`, az eseménykézbesítés, a menüsor és a státuszsor
- [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) — Label, Button, TextInput, TextArea, Checkbox, ListBox, ScrollView, és saját widget írása
- [Rajzolás](https://wiki.teletypegames.org/hu/engines/rubbs/drawing) — a stíluskulcsok (`:window_text`, `:accent`, `:error`, ...), a `FrameBuffer.wrap_ansi`, Markdown
- [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store) — a billentyű-szimbólumok és az egér

'; 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: [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets.md)
