From 1ae4734e81a78d56deb008a1730d1627233eebec Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 13 May 2026 22:24:34 +0200 Subject: [PATCH] new window dsl --- Gemfile.lock | 2 +- bbs.rb | 244 ++++++++++++++++++++++++--- lib/ui/formatters.rb | 118 +++++++++++++ lib/ui/windows/bulletin_window.rb | 37 ---- lib/ui/windows/chat_window.rb | 23 --- lib/ui/windows/games_window.rb | 35 ---- lib/ui/windows/messages_window.rb | 74 -------- lib/ui/windows/profile_window.rb | 31 ---- lib/ui/windows/sysop_window.rb | 42 ----- lib/ui/windows/system_info_window.rb | 34 ---- lib/ui/windows/users_window.rb | 41 ----- lib/ui/windows/wiki_window.rb | 28 --- 12 files changed, 336 insertions(+), 373 deletions(-) create mode 100644 lib/ui/formatters.rb delete mode 100644 lib/ui/windows/bulletin_window.rb delete mode 100644 lib/ui/windows/chat_window.rb delete mode 100644 lib/ui/windows/games_window.rb delete mode 100644 lib/ui/windows/messages_window.rb delete mode 100644 lib/ui/windows/profile_window.rb delete mode 100644 lib/ui/windows/sysop_window.rb delete mode 100644 lib/ui/windows/system_info_window.rb delete mode 100644 lib/ui/windows/users_window.rb delete mode 100644 lib/ui/windows/wiki_window.rb diff --git a/Gemfile.lock b/Gemfile.lock index 6794f07..292b32e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://git.teletype.hu/tools/rubbs.git - revision: 5bf754f38cbd41c7e81a6990db8139ce170e0bc2 + revision: 18e77c14b5e0d48599bc18a1f55cf9c72b64fe47 specs: bbs (0.4.0) artii (~> 2.1) diff --git a/bbs.rb b/bbs.rb index a7e0755..9b7ee85 100644 --- a/bbs.rb +++ b/bbs.rb @@ -16,15 +16,7 @@ require_relative 'lib/service/wiki_service' require_relative 'lib/service/games_service' require_relative 'lib/service/chat_service' require_relative 'lib/service/last_callers_service' -require_relative 'lib/ui/windows/system_info_window' -require_relative 'lib/ui/windows/messages_window' -require_relative 'lib/ui/windows/wiki_window' -require_relative 'lib/ui/windows/games_window' -require_relative 'lib/ui/windows/users_window' -require_relative 'lib/ui/windows/chat_window' -require_relative 'lib/ui/windows/bulletin_window' -require_relative 'lib/ui/windows/profile_window' -require_relative 'lib/ui/windows/sysop_window' +require_relative 'lib/ui/formatters' ONLINE = OnlineService.new(OnlineUsersRepository.new) BOARDS = BoardService.new(BoardRepository.new( @@ -50,11 +42,12 @@ SERVICES = { chat: CHAT, last_callers: LAST_CALLERS, profile: PROFILE, - uptime: STARTED_AT, + uptime: STARTED_AT }.freeze MAIN_APP = BBS::Application.define do theme BBS::Theme.neumanntronics + services SERVICES helpers do def sysop? @@ -63,43 +56,236 @@ MAIN_APP = BBS::Application.define do end end + # ── Windows ──────────────────────────────────────────────────────────────── + + window :bulletin, type: :info do |w| + w.title ' Welcome ' + w.width 70 + w.height 17 + w.body do |ctx| + UI::Formatters::Bulletin.body( + username: ctx[:username] || 'Anonymous', + online_count: ctx[:online].count, + total_messages: ctx[:boards].total_count, + recent_callers: ctx[:last_callers].recent(3) + ) + end + w.button label: '&Continue', primary: true, cancel: true + end + + window :system_info, type: :info do |w| + w.title ' System Info ' + w.width 60 + w.height 14 + w.body do |ctx| + rows = [ + ['Online users', ctx[:online].count.to_s], + ['Messages', ctx[:boards].total_count.to_s], + ['Boards', ctx[:boards].boards.size.to_s], + ['Wiki', 'https://wiki.teletypegames.org'], + ['Games API', 'https://teletypegames.org'], + ['Platform', RUBY_PLATFORM], + ['Ruby', RUBY_VERSION], + ['Uptime', UI::Formatters::SystemInfo.format_uptime(ctx[:uptime])] + ] + [{ kv: rows, label_width: 15 }] + end + end + + window :messages, type: :master_detail do |w| + w.title ' Message Boards ' + w.list_width 20 + w.empty_message ' (no boards configured)' + w.items { |ctx| ctx[:boards].boards } + w.item_label { |board, _ctx| board.title } + w.render_detail do |board, width, ctx| + UI::Formatters::Board.lines(ctx[:boards].messages(board.id, 100), board.title, width) + end + w.action label: '&New', align: :left do |window, ctx| + board = window.list_widget.selected_item + ctx[:app].open_window(:compose_message, board: board, + on_posted: -> { window.reload_detail }) + :keep + end + w.action label: '&Refresh', align: :left do |window, _ctx| + window.reload_detail + :keep + end + end + + window :compose_message, type: :form do |w| + w.title { |ctx| ctx[:board] ? " Post to #{ctx[:board].title} " : ' Compose ' } + w.width { |ctx| [ctx[:app].cols - 8, 70].min } + w.height 14 + w.hint { |ctx| "From: #{ctx[:username] || 'Anonymous'} — Ctrl-Enter sends" } + w.submit_label '&Send' + w.cancel_label '&Cancel' + w.field :body, type: :textarea, max_length: 1000 + w.on_submit do |values, form, ctx| + if ctx[:board].nil? + form.error('Select a board first.') + :keep + else + case ctx[:boards].post(ctx[:board].id, ctx[:username] || 'Anonymous', values[:body]) + when :empty + form.error('Message cannot be empty.') + :keep + else + ctx[:on_posted]&.call + :close + end + end + end + end + + window :wiki, type: :master_detail do |w| + w.title { |ctx| " #{ctx[:title]} " } + w.list_width { |ctx| [32, ctx[:app].cols / 3].max } + w.empty_message ' (no pages found)' + w.close_button false + w.items { |ctx| ctx[:wiki].list(ctx[:category]) } + w.item_label { |page, _ctx| page.title } + w.render_detail do |page, width, ctx| + text = ctx[:wiki].content(page.id) + BBS::Markdown.to_lines(text, width: width - 1, theme: ctx[:app].theme) + end + w.bottom_meta do |page, ctx| + "#{page.created_at.to_s[0, 10]} #{ctx[:wiki].page_url(page.locale, page.path)}" + end + end + + window :games, type: :master_detail do |w| + w.title ' Game Catalog ' + w.list_width 30 + w.empty_message ' (no games)' + w.items { |ctx| ctx[:games].all } + w.item_label { |game, _ctx| game.title } + w.render_detail { |game, width, _ctx| UI::Formatters::Game.card(game, width) } + end + + window :online_users, type: :info do |w| + w.title ' Online Users ' + w.width 50 + w.scroll true + w.height { |ctx| [ctx[:online].user_list.size + 6, 10].max } + w.body { |ctx| UI::Formatters::Users.online_list(ctx[:online].user_list, ctx[:username]) } + end + + window :last_callers, type: :info do |w| + w.title ' Last Callers ' + w.width 64 + w.scroll true + w.height { |ctx| [ctx[:last_callers].recent(15).size + 6, 10].max } + w.body { |ctx| UI::Formatters::Users.last_callers_list(ctx[:last_callers].recent(15)) } + end + + window :chat, type: :stream do |w| + w.title ' Live Chat ' + w.placeholder 'Type a message and press Enter…' + w.initial_lines do |ctx| + ctx[:chat].history.map { |line| UI::Formatters::Chat.line(line) } + end + w.on_submit do |text, _app, ctx| + ctx[:chat].broadcast(ctx[:username] || 'Anonymous', text.strip) + end + w.on_poll do |app_, ctx| + ctx[:chat].drain(app_.session_id).map { |line| UI::Formatters::Chat.line(line) } + end + end + + window :profile, type: :form do |w| + w.title { |ctx| " Profile — #{ctx[:username] || 'Anonymous'} " } + w.width 64 + w.height 14 + w.prefill do |ctx| + existing = ctx[:profile].find(ctx[:username] || 'Anonymous') || {} + { + signature: existing['signature'].to_s, + location: existing['location'].to_s, + homepage: existing['homepage'].to_s, + notes: existing['notes'].to_s + } + end + w.field :signature, label: 'Signature' + w.field :location, label: 'Location' + w.field :homepage, label: 'Homepage' + w.field :notes, label: 'Notes' + w.on_submit do |values, _form, ctx| + ctx[:profile].update(ctx[:username] || 'Anonymous', **values) + BBS::Dialogs.message(ctx[:app], 'Profile saved.', title: ' Saved ') + :close + end + end + + window :sysop_console, type: :info do |w| + w.title ' Sysop Console ' + w.width 70 + w.height 18 + w.scroll true + w.body do |ctx| + UI::Formatters::Sysop.console_body( + username: ctx[:username], + online_count: ctx[:online].count, + total_messages: ctx[:boards].total_count, + chat_subscribers: ctx[:chat].subscriber_count, + online_users: ctx[:online].user_list, + recent_callers: ctx[:last_callers].recent(5) + ) + end + w.button label: '&Broadcast', align: :left do |_window, ctx| + BBS::Dialogs.input(ctx[:app], prompt: 'Sysop broadcast message:', + title: ' Broadcast ', width: 64, + on_submit: lambda do |text| + next if text.strip.empty? + ctx[:chat].broadcast("[SYSOP] #{ctx[:username]}", text.strip) + end) + :keep + end + w.button label: '&Close', cancel: true + end + + # ── Menubar ──────────────────────────────────────────────────────────────── + menubar do menu '&File' do - item '&Bulletin' do UI::Windows::Bulletin.open(self, services: SERVICES) end - item '&System Info' do UI::Windows::SystemInfo.open(self, services: SERVICES) end + item '&Bulletin' do open_window(:bulletin) end + item '&System Info' do open_window(:system_info) end separator item 'E&xit', shortcut: 'Alt-X' do :halt end end menu '&Messages' do - item '&Boards…', shortcut: 'F2' do UI::Windows::Messages.open(self, services: SERVICES) end + item '&Boards…', shortcut: 'F2' do open_window(:messages) end item '&New Post…', shortcut: 'F3' do board = SERVICES[:boards].boards.first - UI::Windows::ComposeMessage.open(self, services: SERVICES, board: board) + if board + open_window(:compose_message, board: board) + else + BBS::Dialogs.message(self, 'Select a board first.', title: ' Compose ') + end end end menu '&Files' do - item '&Blog Posts' do UI::Windows::Wiki.open(self, services: SERVICES, category: 'blog', title: 'Blog Posts') end - item '&HowTo Guides' do UI::Windows::Wiki.open(self, services: SERVICES, category: 'howto', title: 'HowTo Guides') end - item '&Game Catalog' do UI::Windows::Games.open(self, services: SERVICES) end + item '&Blog Posts' do open_window(:wiki, category: 'blog', title: 'Blog Posts') end + item '&HowTo Guides' do open_window(:wiki, category: 'howto', title: 'HowTo Guides') end + item '&Game Catalog' do open_window(:games) end end menu '&Users' do - item '&Online' do UI::Windows::OnlineUsers.open(self, services: SERVICES) end - item '&Last Callers' do UI::Windows::LastCallers.open(self, services: SERVICES) end - item '&Profile…' do UI::Windows::Profile.open(self, services: SERVICES) end + item '&Online' do open_window(:online_users) end + item '&Last Callers' do open_window(:last_callers) end + item '&Profile…' do open_window(:profile) end end menu '&Chat' do - item '&Open chat', shortcut: 'F4' do UI::Windows::Chat.open(self, services: SERVICES) end + item '&Open chat', shortcut: 'F4' do open_window(:chat) end end menu 'S&ysop' do item '&Console', shortcut: 'F10' do - name = (context[:username] || '').downcase - if SYSOPS.include?(name) - UI::Windows::SysopConsole.open(self, services: SERVICES) + if sysop? + open_window(:sysop_console) else BBS::Dialogs.message(self, 'Sysop access denied.', title: ' Sysop ') end @@ -123,12 +309,16 @@ MAIN_APP = BBS::Application.define do end status_hint('F1', 'Help') { BBS::Dialogs.message(self, 'Top menu: Alt+letter — Windows: Tab/Shift-Tab — Close: Esc') } - status_hint('F2', 'Boards') { UI::Windows::Messages.open(self, services: SERVICES) } + status_hint('F2', 'Boards') { open_window(:messages) } status_hint('F3', 'Post') { board = SERVICES[:boards].boards.first - UI::Windows::ComposeMessage.open(self, services: SERVICES, board: board) + if board + open_window(:compose_message, board: board) + else + BBS::Dialogs.message(self, 'Select a board first.', title: ' Compose ') + end } - status_hint('F4', 'Chat') { UI::Windows::Chat.open(self, services: SERVICES) } + status_hint('F4', 'Chat') { open_window(:chat) } status_hint('F10', 'Menu') { menubar.open(0) } status_hint('Alt-X', 'Exit') { :halt } diff --git a/lib/ui/formatters.rb b/lib/ui/formatters.rb new file mode 100644 index 0000000..c523260 --- /dev/null +++ b/lib/ui/formatters.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +# Pure view-layer helpers used by the window DSL in bbs.rb. No business +# logic here — each method takes data + width and returns lines/strings. +module UI + module Formatters + module Bulletin + BANNER = [ + " \e[1;33m _____ _ _ ____ ____ _____\e[0m", + " \e[1;33m|_ _|__ | | | |_ __ _ _ _ ___ | __ )| __ )/ ____|\e[0m", + " \e[1;33m | |/ _ \\ | | | __|/ _` | '_/ _ \\ | _ \\| _ \\\\___ \\\e[0m", + " \e[1;33m | | __/ | |___ | |_| (_| | || __/ | |_) | |_) |___) |\e[0m", + " \e[1;33m |_|\\___| |_____|\\__|\\__,_|_| \\___| |____/|____/|____/\e[0m" + ].freeze + + def self.body(username:, online_count:, total_messages:, recent_callers:) + callers = recent_callers.map do |c| + " · #{c.username} \e[2;37m#{c.timestamp[0, 16].sub('T', ' ')}\e[0m" + end + [ + *BANNER.map { |l| { text: l, style: :accent } }, + :spacer, + "Logged in as \e[1;33m#{username}\e[0m", + :spacer, + "Online now: #{online_count}", + "Total messages: #{total_messages}", + :spacer, + 'Last callers:', + *callers + ] + end + end + + module SystemInfo + def self.format_uptime(start) + return '–' unless start + seconds = (Time.now - start).to_i + h = seconds / 3600 + m = (seconds % 3600) / 60 + s = seconds % 60 + "#{h}h #{m}m #{s}s" + end + end + + module Board + def self.lines(messages, board_title, width) + return [" (no messages on #{board_title} yet — press 'n' to start one)"] if messages.empty? + lines = [] + messages.each do |m| + head = "\e[2;37m#{m.timestamp}\e[0m \e[1;33m#{m.username}\e[0m: " + BBS::FrameBuffer.wrap_ansi(head + m.text.to_s, width - 1).each { |l| lines << l } + lines << '' + end + lines + end + end + + module Game + def self.card(game, width) + lines = [] + lines << " \e[1;33m#{game.title}\e[0m" + lines << " \e[0;36m#{game.platform} · #{game.author}\e[0m" + lines << '' + BBS::FrameBuffer.wrap_ansi(game.desc.to_s, width - 4).each { |l| lines << " #{l}" } + lines << '' + BBS::FrameBuffer.wrap_ansi(game.story.to_s, width - 4).each { |l| lines << " #{l}" } + lines << '' + game.external_links.each do |link| + lines << " \e[0;37m#{link[:label].ljust(10)}\e[0m \e[1;36m#{link[:url]}\e[0m" + end + lines + end + end + + module Chat + def self.line(entry) + " \e[2;37m#{entry.timestamp}\e[0m \e[1;33m#{entry.username}\e[0m: #{entry.text}" + end + end + + module Users + def self.online_list(usernames, current_user) + return [' (no one else here yet)'] if usernames.empty? + usernames.map { |u| u == current_user ? " #{u} ← you" : " #{u}" } + end + + def self.last_callers_list(callers) + return [' (no callers recorded yet)'] if callers.empty? + callers.map do |c| + ts = c.timestamp.to_s[0, 19].sub('T', ' ') + " \e[2;37m#{ts}\e[0m \e[1;33m#{c.username}\e[0m" + end + end + end + + module Sysop + def self.console_body(username:, online_count:, total_messages:, + chat_subscribers:, online_users:, recent_callers:) + body = [] + body << " Sysop: \e[1;33m#{username}\e[0m" + body << :spacer + body << ' Stats' + body << " · Online users: #{online_count}" + body << " · Total messages: #{total_messages}" + body << " · Chat subscribers: #{chat_subscribers}" + body << :spacer + body << ' Online users:' + online_users.each { |u| body << " · #{u}" } + body << :spacer + body << ' Last 5 callers:' + recent_callers.each do |c| + body << " · #{c.username} #{c.timestamp[0, 16].sub('T', ' ')}" + end + body + end + end + end +end diff --git a/lib/ui/windows/bulletin_window.rb b/lib/ui/windows/bulletin_window.rb deleted file mode 100644 index 9cbb641..0000000 --- a/lib/ui/windows/bulletin_window.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - # Login splash: ASCII banner + last callers + new message counts. Closed - # with Enter / Esc / clicking the OK button. - class Bulletin - def self.open(app, services:) - banner_lines = [ - " \e[1;33m _____ _ _ ____ ____ _____\e[0m", - " \e[1;33m|_ _|__ | | | |_ __ _ _ _ ___ | __ )| __ )/ ____|\e[0m", - " \e[1;33m | |/ _ \\ | | | __|/ _` | '_/ _ \\ | _ \\| _ \\\\___ \\\e[0m", - " \e[1;33m | | __/ | |___ | |_| (_| | || __/ | |_) | |_) |___) |\e[0m", - " \e[1;33m |_|\\___| |_____|\\__|\\__,_|_| \\___| |____/|____/|____/\e[0m", - ] - caller_lines = services[:last_callers].recent(3).map do |c| - " · #{c.username} \e[2;37m#{c.timestamp[0, 16].sub('T', ' ')}\e[0m" - end - BBS::Windows::Info.open(app, - title: ' Welcome ', width: 70, height: 17, - body: [ - *banner_lines.map { |l| { text: l, style: :accent } }, - :spacer, - "Logged in as \e[1;33m#{app.context[:username] || 'Anonymous'}\e[0m", - :spacer, - "Online now: #{services[:online].count}", - "Total messages: #{services[:boards].total_count}", - :spacer, - 'Last callers:', - *caller_lines, - ], - buttons: [{ label: '&Continue', primary: true, cancel: true }] - ) - end - end - end -end diff --git a/lib/ui/windows/chat_window.rb b/lib/ui/windows/chat_window.rb deleted file mode 100644 index 7a81355..0000000 --- a/lib/ui/windows/chat_window.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - class Chat - def self.open(app, services:) - username = app.context[:username] || 'Anonymous' - BBS::Windows::Stream.open(app, - title: ' Live Chat ', - initial_lines: services[:chat].history.map { |l| format_chat_line(l) }, - placeholder: 'Type a message and press Enter…', - on_submit: ->(text, _a) { services[:chat].broadcast(username, text.strip) }, - on_poll: ->(a) { services[:chat].drain(a.session_id).map { |l| format_chat_line(l) } } - ) - end - - def self.format_chat_line(line) - " \e[2;37m#{line.timestamp}\e[0m \e[1;33m#{line.username}\e[0m: #{line.text}" - end - private_class_method :format_chat_line - end - end -end diff --git a/lib/ui/windows/games_window.rb b/lib/ui/windows/games_window.rb deleted file mode 100644 index d8105c1..0000000 --- a/lib/ui/windows/games_window.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - # Game catalog browser. Left=titles, right=card with story + links. - class Games - def self.open(app, services:) - BBS::Windows::MasterDetail.open(app, - title: ' Game Catalog ', - items: services[:games].all, - item_label: ->(g) { g.title }, - list_width: 30, - empty_message: ' (no games)', - render_detail: ->(game, width) { game_card_lines(game, width) } - ) - end - - def self.game_card_lines(game, width) - lines = [] - lines << " \e[1;33m#{game.title}\e[0m" - lines << " \e[0;36m#{game.platform} · #{game.author}\e[0m" - lines << '' - BBS::FrameBuffer.wrap_ansi(game.desc.to_s, width - 4).each { |l| lines << " #{l}" } - lines << '' - BBS::FrameBuffer.wrap_ansi(game.story.to_s, width - 4).each { |l| lines << " #{l}" } - lines << '' - game.external_links.each do |link| - lines << " \e[0;37m#{link[:label].ljust(10)}\e[0m \e[1;36m#{link[:url]}\e[0m" - end - lines - end - private_class_method :game_card_lines - end - end -end diff --git a/lib/ui/windows/messages_window.rb b/lib/ui/windows/messages_window.rb deleted file mode 100644 index 8e9d2e8..0000000 --- a/lib/ui/windows/messages_window.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - # Two-pane message reader: left=board list, right=messages of the selected - # board. Press &New to compose, &Refresh to reload. - class Messages - def self.open(app, services:) - BBS::Windows::MasterDetail.open(app, - title: ' Message Boards ', - items: services[:boards].boards, - item_label: ->(b) { b.title }, - list_width: 20, - empty_message: ' (no boards configured)', - render_detail: ->(board, width) { board_lines(services, board, width) }, - actions: [ - { label: '&New', align: :left, on_click: ->(window) { - board = window.list_widget.selected_item - ComposeMessage.open(app, services: services, board: board, - on_posted: -> { window.reload_detail }) - :keep - } }, - { label: '&Refresh', align: :left, on_click: ->(window) { - window.reload_detail - :keep - } } - ] - ) - end - - def self.board_lines(services, board, width) - msgs = services[:boards].messages(board.id, 100) - return [" (no messages on #{board.title} yet — press 'n' to start one)"] if msgs.empty? - lines = [] - msgs.each do |m| - head = "\e[2;37m#{m.timestamp}\e[0m \e[1;33m#{m.username}\e[0m: " - BBS::FrameBuffer.wrap_ansi(head + m.text.to_s, width - 1).each { |l| lines << l } - lines << '' - end - lines - end - private_class_method :board_lines - end - - class ComposeMessage - def self.open(app, services:, board:, on_posted: nil) - return BBS::Dialogs.message(app, 'Select a board first.', title: ' Compose ') unless board - - username = app.context[:username] || 'Anonymous' - width = [app.cols - 8, 70].min - BBS::Windows::Form.open(app, - title: " Post to #{board.title} ", - width: width, height: 14, - hint: "From: #{username} — Ctrl-Enter sends", - submit_label: '&Send', - cancel_label: '&Cancel', - fields: [ - { key: :body, type: :textarea, value: '', max_length: 1000 } - ], - on_submit: ->(values, form) { - result = services[:boards].post(board.id, username, values[:body]) - if result == :empty - form.error('Message cannot be empty.') - :keep - else - on_posted&.call - :close - end - } - ) - end - end - end -end diff --git a/lib/ui/windows/profile_window.rb b/lib/ui/windows/profile_window.rb deleted file mode 100644 index 7127561..0000000 --- a/lib/ui/windows/profile_window.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - class Profile - def self.open(app, services:) - username = app.context[:username] || 'Anonymous' - existing = services[:profile].find(username) || {} - BBS::Windows::Form.open(app, - title: " Profile — #{username} ", - width: 64, height: 14, - fields: [ - { key: :signature, label: 'Signature', value: existing['signature'].to_s }, - { key: :location, label: 'Location', value: existing['location'].to_s }, - { key: :homepage, label: 'Homepage', value: existing['homepage'].to_s }, - { key: :notes, label: 'Notes', value: existing['notes'].to_s } - ], - on_submit: ->(values, _form) { - services[:profile].update(username, - signature: values[:signature], - location: values[:location], - homepage: values[:homepage], - notes: values[:notes] - ) - BBS::Dialogs.message(app, 'Profile saved.', title: ' Saved ') - } - ) - end - end - end -end diff --git a/lib/ui/windows/sysop_window.rb b/lib/ui/windows/sysop_window.rb deleted file mode 100644 index d82014f..0000000 --- a/lib/ui/windows/sysop_window.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - class SysopConsole - def self.open(app, services:) - body = [] - body << " Sysop: \e[1;33m#{app.context[:username]}\e[0m" - body << :spacer - body << ' Stats' - body << " · Online users: #{services[:online].count}" - body << " · Total messages: #{services[:boards].total_count}" - body << " · Chat subscribers: #{services[:chat].subscriber_count}" - body << :spacer - body << ' Online users:' - services[:online].user_list.each { |u| body << " · #{u}" } - body << :spacer - body << ' Last 5 callers:' - services[:last_callers].recent(5).each do |c| - body << " · #{c.username} #{c.timestamp[0, 16].sub('T', ' ')}" - end - - BBS::Windows::Info.open(app, - title: ' Sysop Console ', width: 70, height: 18, scroll: true, - body: body, - buttons: [ - { label: '&Broadcast', align: :left, on_click: ->(_window) { - BBS::Dialogs.input(app, prompt: 'Sysop broadcast message:', - title: ' Broadcast ', width: 64, - on_submit: lambda do |text| - next if text.strip.empty? - services[:chat].broadcast("[SYSOP] #{app.context[:username]}", text.strip) - end) - :keep - } }, - { label: '&Close', cancel: true } - ] - ) - end - end - end -end diff --git a/lib/ui/windows/system_info_window.rb b/lib/ui/windows/system_info_window.rb deleted file mode 100644 index f29f8bb..0000000 --- a/lib/ui/windows/system_info_window.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - class SystemInfo - def self.open(app, services:) - rows = [ - ['Online users', services[:online].count.to_s], - ['Messages', services[:boards].total_count.to_s], - ['Boards', services[:boards].boards.size.to_s], - ['Wiki', 'https://wiki.teletypegames.org'], - ['Games API', 'https://teletypegames.org'], - ['Platform', RUBY_PLATFORM], - ['Ruby', RUBY_VERSION], - ['Uptime', format_uptime(services[:uptime])], - ] - BBS::Windows::Info.open(app, - title: ' System Info ', width: 60, height: 14, - body: [{ kv: rows, label_width: 15 }] - ) - end - - def self.format_uptime(start) - return '–' unless start - seconds = (Time.now - start).to_i - h = seconds / 3600 - m = (seconds % 3600) / 60 - s = seconds % 60 - "#{h}h #{m}m #{s}s" - end - private_class_method :format_uptime - end - end -end diff --git a/lib/ui/windows/users_window.rb b/lib/ui/windows/users_window.rb deleted file mode 100644 index bb10460..0000000 --- a/lib/ui/windows/users_window.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - class OnlineUsers - def self.open(app, services:) - users = services[:online].user_list - username = app.context[:username] - body = if users.empty? - [' (no one else here yet)'] - else - users.map { |u| u == username ? " #{u} ← you" : " #{u}" } - end - BBS::Windows::Info.open(app, - title: ' Online Users ', - width: 50, height: [users.size + 6, 10].max, - scroll: true, body: body - ) - end - end - - class LastCallers - def self.open(app, services:) - callers = services[:last_callers].recent(15) - body = if callers.empty? - [' (no callers recorded yet)'] - else - callers.map do |c| - ts = c.timestamp.to_s[0, 19].sub('T', ' ') - " \e[2;37m#{ts}\e[0m \e[1;33m#{c.username}\e[0m" - end - end - BBS::Windows::Info.open(app, - title: ' Last Callers ', - width: 64, height: [callers.size + 6, 10].max, - scroll: true, body: body - ) - end - end - end -end diff --git a/lib/ui/windows/wiki_window.rb b/lib/ui/windows/wiki_window.rb deleted file mode 100644 index 587d8d6..0000000 --- a/lib/ui/windows/wiki_window.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module UI - module Windows - # Split-view wiki browser. Left=titles, right=selected page content - # (rendered with markdown-aware formatting). - class Wiki - def self.open(app, services:, category:, title:) - items = services[:wiki].list(category) - BBS::Windows::MasterDetail.open(app, - title: " #{title} ", - items: items, - item_label: ->(p) { p.title }, - list_width: [32, app.cols / 3].max, - empty_message: ' (no pages found)', - close_button: false, - render_detail: ->(page, width) { - text = services[:wiki].content(page.id) - BBS::Markdown.to_lines(text, width: width - 1, theme: app.theme) - }, - bottom_meta: ->(page) { - "#{page.created_at.to_s[0, 10]} #{services[:wiki].page_url(page.locale, page.path)}" - } - ) - end - end - end -end