Redesign TUI with DOS Navigator look and two-panel layout
Full blue background (screen_fill), double-line bordered panels side by side: left panel for the menu, right panel for content. The right panel's title bar shows the current page name, username and online count. Color theme (THEME constant) maps to combined fg+bg ANSI codes so all elements look native on the blue background: bright cyan borders, white normal text, yellow labels, black-on-cyan selection highlights. Status bar at the bottom mimics the NC function key bar with bright key labels on cyan background. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
GIT
|
GIT
|
||||||
remote: https://git.teletype.hu/tools/rubbs.git
|
remote: https://git.teletype.hu/tools/rubbs.git
|
||||||
revision: cac10176a25eb5d917f559d4dfb6c7d9c0dc34da
|
revision: 148fd886600c814e3bdb13ec3b3fb3f812d99cb9
|
||||||
specs:
|
specs:
|
||||||
bbs (0.2.0)
|
bbs (0.2.0)
|
||||||
artii (~> 2.1)
|
artii (~> 2.1)
|
||||||
|
|||||||
167
bbs.rb
167
bbs.rb
@@ -7,8 +7,6 @@ require_relative 'lib/repository/message_board_repository'
|
|||||||
require_relative 'lib/repository/wiki_repository'
|
require_relative 'lib/repository/wiki_repository'
|
||||||
require_relative 'lib/repository/catalog_repository'
|
require_relative 'lib/repository/catalog_repository'
|
||||||
|
|
||||||
include BBS::Color
|
|
||||||
|
|
||||||
ONLINE = OnlineUsersRepository.new
|
ONLINE = OnlineUsersRepository.new
|
||||||
MESSAGES = MessageBoardRepository.new(ENV.fetch('MESSAGES_PATH', 'data/messages.dat'))
|
MESSAGES = MessageBoardRepository.new(ENV.fetch('MESSAGES_PATH', 'data/messages.dat'))
|
||||||
WIKI = WikiRepository.new(token: ENV['WEBAPP_WIKIJS_TOKEN'])
|
WIKI = WikiRepository.new(token: ENV['WEBAPP_WIKIJS_TOKEN'])
|
||||||
@@ -20,14 +18,37 @@ MENU_ITEMS = [
|
|||||||
'System Info', 'Exit'
|
'System Info', 'Exit'
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
SEP_X = 21
|
# ── Layout ──────────────────────────────────────────────────────────────────
|
||||||
CONT_X = 23
|
|
||||||
CONT_Y = 3
|
LEFT_W = 21 # left panel outer width
|
||||||
|
CONT_X = 23 # content start x inside right panel (LEFT_W + 2)
|
||||||
|
CONT_Y = 2 # content start y inside panels (top border + 1)
|
||||||
|
|
||||||
|
# ── DOS Navigator color theme ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
THEME = {
|
||||||
|
bg: "\e[0;44m", # blue background
|
||||||
|
normal: "\e[0;37;44m", # white on blue
|
||||||
|
bright: "\e[1;37;44m", # bright white on blue
|
||||||
|
border: "\e[1;36;44m", # bright cyan on blue
|
||||||
|
selected: "\e[0;30;46m", # black on cyan (highlighted menu item)
|
||||||
|
header: "\e[1;36;44m", # bright cyan on blue (section titles)
|
||||||
|
label: "\e[1;33;44m", # yellow on blue (timestamps, labels)
|
||||||
|
status: "\e[0;30;46m", # black on cyan (bottom status bar)
|
||||||
|
input: "\e[1;33;44m", # yellow on blue (text input)
|
||||||
|
success: "\e[1;32;44m", # green on blue
|
||||||
|
error: "\e[1;31;44m", # red on blue
|
||||||
|
reset: "\e[0m",
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# ── TUI definition ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
MAIN_TUI = BBS::TUI.define do
|
MAIN_TUI = BBS::TUI.define do
|
||||||
helpers do
|
helpers do
|
||||||
|
def tc(style, str) = "#{THEME.fetch(style, THEME[:normal])}#{str}#{THEME[:reset]}"
|
||||||
|
|
||||||
def cont_w = term_cols - CONT_X - 1
|
def cont_w = term_cols - CONT_X - 1
|
||||||
def cont_h = term_rows - CONT_Y - 4
|
def cont_h = term_rows - 4
|
||||||
|
|
||||||
def fmt_date(iso)
|
def fmt_date(iso)
|
||||||
Time.parse(iso.to_s).strftime('%Y-%m-%d')
|
Time.parse(iso.to_s).strftime('%Y-%m-%d')
|
||||||
@@ -53,22 +74,23 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
lines
|
lines
|
||||||
end
|
end
|
||||||
|
|
||||||
def wiki_list_render(color:, title:)
|
def wiki_list_render(title)
|
||||||
text c(color, title), x: CONT_X, y: CONT_Y
|
text title, x: CONT_X, y: CONT_Y, style: THEME[:header]
|
||||||
if @items.empty?
|
if @items.empty?
|
||||||
text c(:gray, 'No items found.'), x: CONT_X, y: CONT_Y + 2
|
text 'No items found.', x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
list_h = cont_h - 5
|
list_h = cont_h - 5
|
||||||
visible = @items[@scroll, list_h] || []
|
visible = @items[@scroll, list_h] || []
|
||||||
list visible.map { |p| p.title[0, cont_w - 2] },
|
list visible.map { |p| p.title[0, cont_w - 2] },
|
||||||
x: CONT_X, y: CONT_Y + 2,
|
x: CONT_X, y: CONT_Y + 2,
|
||||||
selected: @item_sel - @scroll, style: :gray, highlight: :white
|
selected: @item_sel - @scroll,
|
||||||
|
style: THEME[:normal], highlight: THEME[:selected]
|
||||||
if (page = @items[@item_sel])
|
if (page = @items[@item_sel])
|
||||||
hint = page.description.to_s.strip[0, cont_w]
|
hint = page.description.to_s.strip[0, cont_w]
|
||||||
text c(:gray, hint), x: CONT_X, y: CONT_Y + cont_h - 2 unless hint.empty?
|
text hint, x: CONT_X, y: CONT_Y + cont_h - 2, style: THEME[:normal] unless hint.empty?
|
||||||
end
|
end
|
||||||
text c(:gray, '↑↓ navigate Enter read q back'), x: CONT_X, y: CONT_Y + cont_h
|
text '↑↓ navigate Enter read q back', x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
|
|
||||||
def scroll_list_up
|
def scroll_list_up
|
||||||
@@ -97,6 +119,7 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
|
|
||||||
init do
|
init do
|
||||||
@menu_sel = 0
|
@menu_sel = 0
|
||||||
|
@page_title = 'TELETYPE BBS'
|
||||||
@items = []
|
@items = []
|
||||||
@item_sel = 0
|
@item_sel = 0
|
||||||
@scroll = 0
|
@scroll = 0
|
||||||
@@ -109,23 +132,36 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
chrome do
|
chrome do
|
||||||
bar y: 1,
|
screen_fill bg: :blue
|
||||||
content: " TELETYPE BBS │ #{@username} │ #{ONLINE.count} online",
|
|
||||||
style: :success
|
panel_h = term_rows - 1
|
||||||
list MENU_ITEMS.map { |i| i[0, 15] }, x: 2, y: CONT_Y,
|
|
||||||
selected: @menu_sel, style: :gray, highlight: :white
|
box x: 1, y: 1, w: LEFT_W, h: panel_h,
|
||||||
(CONT_Y..CONT_Y + cont_h).each { |r| text c(:gray, '│'), x: SEP_X, y: r }
|
style: THEME[:border], bg: :blue
|
||||||
|
|
||||||
|
box x: LEFT_W + 1, y: 1, w: term_cols - LEFT_W, h: panel_h,
|
||||||
|
title: " #{@page_title} │ #{@username} │ #{ONLINE.count} online ",
|
||||||
|
style: THEME[:border], bg: :blue
|
||||||
|
|
||||||
|
list MENU_ITEMS.map { |i| i[0, LEFT_W - 4] },
|
||||||
|
x: 2, y: CONT_Y,
|
||||||
|
selected: @menu_sel,
|
||||||
|
style: THEME[:normal], highlight: THEME[:selected]
|
||||||
|
|
||||||
bar y: term_rows,
|
bar y: term_rows,
|
||||||
content: ' ↑↓ Navigate Enter Select q Back/Quit r Refresh',
|
content: " \e[1;37;46m↑↓\e[0;30;46m Navigate \e[1;37;46mEnter\e[0;30;46m Select " \
|
||||||
style: :muted
|
"\e[1;37;46mQ\e[0;30;46m Back \e[1;37;46mR\e[0;30;46m Refresh",
|
||||||
|
style: THEME[:status]
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── Pages ───────────────────────────────────────────────────────────────────
|
# ── Pages ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
page :idle do
|
page :idle do
|
||||||
|
enter { @page_title = 'TELETYPE BBS' }
|
||||||
|
|
||||||
render do
|
render do
|
||||||
text c(:gray, 'Navigate with ↑↓, select with Enter'),
|
text 'Navigate the menu with ↑↓, select with Enter.',
|
||||||
x: CONT_X, y: CONT_Y + cont_h / 2
|
x: CONT_X, y: CONT_Y + cont_h / 2, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
|
|
||||||
key(:up) { @menu_sel = (@menu_sel - 1) % MENU_ITEMS.size }
|
key(:up) { @menu_sel = (@menu_sel - 1) % MENU_ITEMS.size }
|
||||||
@@ -148,17 +184,18 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
|
|
||||||
page :messages do
|
page :messages do
|
||||||
enter do
|
enter do
|
||||||
@items = MESSAGES.last(30).map { |m| "#{c(:yellow, m.timestamp)} #{c(:white, m.username)}: #{m.text}" }
|
@page_title = 'MESSAGES'
|
||||||
|
@items = MESSAGES.last(30).map { |m| "#{tc(:label, m.timestamp)} #{tc(:bright, m.username)}: #{m.text}" }
|
||||||
@scroll = 0
|
@scroll = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
render do
|
render do
|
||||||
text c(:cyan, '── Message Board ──'), x: CONT_X, y: CONT_Y
|
text '── Message Board ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
|
||||||
(@items[@scroll, cont_h - 2] || []).each_with_index do |line, i|
|
(@items[@scroll, cont_h - 2] || []).each_with_index do |line, i|
|
||||||
text line[0, cont_w], x: CONT_X, y: CONT_Y + 2 + i
|
text line[0, cont_w], x: CONT_X, y: CONT_Y + 2 + i
|
||||||
end
|
end
|
||||||
text c(:gray, "#{@items.size} msg(s) ↑↓ scroll q back"),
|
text "#{@items.size} msg(s) ↑↓ scroll q back",
|
||||||
x: CONT_X, y: CONT_Y + cont_h
|
x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
|
|
||||||
key(:up) { @scroll = [@scroll - 1, 0].max }
|
key(:up) { @scroll = [@scroll - 1, 0].max }
|
||||||
@@ -169,13 +206,16 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
page :new_msg do
|
page :new_msg do
|
||||||
enter { @input = ''; @status = '' }
|
enter { @page_title = 'POST MESSAGE'; @input = ''; @status = '' }
|
||||||
|
|
||||||
render do
|
render do
|
||||||
text c(:cyan, '── Post Message ──'), x: CONT_X, y: CONT_Y
|
text '── Post Message ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
|
||||||
text c(:gray, 'Type message and press Enter. Esc cancels.'), x: CONT_X, y: CONT_Y + 2
|
text 'Type your message and press Enter. Esc cancels.',
|
||||||
text c(:white, "#{@username}: #{@input}_"), x: CONT_X, y: CONT_Y + 4
|
x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
|
||||||
text c(:green, @status), x: CONT_X, y: CONT_Y + 6 unless @status.empty?
|
text "#{tc(:bright, @username)}: #{tc(:input, @input)}_",
|
||||||
|
x: CONT_X, y: CONT_Y + 4
|
||||||
|
text @status, x: CONT_X, y: CONT_Y + 6,
|
||||||
|
style: @status.start_with?('Sent') ? THEME[:success] : THEME[:error] unless @status.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
key(:enter) do
|
key(:enter) do
|
||||||
@@ -193,8 +233,8 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
page :blog do
|
page :blog do
|
||||||
enter { @items = WIKI.list('blog'); @item_sel = 0; @scroll = 0 }
|
enter { @page_title = 'BLOG'; @items = WIKI.list('blog'); @item_sel = 0; @scroll = 0 }
|
||||||
render { wiki_list_render(color: :blue, title: '── Blog Posts ──') }
|
render { wiki_list_render('── Blog Posts ──') }
|
||||||
key(:up) { scroll_list_up }
|
key(:up) { scroll_list_up }
|
||||||
key(:down) { scroll_list_down }
|
key(:down) { scroll_list_down }
|
||||||
key(:enter) { open_wiki_page(back: :blog) }
|
key(:enter) { open_wiki_page(back: :blog) }
|
||||||
@@ -204,8 +244,8 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
page :howto do
|
page :howto do
|
||||||
enter { @items = WIKI.list('howto'); @item_sel = 0; @scroll = 0 }
|
enter { @page_title = 'HOWTO'; @items = WIKI.list('howto'); @item_sel = 0; @scroll = 0 }
|
||||||
render { wiki_list_render(color: :magenta, title: '── HowTo Guides ──') }
|
render { wiki_list_render('── HowTo Guides ──') }
|
||||||
key(:up) { scroll_list_up }
|
key(:up) { scroll_list_up }
|
||||||
key(:down) { scroll_list_down }
|
key(:down) { scroll_list_down }
|
||||||
key(:enter) { open_wiki_page(back: :howto) }
|
key(:enter) { open_wiki_page(back: :howto) }
|
||||||
@@ -216,13 +256,13 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
|
|
||||||
page :page_view do
|
page :page_view do
|
||||||
render do
|
render do
|
||||||
text c(:cyan, @page_header[0, cont_w]), x: CONT_X, y: CONT_Y
|
text @page_header[0, cont_w], x: CONT_X, y: CONT_Y, style: THEME[:bright]
|
||||||
text c(:gray, @page_meta[0, cont_w]), x: CONT_X, y: CONT_Y + 1
|
text @page_meta[0, cont_w], x: CONT_X, y: CONT_Y + 1, style: THEME[:normal]
|
||||||
(@detail[@scroll, cont_h - 3] || []).each_with_index do |l, i|
|
(@detail[@scroll, cont_h - 3] || []).each_with_index do |l, i|
|
||||||
text c(:gray, l[0, cont_w]), x: CONT_X, y: CONT_Y + 3 + i
|
text l[0, cont_w], x: CONT_X, y: CONT_Y + 3 + i, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
text c(:gray, "#{@scroll + 1}/#{@detail.size} ↑↓ scroll q back"),
|
text "#{@scroll + 1}/#{@detail.size} ↑↓ scroll q back",
|
||||||
x: CONT_X, y: CONT_Y + cont_h
|
x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
|
|
||||||
key(:up) { @scroll = [@scroll - 1, 0].max }
|
key(:up) { @scroll = [@scroll - 1, 0].max }
|
||||||
@@ -232,28 +272,28 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
page :games do
|
page :games do
|
||||||
enter { @items = CATALOG.fetch; @item_sel = 0 }
|
enter { @page_title = 'GAME CATALOG'; @items = CATALOG.fetch; @item_sel = 0 }
|
||||||
|
|
||||||
render do
|
render do
|
||||||
text c(:cyan, '── Game Catalog ──'), x: CONT_X, y: CONT_Y
|
text '── Game Catalog ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
|
||||||
if @items.empty?
|
if @items.empty?
|
||||||
text c(:gray, 'No games found.'), x: CONT_X, y: CONT_Y + 2
|
text 'No games found.', x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
|
||||||
else
|
else
|
||||||
game = @items[@item_sel]
|
game = @items[@item_sel]
|
||||||
text c(:white, "#{@item_sel + 1}/#{@items.size} #{game.title}"[0, cont_w]),
|
text "#{@item_sel + 1}/#{@items.size} #{game.title}"[0, cont_w],
|
||||||
x: CONT_X, y: CONT_Y + 1
|
x: CONT_X, y: CONT_Y + 1, style: THEME[:bright]
|
||||||
text c(:gray, "#{game.platform} #{game.author}"[0, cont_w]),
|
text "#{game.platform} #{game.author}"[0, cont_w],
|
||||||
x: CONT_X, y: CONT_Y + 2
|
x: CONT_X, y: CONT_Y + 2, style: THEME[:normal]
|
||||||
wordwrap(game.desc.to_s, cont_w).first(cont_h - 10).each_with_index do |l, i|
|
wordwrap(game.desc.to_s, cont_w).first(cont_h - 10).each_with_index do |l, i|
|
||||||
text c(:gray, l), x: CONT_X, y: CONT_Y + 4 + i
|
text l, x: CONT_X, y: CONT_Y + 4 + i, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
badges = []
|
badges = []
|
||||||
badges << c(:green, '[▶ Play]') unless game.play_path.empty?
|
badges << tc(:success, '[▶ Play]') unless game.play_path.empty?
|
||||||
badges << c(:yellow, '[⬇ Download]') unless game.download_path.empty?
|
badges << tc(:label, '[⬇ Download]') unless game.download_path.empty?
|
||||||
badges << c(:blue, '[Source]') unless game.source_path.empty?
|
badges << tc(:bright, '[Source]') unless game.source_path.empty?
|
||||||
badges << c(:magenta, '[Docs]') unless game.docs_path.empty?
|
badges << tc(:header, '[Docs]') unless game.docs_path.empty?
|
||||||
text badges.join(' '), x: CONT_X, y: CONT_Y + cont_h - 5
|
text badges.join(' '), x: CONT_X, y: CONT_Y + cont_h - 5
|
||||||
text c(:gray, '↑↓ navigate q back'), x: CONT_X, y: CONT_Y + cont_h
|
text '↑↓ navigate q back', x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -265,16 +305,16 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
page :online do
|
page :online do
|
||||||
enter { @items = ONLINE.snapshot.sort.map { |_, name| name } }
|
enter { @page_title = 'ONLINE USERS'; @items = ONLINE.snapshot.sort.map { |_, name| name } }
|
||||||
|
|
||||||
render do
|
render do
|
||||||
text c(:yellow, '── Online Users ──'), x: CONT_X, y: CONT_Y
|
text '── Online Users ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
|
||||||
@items.each_with_index do |uname, i|
|
@items.each_with_index do |uname, i|
|
||||||
you = uname == @username ? c(:gray, ' ← you') : ''
|
you = uname == @username ? tc(:normal, ' ← you') : ''
|
||||||
text c(:white, uname) + you, x: CONT_X, y: CONT_Y + 2 + i
|
text tc(:bright, uname) + you, x: CONT_X, y: CONT_Y + 2 + i
|
||||||
end
|
end
|
||||||
text c(:gray, "#{@items.size} user(s) online q back"),
|
text "#{@items.size} user(s) online q back",
|
||||||
x: CONT_X, y: CONT_Y + cont_h
|
x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
|
|
||||||
key('r') { reload }
|
key('r') { reload }
|
||||||
@@ -284,6 +324,7 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
|
|
||||||
page :sysinfo do
|
page :sysinfo do
|
||||||
enter do
|
enter do
|
||||||
|
@page_title = 'SYSTEM INFO'
|
||||||
@items = [
|
@items = [
|
||||||
"Online users #{ONLINE.count}",
|
"Online users #{ONLINE.count}",
|
||||||
"Messages #{MESSAGES.count}",
|
"Messages #{MESSAGES.count}",
|
||||||
@@ -295,11 +336,11 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
render do
|
render do
|
||||||
text c(:magenta, '── System Info ──'), x: CONT_X, y: CONT_Y
|
text '── System Info ──', x: CONT_X, y: CONT_Y, style: THEME[:header]
|
||||||
@items.each_with_index do |row, i|
|
@items.each_with_index do |row, i|
|
||||||
text c(:gray, row[0, cont_w]), x: CONT_X, y: CONT_Y + 2 + i
|
text row[0, cont_w], x: CONT_X, y: CONT_Y + 2 + i, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
text c(:gray, 'q back'), x: CONT_X, y: CONT_Y + cont_h
|
text 'q back', x: CONT_X, y: CONT_Y + cont_h, style: THEME[:normal]
|
||||||
end
|
end
|
||||||
|
|
||||||
key('q') { go :idle }
|
key('q') { go :idle }
|
||||||
@@ -309,6 +350,8 @@ MAIN_TUI = BBS::TUI.define do
|
|||||||
start :idle
|
start :idle
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── App config ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
BBS.configure do |c|
|
BBS.configure do |c|
|
||||||
c.on_session_end = ->(session) { ONLINE.remove(session.session_id) }
|
c.on_session_end = ->(session) { ONLINE.remove(session.session_id) }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user