Add nav DSL to TUI::Page; helpers accumulates across calls

Page#nav sets up up/down key handlers for four common patterns:
  nav :menu,   size:    – wrapping @menu_sel selection
  nav :cycle           – wrapping @item_sel over @items
  nav :list,   window: – @item_sel/@scroll with a visible window
  nav :scroll, content:, window: – @scroll over arbitrary content

size/window accept an integer or a lambda evaluated on the Context
at runtime, so terminal-size-dependent values work naturally.

TUI#helpers now accumulates into one module instead of replacing it,
so configure methods on page classes can each extend helpers without
clobbering previous calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:17:31 +02:00
parent 148fd88660
commit 4eca9513a8
2 changed files with 43 additions and 4 deletions

View File

@@ -1,6 +1,6 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = 'bbs' s.name = 'bbs'
s.version = '0.2.0' s.version = '0.3.0'
s.summary = 'Universal telnet BBS server library' s.summary = 'Universal telnet BBS server library'
s.author = 'Zsolt Tasnadi' s.author = 'Zsolt Tasnadi'
s.files = Dir['lib/**/*.rb'] s.files = Dir['lib/**/*.rb']

View File

@@ -13,6 +13,7 @@ module BBS
def initialize def initialize
@pages = {} @pages = {}
@start_page = :idle @start_page = :idle
@helper_module = Module.new
end end
def init(&block) = @init_block = block def init(&block) = @init_block = block
@@ -20,7 +21,7 @@ module BBS
def start(name) = @start_page = name def start(name) = @start_page = name
def helpers(&block) def helpers(&block)
@helper_module = Module.new(&block) @helper_module.module_eval(&block)
end end
def page(name, &block) def page(name, &block)
@@ -41,6 +42,44 @@ module BBS
def render(&block) = @render_block = block def render(&block) = @render_block = block
def key(k, &block) = (@key_bindings[k] = block) def key(k, &block) = (@key_bindings[k] = block)
def printable(&block) = (@key_bindings[:printable] = block) def printable(&block) = (@key_bindings[:printable] = block)
def nav(mode, **opts)
lift = ->(v) { v.is_a?(Proc) ? v : -> { v } }
case mode
when :menu
size = lift.(opts.fetch(:size))
key(:up) { @menu_sel = (@menu_sel - 1) % instance_exec(&size) }
key(:down) { @menu_sel = (@menu_sel + 1) % instance_exec(&size) }
when :cycle
key(:up) { @item_sel = (@item_sel - 1) % @items.size if @items.any? }
key(:down) { @item_sel = (@item_sel + 1) % @items.size if @items.any? }
when :list
window = lift.(opts.fetch(:window))
key(:up) do
return if @item_sel <= 0
@item_sel -= 1
@scroll = @item_sel if @item_sel < @scroll
end
key(:down) do
return if @item_sel >= @items.size - 1
@item_sel += 1
wh = instance_exec(&window)
@scroll = @item_sel - wh + 1 if @item_sel >= @scroll + wh
end
when :scroll
content = opts.fetch(:content)
window = lift.(opts.fetch(:window))
key(:up) { @scroll = [@scroll - 1, 0].max }
key(:down) do
max = [instance_exec(&content).size - instance_exec(&window), 0].max
@scroll = [@scroll + 1, max].min
end
end
end
end end
end end
end end