ruby version

This commit is contained in:
2026-04-28 22:58:37 +02:00
parent 6f6dcd062f
commit 4ac5f1632f
12 changed files with 462 additions and 37 deletions

27
lib/catalog.rb Normal file
View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'net/http'
require 'json'
require 'uri'
class CatalogClient
API_URL = 'https://games.teletype.hu/api/software'
GAMES_URL = 'https://games.teletype.hu'
def fetch
uri = URI(API_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.open_timeout = 12
http.read_timeout = 12
data = JSON.parse(http.get(uri.path).body)
data.is_a?(Hash) ? (data['softwares'] || []) : data
rescue => e
warn "Catalog fetch error: #{e}"
[]
end
def play_url(path)
"#{GAMES_URL}#{path}"
end
end

201
lib/display.rb Normal file
View File

@@ -0,0 +1,201 @@
# frozen_string_literal: true
require 'time'
module Display
W = 70
RESET = "\e[0m"
BOLD = "\e[1m"
CYAN = "\e[0;36m"
YELLOW = "\e[0;33m"
GREEN = "\e[1;32m"
RED = "\e[1;31m"
MAGENTA = "\e[0;35m"
WHITE = "\e[1;37m"
BLUE = "\e[0;34m"
GRAY = "\e[0;37m"
module_function
def hr(color = GRAY)
" #{color}#{'─' * (W - 4)}#{RESET}\r\n"
end
def box_header(title, color = CYAN)
pad = [W - title.length - 6, 2].max
" #{color}┌─ #{WHITE}#{title} #{color}#{'─' * pad}#{RESET}\r\n"
end
def strip_md(text)
text.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
.gsub(/[#*_`~>|\\]/, '')
.gsub(/\r?\n+/, ' ')
.strip
end
def wrap(text, indent: 4, width: W - 6)
words = strip_md(text).split
lines = []
line = +''
words.each do |word|
if line.empty?
line << word
elsif line.length + 1 + word.length <= width
line << ' ' << word
else
lines << line.dup
line = +word
end
end
lines << line unless line.empty?
lines.map { |l| "#{' ' * indent}#{l}\r\n" }.join
end
def fmt_date(iso)
Time.parse(iso).strftime('%Y-%m-%d')
rescue
iso.to_s
end
def wait_enter(runner)
runner.write "\r\n #{GRAY}Press ENTER to continue...#{RESET}"
runner.readline
end
def render_messages(messages, runner)
runner.write "\r\n"
runner.write box_header('Message Board')
runner.write "\r\n"
if messages.empty?
runner.write " #{GRAY}No messages yet.#{RESET}\r\n"
else
messages.each_with_index do |msg, i|
runner.write " #{GRAY}[#{i + 1}]#{RESET} #{YELLOW}#{msg.timestamp}#{RESET} #{WHITE}#{msg.username}#{RESET}: #{GRAY}#{msg.text}#{RESET}\r\n"
end
end
runner.write "\r\n"
wait_enter(runner)
end
def render_wiki_list(wiki, tag, title, color, runner)
runner.write "\r\n"
runner.write box_header(title, color)
runner.write "\r\n #{GRAY}Loading...#{RESET}\r\n"
pages = wiki.list(tag)
runner.write "\e[1A\e[2K"
if pages.empty?
runner.write " #{RED}No results.#{RESET}\r\n\r\n"
wait_enter(runner)
return
end
shown = pages.first(25)
shown.each_with_index do |page, i|
runner.write " #{GRAY}#{i + 1}.#{RESET} #{color}#{page['title']}#{RESET} #{GRAY}#{fmt_date(page['createdAt'])}#{RESET}\r\n"
desc = page['description'].to_s.strip
runner.write " #{GRAY}#{desc[0...65]}#{RESET}\r\n" unless desc.empty?
end
runner.write "\r\n #{GRAY}Enter number to read (blank to go back):#{RESET} "
input = runner.readline&.strip
return if input.nil? || input.empty?
idx = input.to_i - 1
page = shown[idx]
unless page
runner.write "\r\n #{RED}Invalid selection.#{RESET}\r\n"
wait_enter(runner)
return
end
runner.write "\r\n #{GRAY}Loading page...#{RESET}\r\n"
body = wiki.content(page['id'])
runner.write "\e[1A\e[2K"
runner.write "\r\n"
runner.write hr(color)
runner.write " #{color}#{page['title']}#{RESET}\r\n"
runner.write " #{GRAY}#{fmt_date(page['createdAt'])} #{wiki.page_url(page['locale'], page['path'])}#{RESET}\r\n"
runner.write hr(color)
runner.write "\r\n"
runner.write wrap(body)
runner.write "\r\n"
wait_enter(runner)
end
def render_catalog(catalog, runner)
runner.write "\r\n"
runner.write box_header('Game Catalog', CYAN)
runner.write "\r\n #{GRAY}Loading...#{RESET}\r\n"
games = catalog.fetch
runner.write "\e[1A\e[2K"
unless games.is_a?(Array) && !games.empty?
runner.write " #{RED}Could not load catalog.#{RESET}\r\n\r\n"
wait_enter(runner)
return
end
games.each_with_index do |entry, i|
next unless entry.is_a?(Hash)
sw = entry['software'] || {}
latest = entry['latestRelease'] || {}
count = (entry['releases'] || []).length
runner.write "\r\n"
runner.write hr
runner.write " #{CYAN}#{i + 1}. #{sw['title']}#{RESET} #{GRAY}#{sw['platform']} #{sw['author']}#{RESET}\r\n"
runner.write wrap(sw['desc'].to_s) unless sw['desc'].to_s.strip.empty?
badges = []
badges << "#{GREEN}[▶ Play]#{RESET}" if latest['htmlFolderPath'].to_s != ''
badges << "#{YELLOW}[⬇ Download]#{RESET}" if latest['cartridgePath'].to_s != ''
badges << "#{BLUE}[Source]#{RESET}" if latest['sourcePath'].to_s != ''
badges << "#{MAGENTA}[Docs]#{RESET}" if latest['docsFolderPath'].to_s != ''
runner.write " #{badges.join(' ')}\r\n" unless badges.empty?
if latest['htmlFolderPath'].to_s != ''
runner.write " #{GRAY}#{catalog.play_url(latest['htmlFolderPath'])}#{RESET}\r\n"
end
runner.write " #{GRAY}#{count} versions available#{RESET}\r\n" if count > 1
end
runner.write "\r\n"
runner.write hr
runner.write " #{GRAY}#{CatalogClient::GAMES_URL}#{RESET}\r\n\r\n"
wait_enter(runner)
end
def render_online(online, my_sid, my_name, runner)
runner.write "\r\n"
runner.write box_header('Online Users', YELLOW)
runner.write "\r\n"
snapshot = online.snapshot
snapshot.sort.each do |sid, name|
marker = sid == my_sid ? " #{GRAY}← you#{RESET}" : ''
runner.write " #{WHITE}#{name}#{RESET}#{marker}\r\n"
end
runner.write "\r\n #{GRAY}#{snapshot.size} user(s) online#{RESET}\r\n\r\n"
wait_enter(runner)
end
def render_sysinfo(online, messages, runner)
runner.write "\r\n"
runner.write box_header('System Info', MAGENTA)
runner.write "\r\n"
runner.write " #{GRAY}Online users #{WHITE}#{online.count}#{RESET}\r\n"
runner.write " #{GRAY}Messages #{WHITE}#{messages.count}#{RESET}\r\n"
runner.write " #{GRAY}Wiki #{WHITE}https://wiki.teletype.hu#{RESET}\r\n"
runner.write " #{GRAY}Games API #{WHITE}https://games.teletype.hu#{RESET}\r\n"
runner.write " #{GRAY}Platform #{WHITE}#{RUBY_PLATFORM}#{RESET}\r\n"
runner.write " #{GRAY}Ruby #{WHITE}#{RUBY_VERSION}#{RESET}\r\n"
runner.write "\r\n"
wait_enter(runner)
end
end

45
lib/message_board.rb Normal file
View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'csv'
require 'time'
require 'fileutils'
class MessageBoard
Message = Struct.new(:timestamp, :username, :text)
def initialize(path)
@path = path
@messages = []
@mu = Mutex.new
load_csv
end
def append(username, text)
msg = Message.new(Time.now.strftime('%m-%d %H:%M'), username, text)
@mu.synchronize do
@messages << msg
FileUtils.mkdir_p(File.dirname(@path))
CSV.open(@path, 'a') { |csv| csv << [msg.timestamp, msg.username, msg.text] }
end
msg
end
def last(n = 30)
@mu.synchronize { @messages.last(n) }
end
def count
@mu.synchronize { @messages.size }
end
private
def load_csv
return unless File.exist?(@path)
CSV.foreach(@path) do |row|
@messages << Message.new(*row)
end
rescue => e
warn "MessageBoard load error: #{e}"
end
end

24
lib/online_users.rb Normal file
View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
class OnlineUsers
def initialize
@users = {}
@mu = Mutex.new
end
def add(session_id, name)
@mu.synchronize { @users[session_id] = name }
end
def remove(session_id)
@mu.synchronize { @users.delete(session_id) }
end
def snapshot
@mu.synchronize { @users.dup }
end
def count
@mu.synchronize { @users.size }
end
end

53
lib/wiki.rb Normal file
View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'net/http'
require 'json'
require 'uri'
class WikiClient
BASE_URL = 'https://wiki.teletype.hu'
def initialize(token: nil)
@token = token
end
def list(tag)
query = <<~GQL
{ pages { list(orderBy: CREATED, orderByDirection: DESC, tags: ["#{tag}"]) {
id path title description createdAt locale
}}}
GQL
graphql(query).dig('data', 'pages', 'list') || []
rescue => e
warn "Wiki list error: #{e}"
[]
end
def content(page_id)
query = "{ pages { single(id: #{page_id}) { content } } }"
graphql(query).dig('data', 'pages', 'single', 'content') || ''
rescue => e
warn "Wiki content error: #{e}"
''
end
def page_url(locale, path)
"#{BASE_URL}/#{locale}/#{path}"
end
private
def graphql(query)
uri = URI("#{BASE_URL}/graphql")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.open_timeout = 12
http.read_timeout = 12
req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
req['Authorization'] = "Bearer #{@token}" if @token
req.body = JSON.generate(query: query)
JSON.parse(http.request(req).body)
end
end