A
BBS::Applicationa 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 azApplication.defineDSL 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, a widgetek a Widgetek oldalon vannak.
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 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.
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:
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:
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.NoteStore.new egyszer fut le, így minden hívó ugyanazokat a jegyzeteket látja. Ezért van a store-ban mutex.context-be kerül (itt a :username, amit a flow ask-ja tölt ki).ctx hasht kapnak; a ctx[:app] a futó application, így a helperek ctx[:app].me alakban érhetők el.call lépéséből (a szokásos mód)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é.
app lépésévelrequire '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.
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):
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 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 |
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(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 |
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 oldalon vannak.
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]).
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 { 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.
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(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 oldalon, a window DSL résznél van leírva.
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(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 oldalon vannak.
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 { 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.
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(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 |
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 { 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.
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 { ... }
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.
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 { ... }
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.
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 { ... }
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.
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 { |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.
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
| 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.
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.
| 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 |
| 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 |
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
| 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 |
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 oldalon van.
| 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 |
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) és a teljes képernyős ANSI-art nézegetőkhöz.
| 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:
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.
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:
Session#readkey(timeout: 1.0)).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.BBS::Event-be csomagolja és kézbesíti. Ha a kézbesítés :halt-ot ad, a ciklus véget ér.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.
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:
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).
A Builder#run az Application#run köré épített kör, ami átviszi a hívót a doorokon:
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 egy sima Hash, ami a hívásé, nem az applicationé.
ctx-e: :session_id, minden ask-válasz (:username), és amit a korábbi lépések beleírtak.BBS::Flow-door a másolatát kapja (lásd Doorok), egy call(session, context)-door magát a hasht.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:
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.
A BBS::Event (lib/bbs/event.rb) az egységesített bemenet, amit minden widget és window kap:
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') |
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:
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 oldalon van; widgetet írni a Widgetek oldal tanít.
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:
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 BBS::Menubar (lib/bbs/menu.rb) az 1. sor sávja. A DSL építi fel, de kézzel is elkészíthető.
&File → File).Menubar#handle_event-en megy át; a belső billentyűkezelés privát.:halt-ot ad, az application véget ér.| 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.
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 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.
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
| 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.
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).
| 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 |
BBS::Window, a window DSL, Info, Form, MasterDetail, Stream, dialógusok