From 4eca9513a8a7654b9a2862c3f8a70a21ccf41bd6 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Mon, 11 May 2026 22:17:31 +0200 Subject: [PATCH] Add nav DSL to TUI::Page; helpers accumulates across calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- bbs.gemspec | 2 +- lib/bbs/tui.rb | 45 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/bbs.gemspec b/bbs.gemspec index a580b02..1540ad8 100644 --- a/bbs.gemspec +++ b/bbs.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'bbs' - s.version = '0.2.0' + s.version = '0.3.0' s.summary = 'Universal telnet BBS server library' s.author = 'Zsolt Tasnadi' s.files = Dir['lib/**/*.rb'] diff --git a/lib/bbs/tui.rb b/lib/bbs/tui.rb index a8e9e04..1cbd64a 100644 --- a/lib/bbs/tui.rb +++ b/lib/bbs/tui.rb @@ -11,8 +11,9 @@ module BBS end def initialize - @pages = {} - @start_page = :idle + @pages = {} + @start_page = :idle + @helper_module = Module.new end def init(&block) = @init_block = block @@ -20,7 +21,7 @@ module BBS def start(name) = @start_page = name def helpers(&block) - @helper_module = Module.new(&block) + @helper_module.module_eval(&block) end def page(name, &block) @@ -41,6 +42,44 @@ module BBS def render(&block) = @render_block = block def key(k, &block) = (@key_bindings[k] = 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