@@ -17,6 +17,7 @@ "github.com/runawaydevil/hammer/internal/community"
"github.com/runawaydevil/hammer/internal/dispatch"
"github.com/runawaydevil/hammer/internal/filearea"
+ "github.com/runawaydevil/hammer/internal/forum"
"github.com/runawaydevil/hammer/internal/localization"
"github.com/runawaydevil/hammer/internal/tools/agecrypt"
"github.com/runawaydevil/hammer/internal/tools/b64"
@@ -567,6 +568,110 @@ online = []community.Online{} }
return &dispatch.Result{Data: online, Lines: lines}, nil
+ },
+ },
+ {
+ Name: "forum.areas",
+ Summary: "the message areas, with topic and post counts",
+ Free: true,
+ Run: func(_ context.Context, c *dispatch.Call) (*dispatch.Result, error) {
+ areas, err := s.forums.Areas(forumReader(callerOf(c)))
+ if err != nil {
+ return nil, err
+ }
+ lines := make([]string, 0, len(areas))
+ for _, a := range areas {
+ lines = append(lines, fmt.Sprintf("%-12s %4d topics %4d new %s", a.Tag, a.Topics, a.Unread, a.Name))
+ }
+ if areas == nil {
+ areas = []forum.Area{}
+ }
+
+ return &dispatch.Result{Data: areas, Lines: lines}, nil
+ },
+ },
+ {
+ Name: "forum.topics",
+ Summary: "the topics in an area, newest activity first",
+ Args: []dispatch.Arg{
+ {Name: "area", Kind: dispatch.KindString, Required: true, Max: 32, Help: "area tag, see forum.areas"},
+ {Name: "page", Kind: dispatch.KindInt, Min: 1, Max: 10000, Help: "page of 20 (default 1)"},
+ },
+ Free: true,
+ Run: func(_ context.Context, c *dispatch.Call) (*dispatch.Result, error) {
+ reader := forumReader(callerOf(c))
+ a, err := s.forums.Area(c.Str("area"), reader)
+ if errors.Is(err, forum.ErrNotFound) {
+ return nil, dispatch.Errf(dispatch.CodeNotFound, "no area %q", c.Str("area"))
+ }
+ if err != nil {
+ return nil, err
+ }
+ page := max(c.Int("page"), 1)
+ topics, total, err := s.forums.Topics(a.ID, reader, (page-1)*20, 20)
+ if err != nil {
+ return nil, err
+ }
+ lines := make([]string, 0, len(topics))
+ for _, t := range topics {
+ lines = append(lines, fmt.Sprintf("%6d %4s %-16s %3d posts %s", t.ID, ago(time.Since(t.LastPostAt)), t.Nick, t.Posts, t.Title))
+ }
+ if topics == nil {
+ topics = []forum.Topic{}
+ }
+
+ return &dispatch.Result{Data: map[string]any{"area": a.Tag, "page": page, "total": total, "topics": topics}, Lines: lines}, nil
+ },
+ },
+ {
+ Name: "forum.read",
+ Summary: "the posts in a topic, oldest first",
+ Args: []dispatch.Arg{
+ {Name: "topic", Kind: dispatch.KindInt, Required: true, Min: 1, Help: "topic id, see forum.topics"},
+ {Name: "page", Kind: dispatch.KindInt, Min: 1, Max: 10000, Help: "page of 20 (default 1)"},
+ },
+ Free: true,
+ Run: func(_ context.Context, c *dispatch.Call) (*dispatch.Result, error) {
+ reader := forumReader(callerOf(c))
+ t, err := s.forums.Topic(int64(c.Int("topic")), reader)
+ if errors.Is(err, forum.ErrNotFound) {
+ return nil, dispatch.Errf(dispatch.CodeNotFound, "no topic %d", c.Int("topic"))
+ }
+ if err != nil {
+ return nil, err
+ }
+ page := max(c.Int("page"), 1)
+ posts, total, err := s.forums.Posts(t.ID, (page-1)*20, 20)
+ if err != nil {
+ return nil, err
+ }
+ if len(posts) > 0 {
+ _ = s.forums.MarkRead(reader, t.ID, posts[len(posts)-1].ID)
+ }
+ lines := []string{t.Title, ""}
+ for _, p := range posts {
+ lines = append(lines, fmt.Sprintf("— %s, %s", p.Nick, p.Created.Format("2006-01-02 15:04")))
+ lines = append(lines, strings.Split(p.Body, "\n")...)
+ lines = append(lines, "")
+ }
+ if posts == nil {
+ posts = []forum.Post{}
+ }
+
+ return &dispatch.Result{Data: map[string]any{"topic": t, "page": page, "total": total, "posts": posts}, Lines: lines}, nil
+ },
+ },
+ {
+ Name: "forum.post",
+ Summary: "start a topic (area + title) or reply (topic); the post is input=, file= or stdin; needs an SSH key",
+ Args: []dispatch.Arg{
+ {Name: "area", Kind: dispatch.KindString, Max: 32, Help: "area tag, to start a topic"},
+ {Name: "title", Kind: dispatch.KindString, Max: 200, Help: "title of the new topic"},
+ {Name: "topic", Kind: dispatch.KindInt, Min: 1, Help: "topic id, to reply"},
+ },
+ Input: true,
+ Run: func(_ context.Context, c *dispatch.Call) (*dispatch.Result, error) {
+ return s.forumPost(c)
},
},
{
@@ -29,228 +29,258 @@ }
// bbsEN is the English fallback when no localizer is wired (tests, early boot).
var bbsEN = map[string]string{
- "BBS_CONTINUE": "press any key to continue...",
- "BBS_MORE": "── more ── (any key: next · q: back)",
- "BBS_PASTE_END": "end with a line containing only . or EOF · Ctrl+C cancels",
- "BBS_WEATHER": "weather",
- "BBS_SETTINGS": "settings",
- "BBS_ABOUT": "about",
- "BBS_CHOOSE": "choose",
- "BBS_QUIT": "quit",
- "BBS_BACK": "back",
- "BBS_CHANGE": "change",
- "BBS_NOT_SET": "(not set)",
- "BBS_UNKNOWN_MAIN": "unknown key — try %s, w, c, s, a, m or q",
- "BBS_SEE_YOU": "⚡ hammer — see you",
- "BBS_TAGLINE": "tools over SSH",
- "BBS_NOW": "now",
- "BBS_SEARCH_CITY": "search city",
- "BBS_MY_CITIES": "my cities",
- "BBS_TITLE_WEATHER": "WEATHER",
- "BBS_TITLE_SEARCH": "SEARCH CITY",
- "BBS_TITLE_CITIES": "MY CITIES",
- "BBS_TITLE_SETTINGS": "SETTINGS",
- "BBS_UNKNOWN_WX": "unknown key — try 1-3, b or q",
- "BBS_NO_DEFAULT": "No default city set. Use [2] to search and save one.",
- "BBS_FETCHING": "fetching weather for %s...",
- "BBS_NO_WEATHER": "Could not get weather for %s.",
- "BBS_SEARCH_HINT": " type a city name — empty or b to go back",
- "BBS_CITY_NAME": "city name: ",
- "BBS_LOOKUP": "looking up %s...",
- "BBS_NOT_FOUND": "%s not found or unavailable. Try again.",
- "BBS_ALREADY_SAVED": "%s is already in your cities",
- "BBS_SAVE_CITY": "Save this city?",
- "BBS_SET_DEFAULT": "Set as default?",
- "BBS_YN": "[y/N]",
- "BBS_SAVED_DEFAULT": "Saved %s and set as default",
- "BBS_SAVED": "Saved %s",
- "BBS_CITIES_PROMPT": "cities> ",
- "BBS_NO_CITY_NUM": "no city #%s — use the numbers in the list",
- "BBS_NO_CITIES": "(no saved cities yet — use search to add one)",
- "BBS_HINT_VIEW": "view",
- "BBS_HINT_DEFAULT": "default",
- "BBS_HINT_REMOVE": "remove",
- "BBS_STAR_DEFAULT": "★ = default city",
- "BBS_USAGE_DEFAULT": "usage: d2 sets city #2 as default",
- "BBS_USAGE_REMOVE": "usage: r2 removes city #2",
- "BBS_CONFIRM_REMOVE": "Remove %s?",
- "BBS_REMOVED": "Removed %s",
- "BBS_REMOVED_DEF": "Removed %s (default cleared)",
- "BBS_NOW_DEFAULT": "%s is now your default",
- "BBS_UNITS": "units",
- "BBS_LANGUAGE": "language",
- "BBS_VIEW": "view",
- "BBS_EMOJI": "emoji",
- "BBS_UNKNOWN_SET": "unknown key — try 1-8 or b",
- "BBS_CURRENT": " current ",
- "BBS_LANG_CODE": "language code: ",
- "BBS_UNKNOWN_LANG": "unknown language %s — try en, pt, de, es, fr...",
- "BBS_LANG_SET": "Language set to %s",
- "BBS_UNITS_METRIC": "metric (°C, km/h)",
- "BBS_UNITS_IMPERIAL": "imperial (°F, mph)",
- "BBS_UNITS_MS": "metric (°C, m/s wind)",
- "BBS_VIEW_CARD": "card (emoji)",
- "BBS_VIEW_V1": "classic (v1)",
- "BBS_VIEW_V2": "graphs (v2)",
- "BBS_ON": "on",
- "BBS_OFF": "off",
- "BBS_ABOUT_BLURB": "An interactive SSH front-end for tools.",
- "BBS_VERSION": "version",
- "BBS_YOUR_KEY": "your key",
- "BBS_ANONYMOUS": "anonymous (no key)",
- "BBS_ANON_NOTE": "anonymous session — settings last until you disconnect; connect with an SSH key to keep them",
- "BBS_TIP_WIDE": "(tip: maximize your terminal for the full layout)",
- "BBS_PROMPT_WX": "weather> ",
- "BBS_PROMPT_SET": "settings> ",
- "TOOL_NET": "net",
- "TOOL_NET_SUM": "DNS, TLS, HTTP, RDAP",
- "TOOL_QR": "qr",
- "TOOL_QR_SUM": "URL, phone, text",
- "TOOL_HASH": "hash",
- "TOOL_HASH_SUM": "SHA, entropy, type",
- "TOOL_JQ": "jq",
- "TOOL_JQ_SUM": "JSON/YAML transform",
- "TOOL_AGE": "age",
- "TOOL_AGE_SUM": "encrypt / decrypt",
- "TOOL_LINKS": "links",
- "TOOL_LINKS_SUM": "broken-link check",
- "TOOL_DATA": "data",
- "TOOL_DATA_SUM": "DuckDB queries",
- "TOOL_PATH": "path",
- "TOOL_PATH_SUM": "ping / MTR",
- "BBS_MORE_TOOLS": "more",
- "BBS_MORE_TOOLS_SUM": "csv, base64, uuid",
- "BBS_MORE_PROMPT": "more> ",
- "BBS_UNKNOWN_MORE": "unknown key — try %s or b",
- "BBS_CSV": "csv",
- "BBS_CSV_SUM": "rows, header, counts",
- "BBS_PASTE_CSV": "paste CSV",
- "BBS_B64": "base64",
- "BBS_B64_SUM": "encode / decode",
- "BBS_B64_ENCODE": "encode",
- "BBS_B64_DECODE": "decode",
- "BBS_B64_PROMPT": "b64> ",
- "BBS_PASTE_B64": "paste Base64",
- "BBS_IDS": "ids",
- "BBS_IDS_SUM": "UUID v4",
- "BBS_HOST_URL": "host or URL: ",
- "BBS_PROBING": "probing %s...",
- "BBS_TEXT": "text: ",
- "BBS_PASTE_BYTES": "paste bytes to inspect",
- "BBS_PASTE_JSON": "paste JSON or YAML",
- "BBS_JQ_FILTER": "jq filter (empty = .): ",
- "BBS_INPUT": "input",
- "BBS_ENCRYPT": "encrypt",
- "BBS_DECRYPT": "decrypt",
- "BBS_GENERATE": "generate",
- "BBS_ENCRYPT_SUM": "plaintext + recipient",
- "BBS_DECRYPT_SUM": "ciphertext + identity",
- "BBS_GENERATE_SUM": "keypair in memory",
- "BBS_PASTE_PLAIN": "paste plaintext",
- "BBS_RECIPIENT": "recipient (age1...): ",
- "BBS_PASTE_CIPHER": "paste ciphertext",
- "BBS_IDENTITY": "identity (AGE-SECRET-KEY-...): ",
- "BBS_RCPT_SHARE": "recipient (share this)",
- "BBS_ID_PRIVATE": "identity (private — shown once, not saved)",
- "BBS_NO_LYCHEE": "lychee is not installed on this host",
- "BBS_PASTE_URL": "paste a URL or markdown",
- "BBS_CHECKING": "checking links...",
- "BBS_NO_DUCKDB": "duckdb is not installed, or datasetsDir is missing",
- "BBS_RUN": "run",
- "BBS_NO_QUERY": "no query #%s",
- "BBS_RUNNING": "running %s...",
- "BBS_NO_PING": "neither ping nor mtr is installed on this host",
- "BBS_HOST": "host: ",
- "BBS_QUERY_PROMPT": "query> ",
- "BBS_AGE_PROMPT": "age> ",
- "BBS_QR_PROMPT": "qr> ",
- "BBS_QR_TEXT": "text",
- "BBS_QR_TEXT_SUM": "any payload",
- "BBS_QR_URL": "url",
- "BBS_QR_URL_SUM": "opens in the camera browser",
- "BBS_QR_PHONE": "phone",
- "BBS_QR_PHONE_SUM": "tel: link for the dialer",
- "BBS_QR_URL_PROMPT": "url: ",
- "BBS_QR_PHONE_PROMPT": "phone: ",
- "BBS_NICK": "nickname",
- "BBS_NICK_PROMPT": "new nickname (2-16: letters, digits, _ . -): ",
- "BBS_NICK_SET": "You are now %s",
- "BBS_NICK_TAKEN": "%s already belongs to someone else",
- "BBS_NICK_FORMAT": "Nicknames are 2-16 characters: letters, digits, _ . -",
- "BBS_NICK_RESERVED": "%s is reserved; pick another",
- "BBS_NICK_NEED_KEY": "Nicknames need an SSH key; anonymous sessions get a guest name",
- "BBS_WALL": "wall",
- "BBS_WALL_SUM": "one-liners",
- "BBS_CHAT": "chat",
- "BBS_CHAT_SUM": "live room",
- "BBS_ONLINE": "%d online",
- "BBS_TITLE_WALL": "WALL",
- "BBS_TITLE_WHO": "ONLINE NOW",
- "BBS_WALL_EMPTY": "(the wall is empty — be the first)",
- "BBS_WALL_POST": "post",
- "BBS_WALL_WHO": "who's online",
- "BBS_WALL_DELETE": "delete #n",
- "BBS_WALL_PROMPT": "wall> ",
- "BBS_WALL_WRITE": "your line (up to %d characters): ",
- "BBS_WALL_POSTED": "Posted to the wall",
- "BBS_WALL_COOLDOWN": "You can post again in %s",
- "BBS_WALL_NEED_KEY": "Posting needs an SSH key — anonymous sessions can only read",
- "BBS_WALL_DELETED": "Deleted",
- "BBS_WALL_NO_LINE": "no line #%s",
- "BBS_UNKNOWN_WALL": "unknown option — try p, o or b",
- "BBS_BANNED": "You are banned from posting",
- "BBS_WHERE_MENU": "menu",
- "BBS_WHERE_CHAT": "in the chat",
- "BBS_GAMES": "games",
- "BBS_GAMES_SUM": "NES in sixel",
- "BBS_TITLE_GAMES": "GAMES",
- "BBS_GAMES_INTRO1": "Homebrew NES games, drawn in sixel graphics.",
- "BBS_GAMES_INTRO2": "Needs a sixel terminal: SyncTERM, IcyTerm, WezTerm, foot, iTerm2.",
- "BBS_GAMES_INTRO3": "Sound and held keys work in SyncTERM and IcyTerm.",
- "BBS_GAMES_INTRO4": "Up to %d minutes a sitting; leaving saves your game.",
- "BBS_GAMES_PLAY": "play",
- "BBS_GAMES_ANON": "Games need an SSH key — connect with one to play",
- "BBS_GAMES_FULL": "All %d game slots are taken — try again soon",
- "BBS_GAMES_LATE": "This connection is about to time out — reconnect to play",
- "BBS_GAMES_EMPTY": "No games on the shelf yet — check back soon",
- "BBS_GAMES_FAILED": "The games could not start — try again later",
- "BBS_WHERE_GAME": "playing",
- "BBS_ANON_TAG": "anonymous",
- "BBS_CONNS": "%d connections",
- "BBS_CHAT_TITLE": "CHAT · #lobby",
- "BBS_CHAT_HINT": "/help for commands · /quit to leave",
- "BBS_CHAT_READONLY": "read only — connect with an SSH key to talk",
- "BBS_CHAT_JOIN": "→ %s joined",
- "BBS_CHAT_LEAVE": "← %s left",
- "BBS_CHAT_NICK": "%s is now %s",
- "BBS_CHAT_KICK": "%s was removed by %s",
- "BBS_CHAT_MUTE": "%s was muted for %s",
- "BBS_CHAT_BAN": "%s was banned by %s",
- "BBS_CHAT_KICKED": "You were removed from the room — press any key",
- "BBS_CHAT_MUTED": "You are muted for %s",
- "BBS_CHAT_SLOW": "Slow down — try again in %s",
- "BBS_CHAT_WHO": "In the room (%d): %s",
- "BBS_CHAT_NOT_ADMIN": "That command is for moderators",
- "BBS_CHAT_NO_NICK": "Nobody called %s is in the room",
- "BBS_CHAT_UNKNOWN_CMD": "Unknown command %s — try /help",
- "BBS_CHAT_UNBANNED": "%s can post again",
- "BBS_CHAT_NOT_BANNED": "%s was not banned",
- "BBS_CHAT_USAGE": "usage: %s",
- "BBS_CHAT_HELP_ME": "/me action describe what you are doing",
- "BBS_CHAT_HELP_NICK": "/nick name change your nickname",
- "BBS_CHAT_HELP_WHO": "/who who is in the room",
- "BBS_CHAT_HELP_QUIT": "/quit back to the menu (Ctrl+C too)",
- "BBS_CHAT_HELP_MOD": "/mute nick [10m] · /kick nick · /ban nick [reason] · /unban nick",
- "BBS_THEME": "theme",
- "BBS_THEME_RETRO90": "retro 90s",
- "BBS_THEME_MODERN": "retro-modern",
- "BBS_THEME_PLAIN": "plain",
- "BBS_FX": "modem effect",
- "BBS_CALLERS": "last callers",
- "BBS_CALLERS_SUM": "who came by",
- "BBS_TITLE_CALLERS": "LAST CALLERS",
- "BBS_CALLERS_EMPTY": "(nobody yet)",
- "BBS_NOTICES": "arrivals",
- "BBS_NOTICE_ARRIVED": "→ %s arrived",
- "BBS_NOTICE_LEFT": "← %s left",
+ "BBS_CONTINUE": "press any key to continue...",
+ "BBS_MORE": "── more ── (any key: next · q: back)",
+ "BBS_PASTE_END": "end with a line containing only . or EOF · Ctrl+C cancels",
+ "BBS_WEATHER": "weather",
+ "BBS_SETTINGS": "settings",
+ "BBS_ABOUT": "about",
+ "BBS_CHOOSE": "choose",
+ "BBS_QUIT": "quit",
+ "BBS_BACK": "back",
+ "BBS_CHANGE": "change",
+ "BBS_NOT_SET": "(not set)",
+ "BBS_UNKNOWN_MAIN": "unknown key — try %s, w, c, s, a, m or q",
+ "BBS_SEE_YOU": "⚡ hammer — see you",
+ "BBS_TAGLINE": "tools over SSH",
+ "BBS_NOW": "now",
+ "BBS_SEARCH_CITY": "search city",
+ "BBS_MY_CITIES": "my cities",
+ "BBS_TITLE_WEATHER": "WEATHER",
+ "BBS_TITLE_SEARCH": "SEARCH CITY",
+ "BBS_TITLE_CITIES": "MY CITIES",
+ "BBS_TITLE_SETTINGS": "SETTINGS",
+ "BBS_UNKNOWN_WX": "unknown key — try 1-3, b or q",
+ "BBS_NO_DEFAULT": "No default city set. Use [2] to search and save one.",
+ "BBS_FETCHING": "fetching weather for %s...",
+ "BBS_NO_WEATHER": "Could not get weather for %s.",
+ "BBS_SEARCH_HINT": " type a city name — empty or b to go back",
+ "BBS_CITY_NAME": "city name: ",
+ "BBS_LOOKUP": "looking up %s...",
+ "BBS_NOT_FOUND": "%s not found or unavailable. Try again.",
+ "BBS_ALREADY_SAVED": "%s is already in your cities",
+ "BBS_SAVE_CITY": "Save this city?",
+ "BBS_SET_DEFAULT": "Set as default?",
+ "BBS_YN": "[y/N]",
+ "BBS_SAVED_DEFAULT": "Saved %s and set as default",
+ "BBS_SAVED": "Saved %s",
+ "BBS_CITIES_PROMPT": "cities> ",
+ "BBS_NO_CITY_NUM": "no city #%s — use the numbers in the list",
+ "BBS_NO_CITIES": "(no saved cities yet — use search to add one)",
+ "BBS_HINT_VIEW": "view",
+ "BBS_HINT_DEFAULT": "default",
+ "BBS_HINT_REMOVE": "remove",
+ "BBS_STAR_DEFAULT": "★ = default city",
+ "BBS_USAGE_DEFAULT": "usage: d2 sets city #2 as default",
+ "BBS_USAGE_REMOVE": "usage: r2 removes city #2",
+ "BBS_CONFIRM_REMOVE": "Remove %s?",
+ "BBS_REMOVED": "Removed %s",
+ "BBS_REMOVED_DEF": "Removed %s (default cleared)",
+ "BBS_NOW_DEFAULT": "%s is now your default",
+ "BBS_UNITS": "units",
+ "BBS_LANGUAGE": "language",
+ "BBS_VIEW": "view",
+ "BBS_EMOJI": "emoji",
+ "BBS_UNKNOWN_SET": "unknown key — try 1-8 or b",
+ "BBS_CURRENT": " current ",
+ "BBS_LANG_CODE": "language code: ",
+ "BBS_UNKNOWN_LANG": "unknown language %s — try en, pt, de, es, fr...",
+ "BBS_LANG_SET": "Language set to %s",
+ "BBS_UNITS_METRIC": "metric (°C, km/h)",
+ "BBS_UNITS_IMPERIAL": "imperial (°F, mph)",
+ "BBS_UNITS_MS": "metric (°C, m/s wind)",
+ "BBS_VIEW_CARD": "card (emoji)",
+ "BBS_VIEW_V1": "classic (v1)",
+ "BBS_VIEW_V2": "graphs (v2)",
+ "BBS_ON": "on",
+ "BBS_OFF": "off",
+ "BBS_ABOUT_BLURB": "An interactive SSH front-end for tools.",
+ "BBS_VERSION": "version",
+ "BBS_YOUR_KEY": "your key",
+ "BBS_ANONYMOUS": "anonymous (no key)",
+ "BBS_ANON_NOTE": "anonymous session — settings last until you disconnect; connect with an SSH key to keep them",
+ "BBS_TIP_WIDE": "(tip: maximize your terminal for the full layout)",
+ "BBS_PROMPT_WX": "weather> ",
+ "BBS_PROMPT_SET": "settings> ",
+ "TOOL_NET": "net",
+ "TOOL_NET_SUM": "DNS, TLS, HTTP, RDAP",
+ "TOOL_QR": "qr",
+ "TOOL_QR_SUM": "URL, phone, text",
+ "TOOL_HASH": "hash",
+ "TOOL_HASH_SUM": "SHA, entropy, type",
+ "TOOL_JQ": "jq",
+ "TOOL_JQ_SUM": "JSON/YAML transform",
+ "TOOL_AGE": "age",
+ "TOOL_AGE_SUM": "encrypt / decrypt",
+ "TOOL_LINKS": "links",
+ "TOOL_LINKS_SUM": "broken-link check",
+ "TOOL_DATA": "data",
+ "TOOL_DATA_SUM": "DuckDB queries",
+ "TOOL_PATH": "path",
+ "TOOL_PATH_SUM": "ping / MTR",
+ "BBS_MORE_TOOLS": "more",
+ "BBS_MORE_TOOLS_SUM": "csv, base64, uuid",
+ "BBS_MORE_PROMPT": "more> ",
+ "BBS_UNKNOWN_MORE": "unknown key — try %s or b",
+ "BBS_CSV": "csv",
+ "BBS_CSV_SUM": "rows, header, counts",
+ "BBS_PASTE_CSV": "paste CSV",
+ "BBS_B64": "base64",
+ "BBS_B64_SUM": "encode / decode",
+ "BBS_B64_ENCODE": "encode",
+ "BBS_B64_DECODE": "decode",
+ "BBS_B64_PROMPT": "b64> ",
+ "BBS_PASTE_B64": "paste Base64",
+ "BBS_IDS": "ids",
+ "BBS_IDS_SUM": "UUID v4",
+ "BBS_HOST_URL": "host or URL: ",
+ "BBS_PROBING": "probing %s...",
+ "BBS_TEXT": "text: ",
+ "BBS_PASTE_BYTES": "paste bytes to inspect",
+ "BBS_PASTE_JSON": "paste JSON or YAML",
+ "BBS_JQ_FILTER": "jq filter (empty = .): ",
+ "BBS_INPUT": "input",
+ "BBS_ENCRYPT": "encrypt",
+ "BBS_DECRYPT": "decrypt",
+ "BBS_GENERATE": "generate",
+ "BBS_ENCRYPT_SUM": "plaintext + recipient",
+ "BBS_DECRYPT_SUM": "ciphertext + identity",
+ "BBS_GENERATE_SUM": "keypair in memory",
+ "BBS_PASTE_PLAIN": "paste plaintext",
+ "BBS_RECIPIENT": "recipient (age1...): ",
+ "BBS_PASTE_CIPHER": "paste ciphertext",
+ "BBS_IDENTITY": "identity (AGE-SECRET-KEY-...): ",
+ "BBS_RCPT_SHARE": "recipient (share this)",
+ "BBS_ID_PRIVATE": "identity (private — shown once, not saved)",
+ "BBS_NO_LYCHEE": "lychee is not installed on this host",
+ "BBS_PASTE_URL": "paste a URL or markdown",
+ "BBS_CHECKING": "checking links...",
+ "BBS_NO_DUCKDB": "duckdb is not installed, or datasetsDir is missing",
+ "BBS_RUN": "run",
+ "BBS_NO_QUERY": "no query #%s",
+ "BBS_RUNNING": "running %s...",
+ "BBS_NO_PING": "neither ping nor mtr is installed on this host",
+ "BBS_HOST": "host: ",
+ "BBS_QUERY_PROMPT": "query> ",
+ "BBS_AGE_PROMPT": "age> ",
+ "BBS_QR_PROMPT": "qr> ",
+ "BBS_QR_TEXT": "text",
+ "BBS_QR_TEXT_SUM": "any payload",
+ "BBS_QR_URL": "url",
+ "BBS_QR_URL_SUM": "opens in the camera browser",
+ "BBS_QR_PHONE": "phone",
+ "BBS_QR_PHONE_SUM": "tel: link for the dialer",
+ "BBS_QR_URL_PROMPT": "url: ",
+ "BBS_QR_PHONE_PROMPT": "phone: ",
+ "BBS_NICK": "nickname",
+ "BBS_NICK_PROMPT": "new nickname (2-16: letters, digits, _ . -): ",
+ "BBS_NICK_SET": "You are now %s",
+ "BBS_NICK_TAKEN": "%s already belongs to someone else",
+ "BBS_NICK_FORMAT": "Nicknames are 2-16 characters: letters, digits, _ . -",
+ "BBS_NICK_RESERVED": "%s is reserved; pick another",
+ "BBS_NICK_NEED_KEY": "Nicknames need an SSH key; anonymous sessions get a guest name",
+ "BBS_WALL": "wall",
+ "BBS_WALL_SUM": "one-liners",
+ "BBS_CHAT": "chat",
+ "BBS_CHAT_SUM": "live room",
+ "BBS_ONLINE": "%d online",
+ "BBS_TITLE_WALL": "WALL",
+ "BBS_TITLE_WHO": "ONLINE NOW",
+ "BBS_WALL_EMPTY": "(the wall is empty — be the first)",
+ "BBS_WALL_POST": "post",
+ "BBS_WALL_WHO": "who's online",
+ "BBS_WALL_DELETE": "delete #n",
+ "BBS_WALL_PROMPT": "wall> ",
+ "BBS_WALL_WRITE": "your line (up to %d characters): ",
+ "BBS_WALL_POSTED": "Posted to the wall",
+ "BBS_WALL_COOLDOWN": "You can post again in %s",
+ "BBS_WALL_NEED_KEY": "Posting needs an SSH key — anonymous sessions can only read",
+ "BBS_WALL_DELETED": "Deleted",
+ "BBS_WALL_NO_LINE": "no line #%s",
+ "BBS_UNKNOWN_WALL": "unknown option — try p, o or b",
+ "BBS_BANNED": "You are banned from posting",
+ "BBS_WHERE_MENU": "menu",
+ "BBS_WHERE_CHAT": "in the chat",
+ "BBS_GAMES": "games",
+ "BBS_GAMES_SUM": "NES in sixel",
+ "BBS_TITLE_GAMES": "GAMES",
+ "BBS_GAMES_INTRO1": "Homebrew NES games, drawn in sixel graphics.",
+ "BBS_GAMES_INTRO2": "Needs a sixel terminal: SyncTERM, IcyTerm, WezTerm, foot, iTerm2.",
+ "BBS_GAMES_INTRO3": "Sound and held keys work in SyncTERM and IcyTerm.",
+ "BBS_GAMES_INTRO4": "Up to %d minutes a sitting; leaving saves your game.",
+ "BBS_GAMES_PLAY": "play",
+ "BBS_GAMES_ANON": "Games need an SSH key — connect with one to play",
+ "BBS_GAMES_FULL": "All %d game slots are taken — try again soon",
+ "BBS_GAMES_LATE": "This connection is about to time out — reconnect to play",
+ "BBS_GAMES_EMPTY": "No games on the shelf yet — check back soon",
+ "BBS_GAMES_FAILED": "The games could not start — try again later",
+ "BBS_WHERE_GAME": "playing",
+ "BBS_ANON_TAG": "anonymous",
+ "BBS_CONNS": "%d connections",
+ "BBS_CHAT_TITLE": "CHAT · #lobby",
+ "BBS_CHAT_HINT": "/help for commands · /quit to leave",
+ "BBS_CHAT_READONLY": "read only — connect with an SSH key to talk",
+ "BBS_CHAT_JOIN": "→ %s joined",
+ "BBS_CHAT_LEAVE": "← %s left",
+ "BBS_CHAT_NICK": "%s is now %s",
+ "BBS_CHAT_KICK": "%s was removed by %s",
+ "BBS_CHAT_MUTE": "%s was muted for %s",
+ "BBS_CHAT_BAN": "%s was banned by %s",
+ "BBS_CHAT_KICKED": "You were removed from the room — press any key",
+ "BBS_CHAT_MUTED": "You are muted for %s",
+ "BBS_CHAT_SLOW": "Slow down — try again in %s",
+ "BBS_CHAT_WHO": "In the room (%d): %s",
+ "BBS_CHAT_NOT_ADMIN": "That command is for moderators",
+ "BBS_CHAT_NO_NICK": "Nobody called %s is in the room",
+ "BBS_CHAT_UNKNOWN_CMD": "Unknown command %s — try /help",
+ "BBS_CHAT_UNBANNED": "%s can post again",
+ "BBS_CHAT_NOT_BANNED": "%s was not banned",
+ "BBS_CHAT_USAGE": "usage: %s",
+ "BBS_CHAT_HELP_ME": "/me action describe what you are doing",
+ "BBS_CHAT_HELP_NICK": "/nick name change your nickname",
+ "BBS_CHAT_HELP_WHO": "/who who is in the room",
+ "BBS_CHAT_HELP_QUIT": "/quit back to the menu (Ctrl+C too)",
+ "BBS_CHAT_HELP_MOD": "/mute nick [10m] · /kick nick · /ban nick [reason] · /unban nick",
+ "BBS_THEME": "theme",
+ "BBS_THEME_RETRO90": "retro 90s",
+ "BBS_THEME_MODERN": "retro-modern",
+ "BBS_THEME_PLAIN": "plain",
+ "BBS_FX": "modem effect",
+ "BBS_CALLERS": "last callers",
+ "BBS_CALLERS_SUM": "who came by",
+ "BBS_TITLE_CALLERS": "LAST CALLERS",
+ "BBS_CALLERS_EMPTY": "(nobody yet)",
+ "BBS_NOTICES": "arrivals",
+ "BBS_NOTICE_ARRIVED": "→ %s arrived",
+ "BBS_NOTICE_LEFT": "← %s left",
+ "BBS_FORUMS": "forums",
+ "BBS_FORUMS_SUM": "message areas",
+ "BBS_TITLE_FORUMS": "FORUMS",
+ "BBS_FORUM_TOPICS": "%d topics",
+ "BBS_FORUM_NEW": "%d new",
+ "BBS_FORUM_NO_TOPICS": "(no topics yet — start one with t)",
+ "BBS_FORUM_PAGE": "page %d of %d · %d topics",
+ "BBS_FORUM_READ": "read",
+ "BBS_FORUM_NEW_TOPIC": "new topic",
+ "BBS_FORUM_PAGES": "pages",
+ "BBS_FORUM_MOD": "pin/lock/delete",
+ "BBS_FORUM_REPLY": "reply",
+ "BBS_FORUM_MORE": "next posts",
+ "BBS_FORUM_EARLIER": "earlier posts",
+ "BBS_FORUM_POSTS_RANGE": "posts %d–%d of %d",
+ "BBS_FORUM_LOCKED": "This topic is locked",
+ "BBS_FORUM_READONLY": "Only moderators post in this area",
+ "BBS_FORUM_GONE": "That topic is gone",
+ "BBS_FORUM_UNKNOWN": "unknown option — type a number, or b to go back",
+ "BBS_FORUM_ERROR": "The forums are unavailable right now",
+ "BBS_FORUM_NEED_KEY": "Posting needs an SSH key — keyless visitors can only read",
+ "BBS_FORUM_TITLE": "title: ",
+ "BBS_FORUM_WRITE": "write your post (up to %d bytes)",
+ "BBS_FORUM_POSTED": "Posted",
+ "BBS_FORUM_CANCELLED": "Nothing posted",
+ "BBS_FORUM_TOO_LONG": "That post is too long",
+ "BBS_FORUM_COOLDOWN": "You can post again in %s",
+ "BBS_FORUM_DONE": "Done",
+ "BBS_FORUM_CONFIRM_DELETE": "Delete the topic \"%s\" and all its posts?",
+ "BBS_FORUM_CONFIRM_POST": "Delete post #%d?",
}