new framework

This commit is contained in:
2026-05-12 22:36:19 +02:00
parent d92768dd23
commit 9d52a2e2c6
29 changed files with 1240 additions and 400 deletions

View File

@@ -0,0 +1,158 @@
# frozen_string_literal: true
module UI
module Windows
module_function
# Two-pane message reader: left=board list, right=messages of the selected
# board. Press "n" or click [+ New] to compose, "r" to refresh.
def messages(app, services:)
cols = app.cols
rows = app.rows
w = cols - 4
h = rows - 6
window = BBS::Window.new(title: ' Message Boards ')
window.layout(3, 3, w, h)
window.theme = app.theme
boards = services[:boards].boards
board_list = BBS::Widgets::ListBox.new(
items: boards,
label: ->(b) { " #{b.title}".ljust(18) }
)
board_list.layout(window.bounds.x + 2, window.bounds.y + 2, 20, h - 5)
window.add(board_list)
msg_lines = BBS::Widgets::ScrollView.new(lines: [])
msg_lines.layout(window.bounds.x + 23, window.bounds.y + 2, w - 26, h - 5)
window.add(msg_lines)
reload = lambda do
board = board_list.selected_item
next unless board
msgs = services[:boards].messages(board.id, 100)
wrap_width = msg_lines.bounds.width - 1
lines = []
if msgs.empty?
lines << " (no messages on #{board.title} yet — press 'n' to start one)"
else
msgs.each do |m|
head = "\e[2;37m#{m.timestamp}\e[0m \e[1;33m#{m.username}\e[0m: "
body = m.text.to_s
wrap_text(head + body, wrap_width).each { |l| lines << l }
lines << ''
end
end
msg_lines.lines = lines
msg_lines.scroll = [lines.size - msg_lines.bounds.height, 0].max
end
board_list.on_select = ->(_, _) { reload.call }
compose = BBS::Widgets::Button.new(
label: '&New',
on_click: ->(_) { compose_message(app, services: services, board: board_list.selected_item, on_posted: reload) }
)
compose.layout(window.bounds.x + 2, window.bounds.y + h - 2, 9, 1)
window.add(compose)
refresh = BBS::Widgets::Button.new(
label: '&Refresh',
on_click: ->(_) { reload.call }
)
refresh.layout(window.bounds.x + 13, window.bounds.y + h - 2, 13, 1)
window.add(refresh)
close = BBS::Widgets::Button.new(
label: '&Close',
on_click: ->(_) { app.close_window(window) }
)
close.layout(window.bounds.x + w - 11, window.bounds.y + h - 2, 9, 1)
window.add(close)
reload.call
window.reset_focus
window.focus_manager.focus(board_list)
app.open_window(window)
window
end
def compose_message(app, services:, board:, on_posted: nil)
return BBS::Dialogs.message(app, 'Select a board first.', title: ' Compose ') unless board
w = [app.cols - 8, 70].min
h = 14
window = BBS::Dialogs.centred_window(app, width: w, height: h,
title: " Post to #{board.title} ")
hint = BBS::Widgets::Label.new(
text: "From: #{app.context[:username] || 'Anonymous'} — Ctrl-Enter sends",
style_key: :input_label
)
hint.layout(window.bounds.x + 2, window.bounds.y + 1, w - 4, 1)
window.add(hint)
area = BBS::Widgets::TextArea.new(max_length: 1000)
area.layout(window.bounds.x + 2, window.bounds.y + 3, w - 4, h - 6)
window.add(area)
status = BBS::Widgets::Label.new(text: '', style_key: :error)
status.layout(window.bounds.x + 2, window.bounds.y + h - 3, w - 4, 1)
window.add(status)
send_btn = BBS::Widgets::Button.new(
label: '&Send',
on_click: lambda do |_|
result = services[:boards].post(board.id, app.context[:username] || 'Anonymous', area.value)
if result == :empty
status.text = 'Message cannot be empty.'
else
app.close_window(window)
on_posted&.call
end
end
)
send_btn.layout(window.bounds.x + w - 22, window.bounds.y + h - 2, 9, 1)
window.add(send_btn)
cancel = BBS::Widgets::Button.new(
label: '&Cancel',
on_click: ->(_) { app.close_window(window) }
)
cancel.layout(window.bounds.x + w - 12, window.bounds.y + h - 2, 10, 1)
window.add(cancel)
window.reset_focus
window.focus_manager.focus(area)
app.open_window(window)
window
end
def wrap_text(text, width)
return [''] if width <= 1
# Treat the text as containing ANSI; wrap on visible width approximately
# by splitting on whitespace.
visible = text.gsub(/\e\[[\d;]*m/, '')
return [text] if visible.length <= width
lines = []
cur = +''
cur_visible = +''
text.scan(/(\e\[[\d;]*m|\S+\s*|\s+)/) do |(token)|
if token.start_with?("\e")
cur << token
next
end
visible_tok = token
if cur_visible.length + visible_tok.length > width && !cur_visible.empty?
lines << cur
cur = +''
cur_visible = +''
end
cur << token
cur_visible << token
end
lines << cur unless cur.empty?
lines
end
end
end