---
title: Application
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/application'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/application.md'
lang: hu
date: '2026-10-27'
description: 'A BBS::Application a rubbs asztal-rétege: teljes képernyős, egeret is kezelő eseményciklus menüsorral, státuszsorral, háttérpanellel és egymás között váltható window-k veremével, Turbo Vision-stílusban. Ez az oldal sorra veszi az Application.define DSL minden metódusát, a példány API-ját, a futási…'
taxonomy:
  tag:
    - reference
---

# Application

> A `BBS::Application` a [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) asztal-rétege: teljes képernyős, egeret is kezelő eseményciklus menüsorral, státuszsorral, háttérpanellel és egymás között váltható window-k veremével, Turbo Vision-stílusban. Ez az oldal sorra veszi az `Application.define` DSL minden metódusát, a példány API-ját, a futási ciklust és az application–door kört, a context hasht, az eseményeket, a fókuszt, a menüsort és a státuszsort. Maguk a window-k a [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows), a widgetek a [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) oldalon vannak.

# A részek

Egy application egy gyökérkonténer, és fölötte néhány réteg. Minden képkocka ebben a sorrendben készül, hátulról előre:

| Réteg | Osztály | Mi ez |
|---|---|---|
| képernyő | — | az egész terminál a téma `:screen` stílusával kitöltve |
| gyökérpanel | `BBS::Container` (`app.root`) | az asztal: az `add`-dal hozzáadott widgetek (ANSI-art, feliratok, egy gomb) |
| window-verem | `BBS::Window` (`app.windows`) | a window-k, az utolsó az aktív |
| menüsor | `BBS::Menubar` (`app.menubar`) | az 1. sor, Alt+betűvel nyíló legördülő menük |
| státuszsor | `BBS::Statusbar` (`app.statusbar`) | az utolsó sor, billentyű-tippek és egy jobbra igazított szöveg |

A menüsor és a státuszsor csak akkor létezik, ha a definíció deklarálja (`menubar`, illetve legalább egy `status_hint`). Nélkülük a gyökérpanel és a window-k kapják az egész képernyőt.

A rajzolás kétpufferes: minden képkocka egy friss `BBS::FrameBuffer`-be készül, összevetjük a legutóbb elküldöttel, és csak a megváltozott cellák mennek ki a vonalra. A terminál átméretezését (NAWS) a következő képkocka veszi észre: újrarajzolja az egész képernyőt, és minden window `relayout`-ot kap.

# Egy teljes példa

Egy közös üzenőfal: jegyzetek listája egy master-detail window-ban, egy űrlap az íráshoz, egy menü egy doorral, státusztippek, óra a státuszsoron. Az oldal többi része erre hivatkozik. Mentsd el `notes.rb` néven, futtasd: `ruby notes.rb`, aztán `telnet localhost 2323`.

```ruby
require 'bbs'

# A tiny shared notice board: one list of notes for every caller.
class NoteStore
  Note = Struct.new(:id, :author, :title, :body, :at)

  def initialize
    @notes = []
    @mutex = Mutex.new
  end

  def all = @mutex.synchronize { @notes.dup }

  def add(author, title, body)
    return :empty if title.strip.empty?

    @mutex.synchronize { @notes << Note.new(@notes.size + 1, author, title.strip, body, Time.now) }
    :ok
  end
end

# A door: any callable(session, context) gets the raw line.
DICE = lambda do |session, context|
  session.write("\r\n  #{context[:username]} rolls: #{rand(1..6)} and #{rand(1..6)}\r\n")
  session.write("  Press ENTER to go back.\r\n")
  session.readline
end

NOTES_APP = BBS::Application.define do
  theme :synchronet do
    accent "\e[1;33;44m"
  end

  services notes: NoteStore.new

  door :dice, DICE, label: '&Dice'

  helpers do
    def me = context[:username] || 'guest'
    def clock = Time.now.strftime('%H:%M')

    def note_list
      open = windows.find { |w| w.is_a?(BBS::Windows::MasterDetail) }
      open ? raise_window(open) : open_window(:notes)
    end

    def refresh_notes
      windows.grep(BBS::Windows::MasterDetail).each { |w| w.replace_items(services[:notes].all) }
    end
  end

  window :notes, type: :master_detail do |w|
    w.title       ' Notes '
    w.maximizable true
    w.items       { |ctx| ctx[:notes].all }
    w.item_label  { |note, _ctx| note.title }
    w.render_detail do |note, width, _ctx|
      ["#{note.title} — #{note.author}", '', *BBS::FrameBuffer.wrap_ansi(note.body.to_s, width)]
    end
    w.bottom_meta   { |note, _ctx| note.at.strftime('%Y-%m-%d %H:%M') }
    w.empty_message '  No notes yet. Alt-N writes one.'
    w.action(label: '&New') { |_win, ctx| ctx[:app].open_window(:new_note); :keep }
  end

  window :new_note, type: :form do |w|
    w.title ' New note '
    w.width 60
    w.hint  'Alt-S saves, Esc cancels.'
    w.field :title, label: 'Title', max_length: 40
    w.field :body,  type: :textarea, rows: 5
    w.on_submit do |values, form, ctx|
      if ctx[:notes].add(ctx[:app].me, values[:title], values[:body]) == :empty
        form.error('A note needs a title.')
        :keep
      else
        ctx[:app].refresh_notes
        :close
      end
    end
  end

  menubar do
    menu '&File' do
      item '&Notes', shortcut: 'F2' do note_list end
      item '&New note', shortcut: 'F3' do open_window(:new_note) end
      separator
      item 'E&xit', shortcut: 'Alt-X' do :halt end
    end
    menu '&Doors' do
      doors
    end
    menu '&Help' do
      item '&About' do BBS::Dialogs.message(self, "Notes desk for #{me}.", title: ' About ') end
    end
  end

  status_hint('F2', 'Notes') { note_list }
  status_hint('F3', 'New', hidden: true) { open_window(:new_note) }
  status_hint('F10', 'Menu') { menubar.open(0) }
  status_hint('Alt-X', 'Bye') { :halt }

  on_tick { statusbar.right_text = "#{me} │ #{clock}" }
  on_door { |door, ctx| warn "#{ctx[:username]} enters #{door.name}" }

  init do
    statusbar.right_text = "#{me} │ #{clock}"
    note_list
  end
end

BBS.configure do |c|
  c.port  = Integer(ENV.fetch('PORT', 2323))
  c.mouse = true
  c.flow  = BBS::Flow.define do
    ask :username, prompt: 'Your name'
    call do |ctx, runner|
      NOTES_APP.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
      nil
    end
    say 'Bye!'
  end
end

BBS.start if $PROGRAM_NAME == __FILE__
```

Ezt látja a hívó, miután beírta a nevét és írt egy jegyzetet:

```text
  File   Doors   Help

  ╔═[■]═  Notes ══════════════════════════════════════════════════════1══[↑]═╗
  ║                                                                          ║
  ║   Hi                       Hi — Ann                                      ║
  ║                                                                          ║
  ║                            body                                          ║
  ║                                                                          ║
  ...
  ║ 2026-10-27 23:16                                                         ║
  ║                                                         [ New ] [ Close ]║
  ╚══════════════════════════════════════════════════════════════════════════┘

 F2 Notes  F10 Menu  Alt-X Bye                                      Ann │ 23:16
```

Amit érdemes észrevenni:

- A `NOTES_APP` egy **definíció** (egy `Application::Builder`): betöltéskor egyszer készül el, és minden hívó osztozik rajta. Minden hívás saját `Application`-példányt kap belőle.
- A service-ek is közösek: a `NoteStore.new` egyszer fut le, így minden hívó ugyanazokat a jegyzeteket látja. Ezért van a store-ban mutex.
- A hívónkénti állapot a `context`-be kerül (itt a `:username`, amit a flow `ask`-ja tölt ki).
- A window-callbackek egy `ctx` hasht kapnak; a `ctx[:app]` a futó application, így a helperek `ctx[:app].me` alakban érhetők el.

# Az application indítása

## Egy flow `call` lépéséből (a szokásos mód)

```ruby
require 'bbs'

APP = BBS::Application.define do
  status_hint('Alt-X', 'Quit') { :halt }
end

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    ask :username, prompt: 'Name'
    call do |ctx, runner|
      APP.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
      nil
    end
    say 'Goodbye!'
  end
end

BBS.start
```

A `Builder#run` felépíti és lefuttatja az applicationt, és ha a hívó egy dooron át hagyta el, lefuttatja a doort, majd újraépíti az applicationt (lásd lent: Az application–door kör). A flow saját `ctx`-e lesz az application `context`-je, így amit a flow kérdezett (`:username`), és amit az application a `context`-be ír, mindkét irányban megmarad. Ha a `call` lépés `nil`-t ad vissza, a flow megy tovább (`say 'Goodbye!'`); `:halt` a hívást is befejezné.

## A flow `app` lépésével

```ruby
require 'bbs'

APP = BBS::Application.define do
  status_hint('Alt-X', 'Quit') { :halt }
end

FLOW = BBS::Flow.define do
  ask :username, prompt: 'Name'
  app APP
  say 'Goodbye!'
end
```

Az `app` lépés rövidebb: az `APP.run(...)`-t (`Builder#run`) futtatja a flow `ctx`-ével, így a doorokon itt is átviszi a hívót (0.17.1 óta; előtte a lépés egyszer építette fel az applicationt, és egy door csak befejezte). A `call` forma akkor kell, ha a futtatás köré kód is kell; a hálózat összes boardja (Teletype BBS, Feedia BBS, Alexandria BBS) ezt használja.

## Flow nélkül

A `Builder#build(session:, session_id:, context: {})` egy kész `Application`-t ad vissza; a `run`-ja magában az eseményciklus. Bármi meghívhatja, aminek van `BBS::Session`-je (egy door, egy teszt, egy saját szerver):

```ruby
require 'bbs'

APP = BBS::Application.define do
  status_hint('Alt-X', 'Quit') { :halt }
end

def serve(session, session_id)
  app = APP.build(session: session, session_id: session_id, context: { username: 'guest' })
  app.run
end
```

# A define DSL

A `BBS::Application.define { ... }` egy friss `BBS::Application::Builder`-en értékeli ki a blokkot, és azt adja vissza. Ekkor még semmi sem épül fel: a builder feljegyzi a definíciót, és a `build` vagy a `run` csinál belőle hívónként egy applicationt.

| Metódus | Szignatúra | Mikor fut | Mit csinál |
|---|---|---|---|
| `theme` | `theme(base = nil) { ... }` | definiáláskor | a színtéma |
| `services` | `services(hash = nil)` | definiáláskor | közös objektumok a window-callbackeknek |
| `helpers` | `helpers { def ... end }` | definiáláskor | metódusok minden application-példányra |
| `window` | `window(name, type:) { \\|w\\| ... }` | definiáláskor | egy nevesített window spec, lásd [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows) |
| `door` | `door(name, program, label: nil)` | definiáláskor | egy door, amibe a hívó beléphet |
| `menubar` | `menubar { menu ... }` | minden buildnél | a menüsor |
| `status_hint` | `status_hint(key, label, hidden: false) { ... }` | minden buildnél | egy státuszsor-bejegyzés |
| `body` | `body { add widget }` | minden buildnél | widgetek a gyökérpanelen |
| `init` | `init { ... }` | minden buildnél, utolsóként | beállító kód az új applicationön |
| `on_tick` | `on_tick { ... }` | tétlenségben nagyjából másodpercenként | időszakos munka |
| `on_idle` | `on_idle { ... }` | amikor a hívó eléri a tétlenségi limitet | vonalban tartja (`:keep`), vagy hagyja bontani |
| `on_door` | `on_door { \\|door, context\\| ... }` | minden door előtt | adminisztráció |

A hívások sorrendje a `define`-on belül mindegy. A `build` sorrendje viszont rögzített: application, menüsor, státuszsor, `init`, `body`.

## theme

```text
theme(base = nil, &block)
```

Beállítja az application, a menüsor, a státuszsor és minden rajta át nyitott window témáját. Alapértelmezés: `BBS::Theme.synchronet`.

| Hívás | Eredmény |
|---|---|
| `theme BBS::Theme.neumanntronics` | az adott téma változatlanul |
| `theme(:dosnavigator) { ... }` | a preset név szerint, a blokk felülírásaival |
| `theme(BBS::Theme.mono) { ... }` | az adott téma felülírásokkal |
| `theme { ... }` | `synchronet` felülírásokkal |

A blokkon belül (ez egy `ThemeBuilder`):

| Metódus | Mit csinál |
|---|---|
| `KULCS "\e[...m"` | bármely egyargumentumú metódus felülírja az azonos nevű stíluskulcsot (`screen "\e[0;37;40m"`) |
| `style(key, value)` | ugyanez, olyan kulcsokra, amik Ruby-metódusnévvel ütköznek |
| `inherit(name_or_theme)` | a blokkon belül lecseréli az alaptémát |

```ruby
require 'bbs'

APP = BBS::Application.define do
  theme :neumanntronics do
    accent      "\e[1;33;44m"
    style :text, "\e[0;37;44m"
  end
end
```

Blokk nélkül egy preset nevét vagy egy `BBS::Theme`-példányt adj át: a `theme :synchronet` és a `theme BBS::Theme.synchronet` ugyanaz (0.17.1 óta; a 0.17.0-ban a Symbol-forma az első képkockán elszállt). Argumentum nélkül a `theme` az eddig beállított témát adja vissza. A presetek és a stíluskulcsok a [Rajzolás](https://wiki.teletypegames.org/hu/engines/rubbs/drawing) oldalon vannak.

## services

```text
services(hash = nil)
```

A `hash`-t beolvasztja a definíció service-ei közé; akárhányszor hívható. Argumentum nélkül visszaadja a hasht. A service-ekből lesz az `app.services`, és minden window spec callbackjének `ctx`-ébe is bekerülnek (`ctx[:notes]`).

```ruby
require 'bbs'

class Clock
  def now = Time.now
end

APP = BBS::Application.define do
  services clock: Clock.new
  services motd: 'Be excellent to each other.'

  window :motd, type: :info do |w|
    w.title ' Message of the day '
    w.body  { |ctx| [ctx[:motd], :spacer, ctx[:clock].now.to_s] }
  end

  init { open_window(:motd) }
end
```

A hash és az objektumai egyszer jönnek létre, és minden hívó közösen használja őket. A hívónkénti állapot a `context`-be való, a közös objektumok legyenek thread-safe-ek: minden hívó saját szálon fut.

## helpers

```text
helpers { def name ... end }
```

A blokk egy modulba értékelődik ki, és a definícióból épített minden application ezzel a modullal bővül (`extend`). A helper-metódusok az applicationön futnak, így a teljes példány-API-t hívhatják (`open_window`, `context`, `services`, `statusbar`, ...), és őket is hívhatja minden, ami az applicationön fut: a menüpontok, a státusztippek, az `init`, az `on_tick`, és a window-callbackek a `ctx[:app]`-on át.

```ruby
require 'bbs'

APP = BBS::Application.define do
  helpers do
    def greet = BBS::Dialogs.message(self, "Hello, #{context[:username]}!")
    def sysop? = context[:username] == 'sysop'
  end

  menubar do
    menu '&Hello' do
      item '&Greet' do greet end
    end
  end

  init { greet if sysop? }
end
```

A `helpers` többször is hívható; a metódusok ugyanabba a modulba gyűlnek.

## window

```text
window(name, type:) { |w| ... }
```

Egy `BBS::WindowSpec`-et regisztrál `name` néven; a `type` lehet `:info`, `:form`, `:master_detail` vagy `:stream`. Megnyitás: `app.open_window(name, **extras)`. Minden attribútum, mező és callback a [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows) oldalon, a window DSL résznél van leírva.

```ruby
require 'bbs'

APP = BBS::Application.define do
  window :about, type: :info do |w|
    w.title ' About '
    w.body  ['Notes desk 1.0', :spacer, { kv: [['Ruby', RUBY_VERSION]] }]
  end

  status_hint('F1', 'About') { open_window(:about) }
end
```

## door

```text
door(name, program, label: nil)
```

Egy `BBS::Door`-t deklarál. A `program` egy `BBS::FakeOS`-definíció, egy `BBS::Flow`, vagy bármi, ami válaszol a `call(session, context)`-re; a `label` a menüpontja szövege (a `&` jelöli a gyorsbillentyűt), alapértelmezése a név nagybetűsítve. A door magától nem indul: az `app.open_door(name)` hagyja el rajta át az applicationt, a `menu`-n belüli `doors` pedig minden deklarált doorhoz felvesz egy menüpontot. A door-programok a [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors) oldalon vannak.

```ruby
require 'bbs'

COIN = ->(session, _context) { side = %w[Heads Tails].sample; session.write("\r\n  #{side}!\r\n"); sleep 2 }

APP = BBS::Application.define do
  door :coin, COIN, label: '&Coin toss'

  menubar do
    menu '&Doors' do
      doors
    end
  end
  status_hint('F9', 'Coin') { open_door(:coin) }
end
```

## menubar

```text
menubar { menu(label) { item ...; separator; doors } }
```

A menüsort deklarálja. A blokk minden `build`-nél kiértékelődik, egy `MenubarBuilder`-en:

| Metódus | Szignatúra | Mit csinál |
|---|---|---|
| `menu` | `menu(label) { ... }` | egy legördülő menü; a címkében a `&` jelöli az Alt-gyorsbillentyűt |
| `item` | `item(label, shortcut: nil, enabled: true) { \\|item\\| ... }` | egy menüpont; a blokk az applicationön fut |
| `separator` | `separator` | vízszintes vonal; sosem választható |
| `doors` | `doors` | minden deklarált doorhoz egy `item`, deklarálási sorrendben, mindegyik `open_door`-t hív |

Az `item` opciói:

| Opció | Alapérték | Jelentés |
|---|---|---|
| `label` | — | a szöveg; a betű előtti `&` jelöli a gyorsbillentyűt (`'E&xit'`) |
| `shortcut:` | `nil` | a legördülő menüben jobbra igazítva kiírt szöveg (`'Alt-X'`). Csak felirat: magát a billentyűt `status_hint`-tel kösd be |
| `enabled:` | `true` | `false` esetén a pont szürke és nem választható. Egy lambdát a menü minden rajzolásakor és használatakor meghív az applicationön (`self` az application): ha truthy, a pont engedélyezett, pl. `enabled: -> { context[:doc] }` |

A menüpont blokkja az applicationön fut (`self` az application), és megkapja a `Menubar::MenuItem`-et. A visszatérési érték számít: `:halt` befejezi az applicationt, minden más mindegy.

```ruby
require 'bbs'

APP = BBS::Application.define do
  menubar do
    menu '&File' do
      item '&Open...', shortcut: 'F3' do BBS::Dialogs.message(self, 'Nothing to open.') end
      item '&Print', enabled: false
      separator
      item 'E&xit', shortcut: 'Alt-X' do :halt end
    end
    menu '&Help' do
      item '&About' do |it| BBS::Dialogs.message(self, "You picked #{it.label.delete('&')}.") end
    end
  end
end
```

Hogy a hívó szemszögéből hogyan viselkedik a menüsor, az lent, A menüsor résznél van.

## status\_hint

```text
status_hint(key, label, hidden: false) { ... }
```

Egy bejegyzést vesz fel a státuszsorra, deklarálási sorrendben. Az első `status_hint` hozza létre a státuszsort; ha egy definícióban nincs ilyen, nincs státuszsor sem (és az `app.statusbar` `nil`).

| Argumentum | Jelentés |
|---|---|
| `key` | a billentyű szövegként: `'F1'`…`'F12'`, `'Alt-X'`, `'Esc'`, `'Tab'`, vagy bármely billentyűnév (lásd lent: Billentyűnevek a tippekben) |
| `label` | a billentyű utáni szöveg |
| `hidden:` | `true`: a billentyű működik, de nem foglal helyet a soron, így marad hely a `right_text`-nek |
| blokk | az applicationön fut, amikor megnyomják a billentyűt vagy rákattintanak a tippre; `:halt` befejezi az applicationt |

```ruby
require 'bbs'

APP = BBS::Application.define do
  status_hint('F1', 'Help') { BBS::Dialogs.message(self, 'Alt+letter opens a menu.') }
  status_hint('F3', 'Search', hidden: true) { BBS::Dialogs.input(self, prompt: 'Find:') }
  status_hint('Alt-X', 'Bye') { :halt }
end
```

Egy státusztipp csak akkor kapja meg a billentyűt, ha előtte sem egy window, sem egy menü nem használta fel (lásd lent: Eseménykézbesítés). A funkcióbillentyűk mindig szabadok, egy betű nem.

## body

```text
body { add(widget) }
```

A blokk egy kis `BodyContext`-en fut, két metódussal: `add(widget)` (a gyökérpanelhez adja és rááll a téma) és `app`. Az asztali widgetek helye; ugyanezt az `init` is megteheti az `add`-dal.

```ruby
require 'bbs'

APP = BBS::Application.define do
  body do
    title = BBS::Widgets::Label.new(text: 'NOTES DESK', style_key: :accent, align: :center)
    title.layout(1, 3, app.cols, 1)
    add title
  end
end
```

A `body` az `init` után fut.

## init

```text
init { ... }
```

Minden frissen épített applicationön lefut, miután a menüsor és a státuszsor már megvan, és a `body` előtt. Itt nyílik az első window, itt kerülnek widgetek az asztalra, itt kap értéket a `statusbar.right_text`, itt olvasható a `context`. Minden door után újra lefut, mert az application újraépül: az egyszeri dolgokat egy context-jelzővel védd.

```ruby
require 'bbs'

APP = BBS::Application.define do
  window :welcome, type: :info do |w|
    w.title ' Welcome '
    w.body  ['Glad you called.']
  end
  status_hint('Alt-X', 'Bye') { :halt }

  init do
    statusbar.right_text = Time.now.strftime('%H:%M')
    unless context[:welcomed]
      context[:welcomed] = true
      open_window(:welcome)
    end
  end
end
```

## on\_tick

```text
on_tick { ... }
```

Az applicationön fut minden olyan másodperc után, amikor nem jött bemenet (a futási ciklus egy másodperces időkorláttal olvassa a billentyűket). Tipikusan: óra vagy számláló a `statusbar.right_text`-ben, egy service lekérdezése, jelenlét-frissítés. A window-knak saját tick-horgonyuk van, a `Window#tick(app)`; előbb az `on_tick` fut, aztán minden nyitott window `tick`-je.

```ruby
require 'bbs'

APP = BBS::Application.define do
  status_hint('Alt-X', 'Bye') { :halt }
  on_tick { statusbar.right_text = Time.now.strftime('%H:%M:%S') }
end
```

Tick csak akkor jön, ha a hívó tétlen: aki folyamatosan gépel, nem kap egyet sem. Pontos időzítésre ne építs rá.

## on\_idle

```text
on_idle { ... }
```

Az applicationön fut, amikor a hívó a session `idle_seconds`-áig csendben volt (a 0.19.0 óta). Ha `:keep`-et ad vissza, a hívó vonalban marad, és a tétlenség számolása elölről indul; bármi mást ad vissza, a hurok kiírja: „Disconnected after N s idle.”, és véget ér, ahogy a blokk nélkül is.

```ruby
require 'bbs'

APP = BBS::Application.define do
  on_idle do
    next nil if @asked                  # asked last time, no answer: drop
    @asked = true
    BBS::Dialogs.confirm(self, 'Still there?', on_yes: -> { @asked = false })
    :keep                               # one more idle period to answer
  end
end
```

A párbeszédablak nem blokkol: megnyit egy ablakot, és a hurok megy tovább, ezért a blokk `:keep`-et ad vissza, hogy a hívónak még egy tétlenségi ideje legyen válaszolni. `idle_seconds` nélkül (se a sessionön, se a configban) nincs limit, és a blokk sosem fut le.

## on\_door

```text
on_door { |door, context| ... }
```

A `Builder#run` hívja, közvetlenül mielőtt átadja a vonalat egy doornak, a `BBS::Door`-ral (`name`, `label`, `program`) és a contexttel. Az application ekkor már nem létezik, ezért a blokk sima blokként hívódik (a `self` az, ahol a `define` íródott), nem az applicationön.

```ruby
require 'bbs'

APP = BBS::Application.define do
  door :coin, ->(session, _ctx) { session.write("Heads!\r\n"); sleep 1 }
  on_door { |door, context| warn "#{context[:username]} -> door #{door.label}" }
  status_hint('F9', 'Coin') { open_door(:coin) }
end
```

## A Builder metódusai

| Metódus | Mit csinál |
|---|---|
| `build(session:, session_id:, context: {})` | egy új `Application`: menüsor, státuszsor, `init`, `body` kész, de még nem fut |
| `run(session:, session_id:, context: {})` | `build` + `Application#run`, az application–door körben; akkor tér vissza, amikor a hívó door nélkül lép ki, vagy a vonal megszűnik |

Az 1.0 óta a buildernek nincsenek olvasói: a regisztrált window specek és doorok a futó application `window_specs`-e és `doors`-a.

# A példány API-ja

Minden itt felsorolt metódus a futó `BBS::Application`-é. A helperek, a menüpontok, a státusztippek, az `init` és az `on_tick` közvetlenül hívják őket, a window-callbackek a `ctx[:app]`-on át.

## Állapot

| Metódus | Visszaadja |
|---|---|
| `session` | a hívó `BBS::Session`-jét |
| `session_id` | a `build`-nek / `run`-nak átadott azonosítót |
| `context` | a hívónkénti hasht (lásd lent: A context) |
| `services` | a közös service-hasht |
| `theme` | a használt `BBS::Theme`-et |
| `cols`, `rows` | a terminál méretét a NAWS-ból (80 és 24, ha a kliens nem közölte) |
| `root` | a gyökérpanelt (`BBS::Container`) |
| `menubar`, `statusbar` | a sorokat, vagy `nil`-t |
| `windows` | a nyitott window-kat hátulról előre (az utolsó az aktív) |
| `window_specs`, `doors` | a regisztrált window speceket és doorokat, név szerint |
| `desktop_rect` | `[x, y, width, height]` a menüsor és a státuszsor között: ide kerül egy nagyított window |

## A képernyő felépítése

| Metódus | Mit csinál |
|---|---|
| `add(widget)` | a gyökérpanelhez ad egy widgetet, beállítja a témáját, visszaadja |
| `menubar = menubar` | beállítja (`nil`-lel eltávolítja) a menüsort, ráállítja a témát |
| `statusbar = statusbar` | ugyanez a státuszsorral |

```ruby
require 'bbs'

APP = BBS::Application.define do
  init do
    hint = BBS::Widgets::Label.new(text: 'F10 opens the menu', style_key: :text_dim)
    hint.layout(5, 5, 26, 1)
    add hint
  end
end
```

## Window-k

| Metódus | Mit csinál |
|---|---|
| `open_window(name, **extras)` | megnyitja a `name` néven regisztrált window specet; az `extras` a `ctx`-ébe kerül; ismeretlen névre `ArgumentError`; a window-t adja vissza |
| `open_window(window)` | egy `BBS::Window`-t tesz a veremre és aktívvá teszi: beállítja a témáját, az `app`-ját, és (ha nem `modal`) a legkisebb szabad számot 1 és 9 között |
| `close_window(window = top_window)` | eltávolítja a window-t, aktívvá teszi az alatta lévőt, aztán meghívja a window `on_close`-át a window-val |
| `top_window` | az aktív window, vagy `nil` |
| `raise_window(window)` | egy nyitott window-t előrehoz és aktívvá tesz |
| `switchable_windows` | a nem modális window-k szám szerint rendezve; üres, amíg modális window van nyitva |
| `cycle_windows(step = 1)` | előrehozza a következő (`1`) vagy előző (`-1`) váltható window-t; kettőnél kevesebbnél `nil` |

```ruby
require 'bbs'

APP = BBS::Application.define do
  window :a, type: :info do |w| w.title ' A ' end
  window :b, type: :info do |w| w.title ' B ' end

  status_hint('F2', 'Both') { open_window(:a); open_window(:b) }
  status_hint('F3', 'A to front') do
    a = windows.find { |w| w.title == ' A ' }
    raise_window(a) if a
  end
  status_hint('F4', 'Close all') { close_window until windows.empty? }
end
```

A window-számok, a váltás, a mozgatás és a méretezés a [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows) oldalon van.

## Hosszú munka és a nyers vonal

| Metódus | Mit csinál |
|---|---|
| `with_loading(text = 'Working...') { ... }` | „Please wait” window-t mutat a `text`-tel, lefuttatja a blokkot (szálban, amit rögtön be is vár), bezárja a window-t, és a blokk értékét adja vissza |
| `with_line { \\|session\\| ... }` | letörli a képernyőt, kikapcsolja az egérjelentést, a nyers sessiont átadja a blokknak, aztán visszakapcsolja az egeret (ha `BBS.config.mouse`), és teljes újrarajzolást kér; a blokk értékét adja vissza |

```ruby
require 'bbs'
require 'net/http'

APP = BBS::Application.define do
  helpers do
    def fetch_motd
      text = with_loading('Fetching...') do
        Net::HTTP.get(URI('https://example.org/')).lines.first.to_s.strip
      rescue StandardError
        nil
      end
      BBS::Dialogs.message(self, text || 'No answer.', title: ' MOTD ')
    end

    def raw_screen
      with_line do |session|
        session.write("A plain terminal for a moment. Press ENTER.\r\n")
        session.readline
      end
    end
  end

  status_hint('F2', 'MOTD') { fetch_motd }
  status_hint('F3', 'Raw') { raw_screen }
end
```

A `with_loading` blokkja alatt a képernyő nem frissül, a közben leütött billentyűk a vonalon várnak. A `with_line` való a ZMODEM-letöltésekhez (`BBS::Zmodem.send_files(session, ...)`, lásd [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors)) és a teljes képernyős ANSI-art nézegetőkhöz.

## Futás és kilépés

| Metódus | Mit csinál |
|---|---|
| `run` | az eseményciklus; akkor tér vissza, amikor az application véget ér |
| `stop!` | a ciklus a most kezelt esemény után véget ér |
| `tick!` | most azonnal lefuttatja az `on_tick`-et és minden window `tick`-jét |
| `open_door(name)` | a doort a `context[:door]`-ba teszi, meghívja a `stop!`-ot, és `:halt`-ot ad vissza; ismeretlen névre `ArgumentError` |
| `line_lost?` | igaz, ha a futás azért ért véget, mert a vonal megszűnt: bontás, az idle-limit, `IOError`; ilyenkor a `Builder#run` nem nyit doort |

Az `open_door` bármelyik kezelőből működik: menüpontból, státusztippből, egy window billentyűkezelőjéből, egy `Dialogs.confirm`-callbackből, és a window-gombokból (az `Info` gombjai, a `MasterDetail` actionjei, a `Form` gombjai) vagy a `MasterDetail` `on_activate`-jéből is, pedig ezek elnyelik a visszatérési értéket. Az eseményciklust maga állítja le, így az application a most kezelt esemény után véget ér, és a `Builder#run` megnyitja a doort:

```ruby
require 'bbs'

APP = BBS::Application.define do
  door :coin, ->(session, _ctx) { session.write("Heads!\r\n"); sleep 1 }

  window :lobby, type: :info do |w|
    w.title ' Lobby '
    w.body  ['Feeling lucky?']
    w.button(label: '&Toss a coin') do |_win, ctx|
      ctx[:app].open_door(:coin)
    end
  end

  init { open_window(:lobby) }
end
```

A 0.17.0-ban egy gombnak az `open_door` után `ctx[:app].stop!`-ot is kellett hívnia; 0.17.1 óta erre nincs szükség (de nem is árt). Ha a futás azért ér véget, mert a vonal megszűnt (`line_lost?`), a `Builder#run` megnyitás nélkül eldobja a `context[:door]`-t, így a bontás előtt épp kiválasztott door nem nyílik meg.

# A futási ciklus

```mermaid
flowchart TD
  R[képkocka rajzolása, a különbség kiküldése] --> K{"readkey, 1 mp időkorlát"}
  K -- nil: bontott --> E[vége]
  K -- tétlen --> T["tick!: on_tick, aztán window.tick"]
  T --> I{"elérte az idle_seconds-ot?"}
  I -- nem --> R
  I -- igen --> M[üzenet: Disconnected] --> E
  K -- billentyű vagy egér --> D[az esemény kézbesítése]
  D -- ":halt" --> E
  D --> R
```

Az `Application#run`:

1. Kirajzolja a képkockát, és csak a változást küldi ki.
2. Legfeljebb egy másodpercig vár egy billentyűre (`Session#readkey(timeout: 1.0)`).
3. Ha abban a másodpercben nem jött semmi: lefuttatja a `tick!`-et, és a másodpercet a tétlen időhöz adja. Ha a tétlen idő eléri a session `idle_seconds`-át (a `BBS.config.idle_seconds`-ból; ha be van állítva), lefuttatja az `on_idle` blokkot: `:keep` esetén elölről kezdi a számolást, egyébként kiírja: „Disconnected after N s idle.”, és véget ér.
4. Billentyű vagy egéresemény: nullázza a tétlen időt, `BBS::Event`-be csomagolja és kézbesíti. Ha a kézbesítés `:halt`-ot ad, a ciklus véget ér.
5. Bontás (`readkey` `nil`-t ad, vagy `IOError`, `EPIPE`, `ECONNRESET`) csendben befejezi a ciklust.

Ha egy kezelő (egy menüpont, egy callback) kivételt dob, az a backtrace első három sorával a stderr-re kerül, az esemény pedig elvész: egyetlen hiba nem bontja a hívó sessionjét. Kilépéskor a ciklus visszakapcsolja a kurzort és letörli a képernyőt.

## Eseménykézbesítés

Minden esemény végigmegy ezen a láncon; az első, aki elveszi, megállítja.

| Lépés | Ki | Mit vesz el |
|---|---|---|
| 1 | a nyitott menü | mindent, amíg menü van nyitva |
| 2 | window-váltás | F6, Shift-F6, Alt-1…9; az aktív mögötti window-ra kattintás előrehozza, és a kattintás megy tovább |
| 3 | az aktív window | a fókuszált widgete, aztán Tab, gomb-gyorsbillentyűk, F5, Ctrl-F5, a bezáró billentyűje |
| 4 | a menüsor | F10, egy menü Alt+betűje, kattintás az 1. sorban |
| 5 | a státuszsor | a tippek billentyűi, kattintás a sávra rajzolt tippre |
| 6 | a gyökérpanel | Tab / Shift-Tab a gyökér-widgetek között, aztán a fókuszált gyökér-widget |

Amit ebből érdemes tudni:

- Az az Alt+betű, ami az aktív window egy gombjának gyorsbillentyűje, azt a gombot nyomja meg, akkor is, ha egy menünek ugyanaz a betűje: a window a menüsor előtt jön.
- A szövegmezőbe gépelt betű sosem jut el egy státusztippig. Tippekhez funkcióbillentyűt és Alt-kombinációt használj.
- Nyitott window mellett a Tab a window-é; a gyökér-widgetek addig nem kapnak billentyűt, amíg minden window be nincs zárva.
- A gyökérpanelre kattintás csak a fókuszált gyökér-widgethez jut el. Ha két gomb van az asztalon, csak a fókuszált reagál a kattintásra, és a gyökérpanel gombjainak nincs betű-gyorsbillentyűjük.

## A kezelők visszatérési értéke

A lánc minden kezelője (egy widget vagy window `handle_event`-je, egy menüpont, egy státusztipp) a visszatérési értékével válaszol:

| Érték | Jelentés |
|---|---|
| `nil` / `false` | nem az enyém: az esemény megy tovább a láncon |
| bármi más (`:handled`, `true`, egy window) | elvettem, a lánc megáll |
| `:halt` | elvettem, és az application véget ér |
| `:close_window` | window-tól: az application bezárja (a `Window#close` ezt adja vissza) |

A menüpontok és a státusztippek bármit adnak vissza, elvettnek számítanak (kivéve a `:halt`-ot).

## Az application–door kör

A `Builder#run` az `Application#run` köré épített kör, ami átviszi a hívót a doorokon:

```mermaid
sequenceDiagram
  participant F as Flow call lépés
  participant B as Builder#run
  participant A as Application
  participant D as Door
  F->>B: run(session:, session_id:, context: ctx)
  loop amíg nincs door
    B->>A: build + run
    A-->>B: véget ért (open_door beállította a context[:door]-t?)
    B->>B: door = context.delete(:door)
    B->>D: on_door(door, ctx), door.run(session, ctx)
    D-->>B: a program visszatért
  end
  B-->>F: visszatér
```

Egy door után az application **újraépül**: az `init` újra lefut, a korábban nyitott window-k eltűnnek, a menüsor és a státuszsor új. Ami túl kell élje a doort, az a `context`-be kerüljön; a Teletype BBS így tartja meg a `context[:mail_seen]`-ben, hány olvasatlan levélre csengetett már, és a `context[:call_started]`-ben a hívás kezdetét.

A kör akkor is véget ér, ha az application azért állt le, mert a vonal megszűnt (`Application#line_lost?`: bontás, az idle-limit): a `context[:door]`-ban már ott lévő doort ilyenkor eldobja, nem nyitja meg.

# A context

A `context` egy sima Hash, ami a hívásé, nem az applicationé.

- Flow-ból indítva maga a flow `ctx`-e: `:session_id`, minden `ask`-válasz (`:username`), és amit a korábbi lépések beleírtak.
- Az application szabadon olvassa és írja; a flow az application visszatérése után látja a változásokat.
- Túléli az application–door kört; egy `BBS::Flow`-door a másolatát kapja (lásd [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors)), egy `call(session, context)`-door magát a hasht.
- Az `open_door` egy pillanatra a `:door` kulcs alá teszi a doort; a `Builder#run` kiveszi onnan.

A window spec callbackjei nem közvetlenül a `context`-et kapják, hanem egy három forrásból összefésült `ctx`-et, azonos kulcsnál a későbbi nyer:

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

A `ctx` minden megnyitott window-nál új Hash: a `ctx[:x] = 1` nem változtatja meg a contextet. Írd így: `ctx[:app].context[:x] = 1`.

# Események

A `BBS::Event` (`lib/bbs/event.rb`) az egységesített bemenet, amit minden widget és window kap:

```ruby
Event = Struct.new(:kind, :key, :mouse, :x, :y, keyword_init: true)
```

| Mező | Jelentés |
|---|---|
| `kind` | `:key`, `:mouse`, `:idle`, `:tick` vagy `:resize` |
| `key` | `:key` esetén: Symbol (`:enter`, `:up`, `:f5`, `:alt_x`, `:ctrl_f5`, `:shift_tab`) vagy egykarakteres String (`'a'`, `'é'`) |
| `mouse` | `:mouse` esetén: a `BBS::Telnet::MouseEvent` (`kind`: `:press`, `:release`, `:drag`, `:wheel`; `button`; `x`, `y`; `mods`) |
| `x`, `y` | `:mouse` esetén: az 1-től számozott képernyőcella |

| Konstruktor / predikátum | Mit csinál |
|---|---|
| `Event.key(name)` | billentyűesemény |
| `Event.mouse(mouse_event)` | egéresemény, az `x`/`y` belőle másolva |
| `Event.idle`, `Event.tick`, `Event.resize(cols, rows)` | a többi fajta (maga a futási ciklus csak billentyű- és egéreseményt gyárt) |
| `key?`, `mouse?` | a fajta |
| `char?` | nyomtatható, egykarakteres billentyű |
| `alt?`, `alt_char` | `:alt_x` alakú billentyű, és a betűje (`'x'`) |

```ruby
require 'bbs'

event = BBS::Event.key(:alt_s)
p event.key?      # => true
p event.alt?      # => true
p event.alt_char  # => "s"
p BBS::Event.key('q').char?  # => true
```

Egy saját widget a `handle_event(event)`-ben válaszol:

```ruby
require 'bbs'

class Counter < BBS::Widget
  attr_reader :count

  def initialize(**kw)
    super
    @count = 0
  end

  def focusable? = true

  def handle_event(event)
    return nil unless event.key? && event.key == '+'

    @count += 1
    :handled
  end

  def render(frame)
    frame.move(bounds.x, bounds.y)
    frame.write("Count: #{@count}", sgr: style(focused ? :input_focused : :text))
  end
end
```

A billentyű-szimbólumok teljes listája és az egérmodell a [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store) oldalon van; widgetet írni a [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) oldal tanít.

# Fókusz

A `BBS::FocusManager` (`lib/bbs/focus_manager.rb`) tartja számon egy konténeren belül a fókuszált widgetet. Minden window-nak van egy (`window.focus_manager`), a gyökérpanel pedig az első eseményénél kap egyet.

| Metódus | Mit csinál |
|---|---|
| `FocusManager.new(container)` | manager, ami az első jelöltre teszi a fókuszt |
| `candidates` | a konténer látható, engedélyezett, fókuszálható leszármazottai, fa-sorrendben |
| `focused` | a fókuszált widget, vagy `nil` |
| `focus(widget)` | ráteszi a fókuszt, ha fókuszálható és engedélyezett |
| `focus_first`, `next_focus`, `prev_focus` | a fókusz léptetése; a következő és az előző körbefordul |
| `handle_focus_key(event)` | Tab → `next_focus`, Shift-Tab → `prev_focus`; `:handled`, ha elvette a billentyűt, különben `nil` |
| `dispatch(event)` | továbbadja az eseményt a fókuszált widgetnek |

A fókusz mozgatásakor mindkét widget `focused`-ja beállítódik, a régin `on_blur`, az újon `on_focus` hívódik. A jelölteket minden lépésnél újra kikeresi, így a később hozzáadott widgetek is részt vesznek; az első fókuszt viszont a manager létrehozásakor választja. Kézzel épített window után hívd meg a `window.reset_focus`-t (új manager), aztán a `window.focus_manager.focus(widget)`-et a kezdőponthoz:

```ruby
require 'bbs'

def open_search(app)
  win = BBS::Window.new(title: ' Search ')
  win.layout(20, 8, 40, 6)
  query = BBS::Widgets::TextInput.new(placeholder: 'words...')
  query.layout(22, 10, 36, 1)
  win.add(query)
  go = BBS::Widgets::Button.new(label: '&Go', on_click: ->(_b) { app.close_window(win) })
  go.layout(36, 12, 8, 1)
  win.add(go)
  win.reset_focus
  win.focus_manager.focus(query)
  app.open_window(win)
end
```

Egy window-n belüli bal kattintás a mutató alatti widgetre teszi a fókuszt; az egérgörgő akkor is a mutató alatti widgethez megy, ha a fókusz máshol van.

# A menüsor

A `BBS::Menubar` (`lib/bbs/menu.rb`) az 1. sor sávja. A DSL építi fel, de kézzel is elkészíthető.

## Viselkedés

- Minden menücímke kiemelt gyorsbillentyű-betűvel jelenik meg (`&File` → **F**ile).
- Az **F10** az első menüt nyitja, az **Alt+betű** azt, amelyiknek az a gyorsbillentyűje. Menücímre kattintás kinyitja, egy második kattintás bezárja. Mindez a `Menubar#handle_event`-en megy át; a belső billentyűkezelés privát.
- Amíg menü van nyitva, minden billentyűt és kattintást ő kap, így semmi nem szivárog át a window-khoz: a **Bal/Jobb** a szomszéd menüre lép (körbefordulva), a **Fel/Le** a pontok között lép, a separatorokat és a letiltott pontokat átugorva, az **Enter** választ, egy **betű** az azzal a gyorsbillentyűvel jelölt pontot választja, az **Esc** vagy az **F10** bezár. Pontra kattintás választ, a menün kívüli kattintás bezárja a menüt.
- Választáskor a menü bezárul, aztán lefut a pont blokkja. Ha az `:halt`-ot ad, az application véget ér.
- A jobb szélhez közeli legördülő menü balra tolódik, hogy kiférjen a képernyőre.

## Kézzel építve

| Metódus | Mit csinál |
|---|---|
| `Menubar.new` | üres menüsor |
| `menus` | a `Menubar::Menu` structok (`label`, `items`, `x`); a kézzel épített menüsor ide teszi a menüit. Az `add_menu` és a `MenuBuilder` az 1.0-ban megszűnt: a DSL `menubar do menu('&File') { item … } end` alakja az út, kézzel pedig a pont `on_select`-je a pontot kapja, és **nem** az applicationön fut, ezért az `app`-ot zárd be a blokkba |
| `open(index)`, `close`, `open?` | menü nyitása pozíció szerint, bezárás, lekérdezés |
| `opened_menu`, `selected_item` | mi van nyitva és kijelölve |
| `handle_event(event)` | billentyűk és kattintások; az application hívja, a nyitott menüt elsőként |
| `on_activate` | callable, ami minden választás után megkapja a pontot |

A `Menubar::MenuItem` egy struct: `label`, `shortcut`, `enabled`, `on_select`, `separator`, `hint`; a `hotkey` és a `display` (a címke `&` nélkül) ezekből számolódik. Az `enabled?`-et kérdezi a menüsor: szeparátorra hamis, ha az `enabled` hívható, a lambda válasza (úgy, ahogy van hívja; csak a DSL `item`-je futtatja az applicationön), különben igaz, hacsak az `enabled` nem `false`.

```ruby
require 'bbs'

def run_plain_app(session, session_id)
  app = BBS::Application.new(session: session, session_id: session_id, theme: BBS::Theme.dosnavigator)

  item = ->(label, shortcut: nil, &block) { BBS::Menubar::MenuItem.new(label: label, shortcut: shortcut, on_select: block) }
  app.menubar = BBS::Menubar.new
  app.menubar.menus << BBS::Menubar::Menu.new(label: '&File', items: [
    item.('&Hello') { BBS::Dialogs.message(app, 'Hello!') },
    BBS::Menubar::MenuItem.new(label: '', separator: true, enabled: false),
    item.('E&xit', shortcut: 'Alt-X') { :halt }
  ])

  app.statusbar = BBS::Statusbar.new(right_text: 'plain')
  app.statusbar.add_hint('F1', 'Hello') { BBS::Dialogs.message(app, 'Hello!') }
  app.statusbar.add_hint('Alt-X', 'Quit') { :halt }

  label = BBS::Widgets::Label.new(text: 'Background', style_key: :text)
  label.layout(3, 3, 20, 1)
  app.add(label)

  app.run
end
```

A `BBS::Application.new(session:, session_id:, context: {}, theme: nil, on_idle: nil, on_tick: nil, helpers: nil, services: {}, window_specs: {}, doors: {})` az, amit a `build` hív; az `on_tick` egy callable, ami az applicationt kapja. Az `on_idle` egy callable, ami az applicationt kapja, és a tétlenségi limitnél hívódik (lásd *on\_idle*).

# A státuszsor

A `BBS::Statusbar` (`lib/bbs/statusbar.rb`) az utolsó sor sávja: balra a tippek (a billentyű a `:statusbar_key` stílusban, utána a címke), jobbra egy opcionális szöveg.

| Tag | Mit csinál |
|---|---|
| `Statusbar.new(hints: [], right_text: nil)` | egy státuszsor |
| `add_hint(key, label, hidden: false) { ... }` | felvesz egy tippet; a blokk akkor hívódik, amikor megnyomják a billentyűjét vagy rákattintanak (kattintani csak a sávra rajzolt tippre lehet) |
| `hints` | a `Statusbar::Hint` structok (`key`, `label`, `on_activate`, `hidden`) |
| `shown` | a nem rejtett tippek |
| `right_text`, `right_text=` | a jobbra igazított szöveg; csak akkor rajzolódik ki, ha nem lóg rá a tippekre |

Ami nem fér ki, az a végéről lemarad; a rejtett tippek nem foglalnak helyet, így lehet egy billentyűt működésben tartani úgy, hogy a hely a `right_text`-é legyen. Ha egy tipp blokkja `:halt`-ot ad, az application véget ér; minden más visszatérési érték elvettnek számít.

```ruby
require 'bbs'

APP = BBS::Application.define do
  status_hint('F1', 'Help') { BBS::Dialogs.message(self, 'No help yet.') }
  status_hint('F5', 'Refresh', hidden: true) { statusbar.right_text = 'refreshed' }
  status_hint('Alt-X', 'Bye') { :halt }

  init    { statusbar.right_text = "#{context[:username]} │ #{Time.now.strftime('%H:%M')}" }
  on_tick { statusbar.right_text = "#{context[:username]} │ #{Time.now.strftime('%H:%M')}" }
end
```

## Billentyűnevek a tippekben

| Tipp-kulcs | Erre illeszkedik |
|---|---|
| `'F1'` … `'F12'` | `:f1` … `:f12` |
| `'Alt-X'` | `:alt_x` (bármely betű vagy szám, kis-nagybetűtől függetlenül) |
| `'Esc'` | `:escape` |
| `'Tab'` | `:tab` |
| bármi más | a billentyű saját neve, kis-nagybetűtől függetlenül: `'enter'`, `'ctrl_s'`, `'?'` |

A `'Ctrl-S'` semmire sem illeszkedik (a billentyű `:ctrl_s`); írd `'ctrl_s'`-nek, vagy tedd a billentyűt egy rejtett tippbe, és egy látható tipp címkéjébe írd, hogy „Ctrl-S”. Az `Esc` és a `Tab` többnyire előbb egy window-hoz jut el.

# Billentyűk és egér a hívónak

Ezt teheti a hívó bármely rubbs-applicationben. Érdemes egy változatát a Help menübe tenni; a Teletype BBS és a Feedia BBS a Help → Keys alatt mutat egyet.

| Billentyű / művelet | Mit csinál |
|---|---|
| **F10** | kinyitja az első menüt; nyitott menüt bezár |
| **Alt+betű** | kinyitja az azzal a gyorsbillentyűvel jelölt menüt, vagy megnyomja az aktív window azzal jelölt gombját |
| **Bal / Jobb** (nyitott menü) | előző / következő menü |
| **Fel / Le** (nyitott menü) | előző / következő pont |
| **Enter** / **betű** (nyitott menü) | a kijelölt pontot / az azzal a betűvel jelölt pontot választja |
| **Esc** | bezárja a nyitott menüt, vagy az aktív window-t |
| **Tab / Shift-Tab** | következő / előző vezérlő a window-ban |
| **Enter / Szóköz** | megnyomja a fókuszált gombot, átbillenti a fókuszált jelölőnégyzetet |
| **betű** | megnyomja az azzal a gyorsbillentyűvel jelölt gombot, ha a fókuszált vezérlő nem fogad betűt |
| **F5** vagy **Alt-Enter** | nagyítja a window-t és vissza (a `[↑]` gombos window-knál) |
| **F6 / Shift-F6** | következő / előző window |
| **Alt-1** … **Alt-9** | a keretén azzal a számmal jelölt window |
| **Ctrl-F5** (vagy **F6**, ha csak egy window van nyitva) | mozgató mód: a nyilak mozgatnak, a Shift+nyilak (vagy Tab, aztán nyilak) méreteznek, Enter vagy Esc befejezi |
| **státuszsor-billentyűk** | amit az application odatett (F1, F2, Alt-X, ...) |
| kattintás menücímre / pontra | kinyitja a menüt / választja a pontot |
| kattintás státusztippre | ugyanaz, mint a billentyűje |
| kattintás a `[■]`-ra | bezárja a window-t |
| kattintás a `[↑]` / `[↕]`-ra, dupla kattintás a címsoron | nagyítás / visszaállítás |
| a címsor húzása | mozgatja a window-t |
| a `┘` sarok húzása | méretezi a window-t |
| kattintás egy hátsó window-ra | előrehozza |
| egérgörgő | görgeti a mutató alatti listát vagy szöveget |

Az egér akkor működik, ha a szerveren `c.mouse = true`, és a terminál SGR vagy X10 egéreseményeket küld (lásd [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store)).

# Valódi boardok

| Board | Fájl | Mit érdemes megnézni benne |
|---|---|---|
| Feedia | `bbs/feedia-bbs/lib/ui/modern.rb` | egy kicsi, teljes application: két master-detail spec, két űrlap, `with_loading`-os helperek, `init`-ben épített fejléc, óra `on_tick`-kel |
| Alexandria | `bbs/alexandria-bbs/lib/ui/modern.rb` | hívónkénti nyelv a `context`-ben, `replace_items` a navigációhoz, Markdown a részletpanelen |
| Teletype BBS | `bbs/teletype-bbs/lib/ui/modern.rb` | listából deklarált doorok, jelenlét `on_door`-ral, rejtett `F3`-tipp, gomb a gyökérpanelen, `on_tick`-ből frissülő élő irányítópult-widget |

# Lásd még

- [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows) — `BBS::Window`, a window DSL, Info, Form, MasterDetail, Stream, dialógusok
- [Widgetek](https://wiki.teletypegames.org/hu/engines/rubbs/widgets) — a vezérlők a window-kban és az asztalon
- [Rajzolás](https://wiki.teletypegames.org/hu/engines/rubbs/drawing) — témák és stíluskulcsok, a FrameBuffer
- [Flow](https://wiki.teletypegames.org/hu/engines/rubbs/flow) — a soronkénti réteg, ami többnyire elindítja az applicationt
- [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors) — door-programok, FakeOS, gatewayek, ZMODEM
- [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store) — billentyű-szimbólumok, az egér, a sessionök

'; 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: [AI-karakterek](https://wiki.teletypegames.org/hu/engines/rubbs/ai.md)
- Next: [Rajzok és grafikus képernyők](https://wiki.teletypegames.org/hu/engines/rubbs/art.md)
