refact
This commit is contained in:
76
lib/menu.catalog.go
Normal file
76
lib/menu.catalog.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Session) ShowGames() {
|
||||
s.Printer.BoxHeader(s.Lang["CatTitle"], YL)
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["WikiLoading"], R))
|
||||
|
||||
softwares, err := s.BBS.CatalogRepo.FetchGames()
|
||||
if err != nil {
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", RD, s.Lang["WikiConnError"], err, R))
|
||||
s.Printer.Pause(s.Lang)
|
||||
return
|
||||
}
|
||||
|
||||
s.Printer.Send("\r\033[A\033[2K")
|
||||
|
||||
if len(softwares) == 0 {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["CatNoGames"], R))
|
||||
s.Printer.Pause(s.Lang)
|
||||
return
|
||||
}
|
||||
|
||||
base := "https://games.teletype.hu"
|
||||
|
||||
for i, entry := range softwares {
|
||||
sw := entry.Software
|
||||
lr := entry.LatestRelease
|
||||
|
||||
s.Printer.Send(fmt.Sprintf("\r\n %s%s%s\r\n", YL, strings.Repeat("─", W-4), R))
|
||||
s.Printer.Send(fmt.Sprintf(" %s%2d.%s %s%s%s%s %s[%s] by %s%s\r\n", YL, i+1, R, WH, B, sw.Title, R, GY, sw.Platform, sw.Author, R))
|
||||
|
||||
if sw.Desc != "" {
|
||||
wrappedDesc := s.Printer.Wrapped(sw.Desc, 7, 1000)
|
||||
for _, line := range strings.Split(wrappedDesc, "\r\n") {
|
||||
s.Printer.Send(fmt.Sprintf("%s%s\r\n", GY, line))
|
||||
}
|
||||
}
|
||||
|
||||
if lr != nil {
|
||||
badges := []string{}
|
||||
if lr.HTMLFolderPath != "" {
|
||||
badges = append(badges, fmt.Sprintf("%s[▶ Play]%s", GR, R))
|
||||
}
|
||||
if lr.CartridgePath != "" {
|
||||
badges = append(badges, fmt.Sprintf("%s[⬇ Download]%s", BL, R))
|
||||
}
|
||||
if lr.SourcePath != "" {
|
||||
badges = append(badges, fmt.Sprintf("%s[Source]%s", MG, R))
|
||||
}
|
||||
if lr.DocsFolderPath != "" {
|
||||
badges = append(badges, fmt.Sprintf("%s[Docs]%s", YL, R))
|
||||
}
|
||||
|
||||
badgeStr := "–"
|
||||
if len(badges) > 0 {
|
||||
badgeStr = strings.Join(badges, " ")
|
||||
}
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s: v%s%s %s\r\n", GY, s.Lang["CatLatest"], lr.Version, R, badgeStr))
|
||||
|
||||
if lr.HTMLFolderPath != "" {
|
||||
url := base + lr.HTMLFolderPath
|
||||
s.Printer.Send(fmt.Sprintf(" %s▶ %s%s\r\n", DIM, url, R))
|
||||
}
|
||||
}
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, fmt.Sprintf(s.Lang["CatVersions"], len(entry.Releases)), R))
|
||||
}
|
||||
|
||||
s.Printer.HR("─", YL)
|
||||
s.Printer.Send("\r\n")
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s: %s%s\r\n", GY, s.Lang["CatFull"], base, R))
|
||||
s.Printer.Pause(s.Lang)
|
||||
}
|
||||
33
lib/menu.online.go
Normal file
33
lib/menu.online.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func (s *Session) ShowOnline() {
|
||||
s.Printer.BoxHeader(s.Lang["OnlineTitle"], CY)
|
||||
s.BBS.Mu.Lock()
|
||||
snap := make(map[string]string)
|
||||
for k, v := range s.BBS.OnlineUsers {
|
||||
snap[k] = v
|
||||
}
|
||||
s.BBS.Mu.Unlock()
|
||||
|
||||
keys := make([]string, 0, len(snap))
|
||||
for k := range snap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, addr := range keys {
|
||||
user := snap[addr]
|
||||
marker := ""
|
||||
if addr == s.Addr {
|
||||
marker = fmt.Sprintf(" %s%s%s", GR, s.Lang["OnlineYou"], R)
|
||||
}
|
||||
s.Printer.Send(fmt.Sprintf(" %s•%s %s%s%s%s\r\n", CY, R, WH, user, R, marker))
|
||||
}
|
||||
s.Printer.Send(fmt.Sprintf("\r\n %s%s\r\n", GY, fmt.Sprintf(s.Lang["OnlineTotal"], WH, len(snap), GY, R)))
|
||||
s.Printer.Pause(s.Lang)
|
||||
}
|
||||
31
lib/menu.sysinfo.go
Normal file
31
lib/menu.sysinfo.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Session) ShowSysinfo() {
|
||||
s.Printer.BoxHeader(s.Lang["SysInfoTitle"], GY)
|
||||
s.BBS.Mu.Lock()
|
||||
uc := len(s.BBS.OnlineUsers)
|
||||
mc := len(s.BBS.Messages)
|
||||
s.BBS.Mu.Unlock()
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
rows := [][]string{
|
||||
{s.Lang["SysServerTime"], now},
|
||||
{s.Lang["SysOnlineUsers"], strconv.Itoa(uc)},
|
||||
{s.Lang["SysMsgCount"], strconv.Itoa(mc)},
|
||||
{s.Lang["SysWikiURL"], WikiJSBaseURL},
|
||||
{s.Lang["SysGamesAPI"], GamesAPIURL},
|
||||
{s.Lang["SysPlatform"], "Go BBS v2.0"},
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%-18s%s %s%s%s\r\n", GY, row[0], R, WH, row[1], R))
|
||||
}
|
||||
s.Printer.Pause(s.Lang)
|
||||
}
|
||||
47
lib/menu.uzenopal.go
Normal file
47
lib/menu.uzenopal.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Session) ShowUzenopal() {
|
||||
s.Printer.BoxHeader(s.Lang["MsgBoardTitle"], GR)
|
||||
s.BBS.Mu.Lock()
|
||||
snap := make([]Message, len(s.BBS.Messages))
|
||||
copy(snap, s.BBS.Messages)
|
||||
s.BBS.Mu.Unlock()
|
||||
|
||||
if len(snap) == 0 {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["MsgNoMessages"], R))
|
||||
} else {
|
||||
start := 0
|
||||
if len(snap) > 30 {
|
||||
start = len(snap) - 30
|
||||
}
|
||||
for i, msg := range snap[start:] {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%02d%s %s%s%s %s%s:%s %s\r\n", GR, i+1, R, GY, msg.Timestamp, R, WH, msg.User, R, msg.Text))
|
||||
}
|
||||
}
|
||||
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s[N]%s %s %s[ENTER]%s %s → ", GY, R, s.Lang["MsgNew"], GY, R, s.Lang["MsgBack"]))
|
||||
choice, _ := s.Printer.ReadLine()
|
||||
if strings.ToUpper(strings.TrimSpace(choice)) == "N" {
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", WH, s.Lang["MsgEnterText"], R))
|
||||
msgText, _ := s.Printer.ReadLine()
|
||||
msgText = strings.TrimSpace(msgText)
|
||||
if msgText != "" {
|
||||
if len(msgText) > 200 {
|
||||
msgText = msgText[:200]
|
||||
}
|
||||
ts := time.Now().Format("01-02 15:04")
|
||||
s.BBS.Mu.Lock()
|
||||
s.BBS.Messages = append(s.BBS.Messages, Message{User: s.Username, Timestamp: ts, Text: msgText})
|
||||
s.BBS.Mu.Unlock()
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GR, s.Lang["MsgSent"], R))
|
||||
} else {
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GY, s.Lang["MsgEmpty"], R))
|
||||
}
|
||||
}
|
||||
}
|
||||
93
lib/menu.wiki.go
Normal file
93
lib/menu.wiki.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Session) ShowWikiList(tag string, color string, title string) {
|
||||
s.Printer.BoxHeader(title, color)
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["WikiLoading"], R))
|
||||
|
||||
pages, err := s.BBS.WikiRepo.FetchList(tag)
|
||||
if err != nil {
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", RD, s.Lang["WikiConnError"], err, R))
|
||||
s.Printer.Pause(s.Lang)
|
||||
return
|
||||
}
|
||||
|
||||
s.Printer.Send("\r\033[A\033[2K")
|
||||
|
||||
if len(pages) == 0 {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, fmt.Sprintf(s.Lang["WikiNoResults"], tag), R))
|
||||
s.Printer.Pause(s.Lang)
|
||||
return
|
||||
}
|
||||
|
||||
maxIdx := 25
|
||||
if len(pages) < maxIdx {
|
||||
maxIdx = len(pages)
|
||||
}
|
||||
|
||||
for i, p := range pages[:maxIdx] {
|
||||
date := s.Printer.FmtDate(p.CreatedAt)
|
||||
if date == "–" {
|
||||
date = s.Printer.FmtDate(p.UpdatedAt)
|
||||
}
|
||||
titleP := p.Title
|
||||
if titleP == "" {
|
||||
titleP = p.Path
|
||||
}
|
||||
if len(titleP) > 55 {
|
||||
titleP = titleP[:55]
|
||||
}
|
||||
desc := p.Description
|
||||
if len(desc) > 60 {
|
||||
desc = desc[:60]
|
||||
}
|
||||
s.Printer.Send(fmt.Sprintf(" %s%2d%s %s%s%s\r\n", color, i+1, R, WH, titleP, R))
|
||||
if desc != "" {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", GY, date, DIM, desc, R))
|
||||
} else {
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, date, R))
|
||||
}
|
||||
}
|
||||
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", GY, s.Lang["WikiEnterNum"], R))
|
||||
choice, _ := s.Printer.ReadLine()
|
||||
choice = strings.TrimSpace(choice)
|
||||
idx, err := strconv.Atoi(choice)
|
||||
if err != nil || idx < 1 || idx > len(pages) {
|
||||
return
|
||||
}
|
||||
|
||||
page := pages[idx-1]
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s", GY, s.Lang["WikiFetchContent"], R))
|
||||
content, err := s.BBS.WikiRepo.FetchContent(page.ID)
|
||||
if err != nil {
|
||||
s.Printer.Send(fmt.Sprintf("\r\n%sHiba: %v%s\r\n", RD, err, R))
|
||||
s.Printer.Pause(s.Lang)
|
||||
return
|
||||
}
|
||||
|
||||
s.Printer.Send("\r\033[A\033[2K")
|
||||
s.Printer.Send(fmt.Sprintf("\r\n"))
|
||||
s.Printer.HR("═", color)
|
||||
s.Printer.Send("\r\n")
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s%s%s\r\n", WH, B, page.Title, R))
|
||||
url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path)
|
||||
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", GY, s.Printer.FmtDate(page.CreatedAt), DIM, url, R))
|
||||
s.Printer.HR("─", GY)
|
||||
s.Printer.Send("\r\n\r\n")
|
||||
|
||||
body := s.Printer.Wrapped(content, 2, 5000)
|
||||
for _, line := range strings.Split(body, "\r\n") {
|
||||
s.Printer.Send(line+"\r\n")
|
||||
}
|
||||
|
||||
s.Printer.Send("\r\n")
|
||||
s.Printer.HR("─", GY)
|
||||
s.Printer.Send("\r\n")
|
||||
s.Printer.Pause(s.Lang)
|
||||
}
|
||||
49
lib/repository.catalog.go
Normal file
49
lib/repository.catalog.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const GamesAPIURL = "https://games.teletype.hu/api/software"
|
||||
|
||||
type SoftwareEntry struct {
|
||||
Software struct {
|
||||
Title string `json:"title"`
|
||||
Platform string `json:"platform"`
|
||||
Author string `json:"author"`
|
||||
Desc string `json:"desc"`
|
||||
} `json:"software"`
|
||||
Releases []interface{} `json:"releases"`
|
||||
LatestRelease *struct {
|
||||
Version string `json:"version"`
|
||||
HTMLFolderPath string `json:"htmlFolderPath"`
|
||||
CartridgePath string `json:"cartridgePath"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
DocsFolderPath string `json:"docsFolderPath"`
|
||||
} `json:"latestRelease"`
|
||||
}
|
||||
|
||||
type CatalogRepository struct{}
|
||||
|
||||
func NewCatalogRepository() *CatalogRepository {
|
||||
return &CatalogRepository{}
|
||||
}
|
||||
|
||||
func (r *CatalogRepository) FetchGames() ([]SoftwareEntry, error) {
|
||||
client := &http.Client{Timeout: 12 * time.Second}
|
||||
resp, err := client.Get(GamesAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Softwares []SoftwareEntry `json:"softwares"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Softwares, nil
|
||||
}
|
||||
106
lib/repository.wiki.go
Normal file
106
lib/repository.wiki.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const WikiJSBaseURL = "https://wiki.teletype.hu"
|
||||
|
||||
type WikiPage struct {
|
||||
ID interface{} `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Locale string `json:"locale"`
|
||||
}
|
||||
|
||||
type WikiRepository struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func NewWikiRepository(token string) *WikiRepository {
|
||||
return &WikiRepository{Token: token}
|
||||
}
|
||||
|
||||
func (r *WikiRepository) graphql(query string) (map[string]interface{}, error) {
|
||||
payload := map[string]string{"query": query}
|
||||
data, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest("POST", WikiJSBaseURL+"/graphql", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if r.Token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+r.Token)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 12 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *WikiRepository) FetchList(tag string) ([]WikiPage, error) {
|
||||
q := fmt.Sprintf(`{ pages { list(orderBy: CREATED, orderByDirection: DESC, tags: ["%s"]) { id path title description createdAt updatedAt locale } } }`, tag)
|
||||
res, err := r.graphql(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, ok := res["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format")
|
||||
}
|
||||
pages, ok := data["pages"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format")
|
||||
}
|
||||
listRaw, ok := pages["list"].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format")
|
||||
}
|
||||
|
||||
var list []WikiPage
|
||||
jsonBytes, _ := json.Marshal(listRaw)
|
||||
json.Unmarshal(jsonBytes, &list)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *WikiRepository) FetchContent(pageID interface{}) (string, error) {
|
||||
var idStr string
|
||||
switch v := pageID.(type) {
|
||||
case string:
|
||||
idStr = v
|
||||
case float64:
|
||||
idStr = fmt.Sprintf("%.0f", v)
|
||||
default:
|
||||
idStr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`{ pages { single(id: %s) { content } } }`, idStr)
|
||||
res, err := r.graphql(q)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := res["data"].(map[string]interface{})
|
||||
pages := data["pages"].(map[string]interface{})
|
||||
single := pages["single"].(map[string]interface{})
|
||||
return single["content"].(string), nil
|
||||
}
|
||||
102
lib/sys.header.go
Normal file
102
lib/sys.header.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const Banner = `
|
||||
████████╗███████╗██╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗
|
||||
██╔══╝██╔════╝██║ ██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝
|
||||
██║ █████╗ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝█████╗
|
||||
██║ ██╔══╝ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝
|
||||
██║ ███████╗███████╗███████╗ ██║ ██║ ██║ ███████╗
|
||||
╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
||||
|
||||
██████╗ █████╗ ███╗ ███╗███████╗███████╗
|
||||
██╔════╝ ██╔══██╗████╗ ████║██╔════╝██╔════╝
|
||||
██║ ███╗███████║██╔████╔██║█████╗ ███████╗
|
||||
██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ╚════██║
|
||||
╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
|
||||
|
||||
░░ BBS v2.0 ░░ teletype.hu ░░
|
||||
Welcome to the Teletype community bulletin board!
|
||||
`
|
||||
|
||||
type BBS struct {
|
||||
Messages []Message
|
||||
OnlineUsers map[string]string // addr -> username
|
||||
Mu sync.Mutex
|
||||
WikiToken string
|
||||
WikiRepo *WikiRepository
|
||||
CatalogRepo *CatalogRepository
|
||||
}
|
||||
|
||||
func NewBBS(wikiToken string) *BBS {
|
||||
return &BBS{
|
||||
Messages: []Message{},
|
||||
OnlineUsers: make(map[string]string),
|
||||
WikiToken: wikiToken,
|
||||
WikiRepo: NewWikiRepository(wikiToken),
|
||||
CatalogRepo: NewCatalogRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
type UI struct {
|
||||
Printer *Printer
|
||||
Lang T
|
||||
}
|
||||
|
||||
func NewUI(printer *Printer, lang T) *UI {
|
||||
return &UI{Printer: printer, Lang: lang}
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
BBS *BBS
|
||||
Printer *Printer
|
||||
Username string
|
||||
Addr string
|
||||
Lang T
|
||||
}
|
||||
|
||||
func NewSession(bbs *BBS, printer *Printer, username string, addr string, lang T) *Session {
|
||||
return &Session{
|
||||
BBS: bbs,
|
||||
Printer: printer,
|
||||
Username: username,
|
||||
Addr: addr,
|
||||
Lang: lang,
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UI) MainMenu(username string) string {
|
||||
headerLine := strings.Repeat("═", W)
|
||||
|
||||
l1 := ui.Printer.PadLine(fmt.Sprintf(" %s%s%s %s@%s%s", YL, ui.Lang["MainMenuTitle"], R, GY, username, R), W)
|
||||
l2 := ui.Printer.PadLine(fmt.Sprintf(" %s[1]%s %s", GR, R, ui.Lang["MenuUzenopal"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[2]%s %s", BL, R, ui.Lang["MenuBlog"]), W/2)
|
||||
l3 := ui.Printer.PadLine(fmt.Sprintf(" %s[3]%s %s", MG, R, ui.Lang["MenuHowto"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[4]%s %s", YL, R, ui.Lang["MenuCatalog"]), W/2)
|
||||
l4 := ui.Printer.PadLine(fmt.Sprintf(" %s[5]%s %s", CY, R, ui.Lang["MenuOnline"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[6]%s %s", GY, R, ui.Lang["MenuSysinfo"]), W/2)
|
||||
l5 := ui.Printer.PadLine(fmt.Sprintf(" %s[Q]%s %s", RD, R, ui.Lang["MenuExit"]), W)
|
||||
|
||||
return fmt.Sprintf(
|
||||
"\n%s╔%s╗%s\n"+
|
||||
"%s║%s%s║%s\n"+
|
||||
"%s╠%s╣%s\n"+
|
||||
"%s║%s%s║%s\n"+
|
||||
"%s║%s%s║%s\n"+
|
||||
"%s║%s%s║%s\n"+
|
||||
"%s║%s%s║%s\n"+
|
||||
"%s╚%s╝%s\n%s",
|
||||
WH, headerLine, R,
|
||||
WH, l1, WH, R,
|
||||
WH, headerLine, R,
|
||||
WH, l2, WH, R,
|
||||
WH, l3, WH, R,
|
||||
WH, l4, WH, R,
|
||||
WH, l5, WH, R,
|
||||
WH, headerLine, R,
|
||||
ui.Lang["Choice"],
|
||||
)
|
||||
}
|
||||
49
lib/sys.i18n.go
Normal file
49
lib/sys.i18n.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package lib
|
||||
|
||||
type T map[string]string
|
||||
|
||||
var En = T{
|
||||
"Welcome": "Welcome to Teletype's Bulletin Board System!",
|
||||
"AskName": "Enter your name:",
|
||||
"Greeting": "Hello, %s%s%s! Welcome to Teletype BBS!",
|
||||
"MainMenuTitle": "MAIN MENU",
|
||||
"MenuUzenopal": "Message Board",
|
||||
"MenuBlog": "Blog Posts",
|
||||
"MenuHowto": "HowTo Guides",
|
||||
"MenuCatalog": "Game Catalog",
|
||||
"MenuOnline": "Online Users",
|
||||
"MenuSysinfo": "System Info",
|
||||
"MenuExit": "Exit",
|
||||
"Choice": "Choice: ",
|
||||
"Pause": "Press ENTER...",
|
||||
"Goodbye": "Goodbye, %s! 👋",
|
||||
"MsgBoardTitle": "📋 MESSAGE BOARD",
|
||||
"MsgNoMessages": "(No messages yet — be the first!)",
|
||||
"MsgNew": "Write new message",
|
||||
"MsgBack": "Back",
|
||||
"MsgEnterText": "Message text:",
|
||||
"MsgSent": "✓ Sent!",
|
||||
"MsgEmpty": "(Empty – not sent)",
|
||||
"WikiLoading": "Loading...",
|
||||
"WikiConnError": "Connection error",
|
||||
"WikiNoResults": "No results for tag '%s'.",
|
||||
"WikiEnterNum": "Enter number to open, ENTER to go back:",
|
||||
"WikiFetchContent": "Fetching content...",
|
||||
"CatTitle": "🎮 GAME CATALOG",
|
||||
"CatNoGames": "No games available.",
|
||||
"CatLatest": "Latest",
|
||||
"CatVersions": "%d versions available",
|
||||
"CatFull": "Full catalog",
|
||||
"OnlineTitle": "👥 ONLINE USERS",
|
||||
"OnlineYou": "← you",
|
||||
"OnlineTotal": "Total: %s%d%s users online",
|
||||
"SysInfoTitle": "ℹ SYSTEM INFO",
|
||||
"SysServerTime": "Server time",
|
||||
"SysOnlineUsers": "Online users",
|
||||
"SysMsgCount": "Message count",
|
||||
"SysWikiURL": "Wiki URL",
|
||||
"SysGamesAPI": "Games API",
|
||||
"SysPlatform": "Platform",
|
||||
"SysYes": "yes",
|
||||
"SysNo": "no",
|
||||
}
|
||||
202
lib/sys.print.go
Normal file
202
lib/sys.print.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ANSI Colors
|
||||
const (
|
||||
R = "\033[0m"
|
||||
B = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
CY = "\033[1;36m"
|
||||
YL = "\033[1;33m"
|
||||
GR = "\033[1;32m"
|
||||
RD = "\033[1;31m"
|
||||
MG = "\033[1;35m"
|
||||
WH = "\033[1;37m"
|
||||
BL = "\033[1;34m"
|
||||
GY = "\033[0;37m"
|
||||
)
|
||||
|
||||
const W = 70
|
||||
|
||||
type Message struct {
|
||||
User string
|
||||
Timestamp string
|
||||
Text string
|
||||
}
|
||||
|
||||
type Printer struct {
|
||||
Conn net.Conn
|
||||
}
|
||||
|
||||
func NewPrinter(conn net.Conn) *Printer {
|
||||
return &Printer{Conn: conn}
|
||||
}
|
||||
|
||||
func (p *Printer) Send(text string) {
|
||||
normalized := strings.ReplaceAll(text, "\r\n", "\n")
|
||||
normalized = strings.ReplaceAll(normalized, "\n", "\r\n")
|
||||
p.Conn.Write([]byte(normalized))
|
||||
}
|
||||
|
||||
func (p *Printer) ReadLine() (string, error) {
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
b := make([]byte, 1)
|
||||
_, err := p.Conn.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ch := b[0]
|
||||
|
||||
if ch == 255 {
|
||||
cmd := make([]byte, 2)
|
||||
_, err := p.Conn.Read(cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if cmd[0] == 250 {
|
||||
for {
|
||||
tmp := make([]byte, 1)
|
||||
_, err := p.Conn.Read(tmp)
|
||||
if err != nil || tmp[0] == 240 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '\r' || ch == '\n' {
|
||||
res := buf.String()
|
||||
p.Send("\r\n")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if ch == 8 || ch == 127 {
|
||||
if buf.Len() > 0 {
|
||||
curr := buf.Bytes()
|
||||
buf.Reset()
|
||||
buf.Write(curr[:len(curr)-1])
|
||||
p.Send("\b \b")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch >= 32 && ch < 127 {
|
||||
buf.WriteByte(ch)
|
||||
p.Send(string(ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Printer) Pause(lang T) {
|
||||
p.Send(fmt.Sprintf("\r\n%s [ %s ]%s ", GY, lang["Pause"], R))
|
||||
p.ReadLine()
|
||||
}
|
||||
|
||||
func (p *Printer) VisibleLen(s string) int {
|
||||
re := regexp.MustCompile(`\033\[[0-9;]*m`)
|
||||
return utf8.RuneCountInString(re.ReplaceAllString(s, ""))
|
||||
}
|
||||
|
||||
func (p *Printer) PadLine(content string, width int) string {
|
||||
vLen := p.VisibleLen(content)
|
||||
padding := width - vLen
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
return content + strings.Repeat(" ", padding)
|
||||
}
|
||||
|
||||
func (p *Printer) BoxHeader(title string, color string) {
|
||||
line := strings.Repeat("═", W)
|
||||
titleLen := utf8.RuneCountInString(title)
|
||||
padding := (W - 2 - titleLen) / 2
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
inner := strings.Repeat(" ", padding) + title + strings.Repeat(" ", W-2-titleLen-padding)
|
||||
p.Send(fmt.Sprintf(
|
||||
"\n%s%s%s\n%s║%s %s%s%s %s║%s\n%s%s%s\n",
|
||||
color, line, R,
|
||||
color, R, B, inner, R, color, R,
|
||||
color, line, R,
|
||||
))
|
||||
}
|
||||
|
||||
func (p *Printer) HR(char string, color string) {
|
||||
p.Send(fmt.Sprintf("%s%s%s", color, strings.Repeat(char, W), R))
|
||||
}
|
||||
|
||||
func (p *Printer) FmtDate(s string) string {
|
||||
t, err := time.Parse(time.RFC3339, strings.Replace(s, "Z", "+00:00", 1))
|
||||
if err != nil {
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
}
|
||||
return "–"
|
||||
}
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func (p *Printer) StripMD(text string) string {
|
||||
reImg := regexp.MustCompile(`!\[.*?\]\(.*?\)|\[(.*?)\]\(.*?\)|#{1,6}\s*|[*_` + "`" + `~>|]`)
|
||||
text = reImg.ReplaceAllStringFunc(text, func(s string) string {
|
||||
if strings.HasPrefix(s, "[") {
|
||||
sub := regexp.MustCompile(`\[(.*?)\]\(.*?\)` )
|
||||
matches := sub.FindStringSubmatch(s)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "#") || strings.ContainsAny(s, "*_`~>|") {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
})
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func (p *Printer) Wrapped(text string, indent int, maxChars int) string {
|
||||
if len(text) > maxChars {
|
||||
text = text[:maxChars]
|
||||
}
|
||||
text = p.StripMD(text)
|
||||
prefix := strings.Repeat(" ", indent)
|
||||
var lines []string
|
||||
|
||||
paras := strings.Split(text, "\n")
|
||||
for _, para := range paras {
|
||||
para = strings.TrimSpace(para)
|
||||
if para == "" {
|
||||
lines = append(lines, "")
|
||||
continue
|
||||
}
|
||||
|
||||
words := strings.Fields(para)
|
||||
if len(words) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
currentLine := prefix + words[0]
|
||||
for _, word := range words[1:] {
|
||||
if len(currentLine)+1+len(word) > W {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = prefix + word
|
||||
} else {
|
||||
currentLine += " " + word
|
||||
}
|
||||
}
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
return strings.Join(lines, "\r\n")
|
||||
}
|
||||
Reference in New Issue
Block a user