Files
MontaukOS/programs/src/wiki/main.cpp
T

950 lines
31 KiB
C++

/*
* main.cpp
* Wikipedia client for MontaukOS (TLS 1.2 via BearSSL)
* Interactive fullscreen pager with colored output
* Usage: wiki <title> Show article summary
* wiki -f <title> Show full article
* wiki -s <query> Search for articles
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <tls/tls.hpp>
extern "C" {
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
}
using montauk::skip_spaces;
static constexpr int RESP_MAX = 131072; // 128 KB
static const char WIKI_HOST[] = "en.wikipedia.org";
static constexpr int MAX_LINES = 4096;
static constexpr int MAX_SEARCH_RESULTS = 10;
enum Mode { MODE_SUMMARY, MODE_FULL, MODE_SEARCH, MODE_DUMP };
enum LineType { LINE_BLANK, LINE_TITLE, LINE_DESC, LINE_SECTION, LINE_BODY };
struct WikiLine {
const char* text;
int len;
LineType type;
int level;
};
// ---- Global state (loaded once, reused across fetches) ----
static uint32_t g_serverIp = 0;
static tls::TrustAnchors g_tas = {nullptr, 0, 0};
// ---- Screen buffer for flicker-free rendering ----
static constexpr int SB_SIZE = 32768;
static char g_sb[SB_SIZE];
static int g_sbPos = 0;
static void sb_reset() { g_sbPos = 0; }
static void sb_putc(char c) { if (g_sbPos < SB_SIZE - 1) g_sb[g_sbPos++] = c; }
static void sb_puts(const char* s) { while (*s && g_sbPos < SB_SIZE - 1) g_sb[g_sbPos++] = *s++; }
static void sb_flush() { g_sb[g_sbPos] = '\0'; montauk::print(g_sb); }
static int int_to_buf(char* buf, int n) {
if (n <= 0) { buf[0] = '0'; return 1; }
char tmp[12]; int i = 0;
while (n > 0) { tmp[i++] = '0' + (n % 10); n /= 10; }
for (int j = 0; j < i; j++) buf[j] = tmp[i - 1 - j];
return i;
}
static void sb_cursor_to(int row, int col) {
sb_puts("\033[");
char tmp[12]; int n = int_to_buf(tmp, row);
for (int i = 0; i < n; i++) sb_putc(tmp[i]);
sb_putc(';');
n = int_to_buf(tmp, col);
for (int i = 0; i < n; i++) sb_putc(tmp[i]);
sb_putc('H');
}
// ---- Keyboard abort check for TLS ----
static bool check_keyboard_abort() {
if (montauk::is_key_available()) {
montauk::abi::KeyEvent ev;
montauk::getkey(&ev);
if (ev.pressed && ev.ctrl && ev.ascii == 'q') return true;
}
return false;
}
// ---- HTTPS fetch wrapper ----
static int wiki_fetch(const char* path, char* respBuf, int respMax) {
static char request[2560]; // keep off stack
int reqLen = snprintf(request, sizeof(request),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"User-Agent: MontaukOS/1.0 wiki\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n",
path, WIKI_HOST);
return tls::https_fetch(WIKI_HOST, g_serverIp, 443,
request, reqLen, g_tas,
respBuf, respMax, check_keyboard_abort);
}
// ---- HTTP response parsing ----
static int find_header_end(const char* buf, int len) {
for (int i = 0; i + 3 < len; i++) {
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n')
return i + 4;
}
return -1;
}
static int parse_status_code(const char* buf, int len) {
int i = 0;
while (i < len && buf[i] != ' ') i++;
if (i >= len) return -1;
i++;
if (i + 2 >= len) return -1;
if (buf[i] < '0' || buf[i] > '9') return -1;
return (buf[i] - '0') * 100 + (buf[i+1] - '0') * 10 + (buf[i+2] - '0');
}
// ---- URL encoding ----
static int url_encode_title(const char* in, char* out, int maxLen) {
const char hex[] = "0123456789ABCDEF";
int j = 0;
for (int i = 0; in[i] && j < maxLen - 4; i++) {
char c = in[i];
if (c == ' ') {
out[j++] = '_';
} else if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' ||
c == '.' || c == '~' || c == '(' || c == ')' || c == ',') {
out[j++] = c;
} else {
out[j++] = '%';
out[j++] = hex[(unsigned char)c >> 4];
out[j++] = hex[(unsigned char)c & 0x0F];
}
}
out[j] = '\0';
return j;
}
static int url_encode_query(const char* in, char* out, int maxLen) {
const char hex[] = "0123456789ABCDEF";
int j = 0;
for (int i = 0; in[i] && j < maxLen - 4; i++) {
char c = in[i];
if (c == ' ') {
out[j++] = '+';
} else if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' ||
c == '.' || c == '~') {
out[j++] = c;
} else {
out[j++] = '%';
out[j++] = hex[(unsigned char)c >> 4];
out[j++] = hex[(unsigned char)c & 0x0F];
}
}
out[j] = '\0';
return j;
}
// ---- JSON string extraction ----
static int extract_json_string(const char* buf, int len, const char* key,
char* out, int maxOut) {
int klen = (int)strlen(key);
for (int i = 0; i < len - klen - 3; i++) {
if (buf[i] != '"') continue;
if (memcmp(buf + i + 1, key, klen) != 0) continue;
if (buf[i + 1 + klen] != '"') continue;
if (buf[i + 2 + klen] != ':') continue;
int p = i + 3 + klen;
while (p < len && (buf[p] == ' ' || buf[p] == '\t')) p++;
if (p >= len || buf[p] != '"') continue;
p++;
int j = 0;
while (p < len && j < maxOut - 4) {
if (buf[p] == '"') break;
if (buf[p] == '\\' && p + 1 < len) {
p++;
switch (buf[p]) {
case '"': out[j++] = '"'; break;
case '\\': out[j++] = '\\'; break;
case 'n': out[j++] = '\n'; break;
case 'r': break;
case 't': out[j++] = '\t'; break;
case '/': out[j++] = '/'; break;
case 'u': {
if (p + 4 < len) {
unsigned val = 0;
for (int k = 1; k <= 4; k++) {
char h = buf[p + k];
val <<= 4;
if (h >= '0' && h <= '9') val |= h - '0';
else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
}
p += 4;
if (val < 128) out[j++] = (char)val;
else if (val == 0x2013 || val == 0x2014) out[j++] = '-';
else if (val == 0x2018 || val == 0x2019) out[j++] = '\'';
else if (val == 0x201C || val == 0x201D) out[j++] = '"';
else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; }
else out[j++] = '?';
}
break;
}
default: out[j++] = buf[p]; break;
}
} else {
out[j++] = buf[p];
}
p++;
}
out[j] = '\0';
return j;
}
out[0] = '\0';
return 0;
}
// ---- Display line building (word wrap + section detection) ----
// Word-wrap a single paragraph into the lines array. Returns lines added.
static int wrap_paragraph(const char* text, int textLen, int cols,
WikiLine* lines, int maxLines, LineType type) {
if (maxLines <= 0 || textLen <= 0 || cols <= 0) return 0;
int count = 0;
const char* p = text;
const char* end = text + textLen;
while (p < end && count < maxLines) {
while (p < end && *p == ' ') p++;
if (p >= end) break;
const char* lineStart = p;
const char* lastSpace = nullptr;
int col = 0;
while (p < end && col < cols) {
if (*p == ' ') lastSpace = p;
p++;
col++;
}
if (p >= end) {
// Rest fits on one line
lines[count].text = lineStart;
lines[count].len = (int)(p - lineStart);
lines[count].type = type;
lines[count].level = 0;
count++;
} else if (lastSpace && lastSpace > lineStart) {
// Wrap at last space
lines[count].text = lineStart;
lines[count].len = (int)(lastSpace - lineStart);
lines[count].type = type;
lines[count].level = 0;
count++;
p = lastSpace + 1;
} else {
// Force break (word longer than cols)
lines[count].text = lineStart;
lines[count].len = (int)(p - lineStart);
lines[count].type = type;
lines[count].level = 0;
count++;
}
}
return count;
}
// Build all display lines from article parts. Returns total line count.
static int build_lines(WikiLine* lines, int maxLines,
const char* title, const char* description,
const char* extract, int extractLen,
int cols, bool fullMode) {
int n = 0;
// Title
if (title && title[0] && n < maxLines) {
n += wrap_paragraph(title, (int)strlen(title), cols,
lines + n, maxLines - n, LINE_TITLE);
}
// Description
if (description && description[0] && n < maxLines) {
n += wrap_paragraph(description, (int)strlen(description), cols,
lines + n, maxLines - n, LINE_DESC);
}
// Blank separator
if (n > 0 && n < maxLines) {
lines[n].text = "";
lines[n].len = 0;
lines[n].type = LINE_BLANK;
lines[n].level = 0;
n++;
}
// Process extract line by line
const char* p = extract;
const char* end = extract + extractLen;
while (p < end && n < maxLines) {
const char* lineStart = p;
while (p < end && *p != '\n') p++;
int lineLen = (int)(p - lineStart);
if (p < end) p++; // skip \n
// Blank line
if (lineLen == 0) {
lines[n].text = "";
lines[n].len = 0;
lines[n].type = LINE_BLANK;
lines[n].level = 0;
n++;
continue;
}
// Section header detection (== Title ==)
if (fullMode && lineLen >= 4 && lineStart[0] == '=' && lineStart[1] == '=') {
int level = 0;
int si = 0;
while (si < lineLen && lineStart[si] == '=') { level++; si++; }
while (si < lineLen && lineStart[si] == ' ') si++;
int ei = lineLen;
while (ei > si && lineStart[ei - 1] == '=') ei--;
while (ei > si && lineStart[ei - 1] == ' ') ei--;
// Blank line before section for spacing
if (n > 0 && n < maxLines) {
lines[n].text = "";
lines[n].len = 0;
lines[n].type = LINE_BLANK;
lines[n].level = 0;
n++;
}
if (n < maxLines) {
lines[n].text = lineStart + si;
lines[n].len = ei - si;
lines[n].type = LINE_SECTION;
lines[n].level = level;
n++;
}
continue;
}
// Regular text — word wrap
n += wrap_paragraph(lineStart, lineLen, cols,
lines + n, maxLines - n, LINE_BODY);
}
return n;
}
// ---- Pager rendering ----
static void render_pager(WikiLine* lines, int totalLines, int scroll,
int rows, int cols, const char* statusTitle,
const char* modeLabel) {
int contentRows = rows - 1;
sb_reset();
sb_puts("\033[?25l");
for (int r = 0; r < contentRows; r++) {
sb_cursor_to(r + 1, 1);
sb_puts("\033[2K");
int idx = scroll + r;
if (idx < 0 || idx >= totalLines) continue;
WikiLine& ln = lines[idx];
if (ln.type == LINE_BLANK) continue;
switch (ln.type) {
case LINE_TITLE:
sb_puts("\033[1;36m"); // bold cyan
break;
case LINE_DESC:
sb_puts("\033[33m"); // yellow
break;
case LINE_SECTION: {
int indent = (ln.level <= 2) ? 0 : (ln.level - 2) * 2;
for (int i = 0; i < indent; i++) sb_putc(' ');
sb_puts("\033[1;32m"); // bold green
break;
}
default:
break;
}
int maxW = cols;
if (ln.type == LINE_SECTION && ln.level > 2)
maxW -= (ln.level - 2) * 2;
int printLen = ln.len;
if (printLen > maxW) printLen = maxW;
for (int c = 0; c < printLen; c++)
sb_putc(ln.text[c]);
if (ln.type != LINE_BODY)
sb_puts("\033[0m");
}
// Status bar
sb_cursor_to(rows, 1);
sb_puts("\033[7m");
int visCol = 0;
// Helper: write string and track visible columns
#define SB_STATUS(s) do { const char* _s = (s); while (*_s) { sb_putc(*_s++); visCol++; } } while(0)
SB_STATUS(" Wikipedia ");
sb_puts("\033[7;33m"); // yellow reverse
SB_STATUS(modeLabel);
sb_puts("\033[7;37m"); // white reverse
SB_STATUS(" | ");
// Truncated title
int maxTitleLen = cols / 3;
int titleLen = (int)strlen(statusTitle);
if (titleLen > maxTitleLen && maxTitleLen > 3) {
for (int i = 0; i < maxTitleLen - 3; i++) { sb_putc(statusTitle[i]); visCol++; }
SB_STATUS("...");
} else {
SB_STATUS(statusTitle);
}
SB_STATUS(" | ");
char numStr[32];
int nLen = snprintf(numStr, sizeof(numStr), "%d/%d", scroll + 1, totalLines);
for (int i = 0; i < nLen; i++) { sb_putc(numStr[i]); visCol++; }
SB_STATUS(" | q:Quit j/k:Scroll Space/b:Page ");
#undef SB_STATUS
// Pad to fill row
while (visCol < cols) { sb_putc(' '); visCol++; }
sb_puts("\033[0m");
sb_flush();
}
static void run_pager(WikiLine* lines, int totalLines, const char* title,
const char* modeLabel, bool useAltScreen) {
int cols = 80, rows = 25;
montauk::termsize(&cols, &rows);
if (useAltScreen) {
montauk::print("\033[?1049h");
montauk::print("\033[?25l");
}
int scroll = 0;
int maxScroll = totalLines - (rows - 1);
if (maxScroll < 0) maxScroll = 0;
render_pager(lines, totalLines, scroll, rows, cols, title, modeLabel);
bool running = true;
while (running) {
while (!montauk::is_key_available()) montauk::yield();
montauk::abi::KeyEvent ev;
montauk::getkey(&ev);
if (!ev.pressed) continue;
int contentRows = rows - 1;
if (ev.ascii == 'q' || (ev.ctrl && ev.ascii == 'q')) {
running = false;
break;
}
switch (ev.ascii) {
case 'j': if (scroll < maxScroll) scroll++; break;
case 'k': if (scroll > 0) scroll--; break;
case ' ': scroll += contentRows; if (scroll > maxScroll) scroll = maxScroll; break;
case 'b': scroll -= contentRows; if (scroll < 0) scroll = 0; break;
case 'g': scroll = 0; break;
case 'G': scroll = maxScroll; break;
default:
switch (ev.scancode) {
case 0x48: if (scroll > 0) scroll--; break; // Up
case 0x50: if (scroll < maxScroll) scroll++; break; // Down
case 0x49: scroll -= contentRows; if (scroll < 0) scroll = 0; break; // PgUp
case 0x51: scroll += contentRows; if (scroll > maxScroll) scroll = maxScroll; break; // PgDn
case 0x47: scroll = 0; break; // Home
case 0x4F: scroll = maxScroll; break; // End
}
break;
}
if (running)
render_pager(lines, totalLines, scroll, rows, cols, title, modeLabel);
}
if (useAltScreen) {
montauk::print("\033[?25h");
montauk::print("\033[?1049l");
}
}
// ---- Search results ----
static int parse_search_titles(const char* body, int bodyLen,
char titles[][256], int maxResults) {
int brackets = 0;
int start = -1;
for (int i = 0; i < bodyLen; i++) {
if (body[i] == '[') {
brackets++;
if (brackets == 2) { start = i + 1; break; }
}
}
if (start < 0) return 0;
int count = 0;
int i = start;
while (i < bodyLen && body[i] != ']' && count < maxResults) {
while (i < bodyLen && (body[i] == ' ' || body[i] == ',' ||
body[i] == '\n' || body[i] == '\r')) i++;
if (i >= bodyLen || body[i] == ']') break;
if (body[i] == '"') {
i++;
int j = 0;
while (i < bodyLen && body[i] != '"' && j < 255) {
if (body[i] == '\\' && i + 1 < bodyLen) {
i++;
titles[count][j++] = body[i];
} else {
titles[count][j++] = body[i];
}
i++;
}
titles[count][j] = '\0';
if (i < bodyLen) i++;
count++;
} else {
i++;
}
}
return count;
}
static void render_search(char titles[][256], int count, const char* query,
int rows, int cols) {
sb_reset();
sb_puts("\033[?25l");
sb_puts("\033[2J"); // clear screen
// Header
sb_cursor_to(2, 3);
sb_puts("\033[1;36mWikipedia\033[0m");
sb_puts("\033[90m - The Free Encyclopedia\033[0m");
// Search query
sb_cursor_to(4, 3);
sb_puts("\033[1mSearch results for \"\033[33m");
sb_puts(query);
sb_puts("\033[0;1m\":\033[0m");
// Results
int resultRow = 6;
for (int i = 0; i < count; i++) {
sb_cursor_to(resultRow + i, 3);
sb_puts("\033[1;36m");
char num[8];
snprintf(num, sizeof(num), "%2d", i + 1);
sb_puts(num);
sb_puts("\033[0m \033[1;37m");
// Truncate long titles
int tLen = (int)strlen(titles[i]);
int maxT = cols - 10;
if (tLen > maxT && maxT > 3) {
for (int c = 0; c < maxT - 3; c++) sb_putc(titles[i][c]);
sb_puts("...");
} else {
sb_puts(titles[i]);
}
sb_puts("\033[0m");
}
if (count == 0) {
sb_cursor_to(resultRow, 3);
sb_puts("\033[33m(no results found)\033[0m");
}
// Instructions
sb_cursor_to(resultRow + count + 2, 3);
sb_puts("\033[90mPress ");
if (count > 0) {
sb_puts("\033[0;1m1");
if (count > 1) {
sb_putc('-');
if (count >= 10) sb_putc('0');
else sb_putc('0' + count);
}
sb_puts("\033[0;90m to view article, ");
}
sb_puts("\033[0;1mq\033[0;90m to quit\033[0m");
// Status bar
sb_cursor_to(rows, 1);
sb_puts("\033[7m");
char statusStr[256];
snprintf(statusStr, sizeof(statusStr),
" Wikipedia Search | \"%s\" | %d result%s ",
query, count, count == 1 ? "" : "s");
sb_puts(statusStr);
int sLen = (int)strlen(statusStr);
for (int i = sLen; i < cols; i++) sb_putc(' ');
sb_puts("\033[0m");
sb_flush();
}
// Returns selected index (0-based), or -1 for quit
static int run_search(char titles[][256], int count, const char* query,
int rows, int cols) {
render_search(titles, count, query, rows, cols);
while (true) {
while (!montauk::is_key_available()) montauk::yield();
montauk::abi::KeyEvent ev;
montauk::getkey(&ev);
if (!ev.pressed) continue;
if (ev.ascii == 'q' || (ev.ctrl && ev.ascii == 'q'))
return -1;
int sel = -1;
if (ev.ascii >= '1' && ev.ascii <= '9') sel = ev.ascii - '1';
else if (ev.ascii == '0') sel = 9;
if (sel >= 0 && sel < count) return sel;
}
}
// ---- Entry point ----
extern "C" void _start() {
static char argbuf[1024]; // keep off stack (16KB limit)
montauk::getargs(argbuf, sizeof(argbuf));
const char* arg = skip_spaces(argbuf);
if (*arg == '\0') {
montauk::print("\033[1;36mwiki\033[0m - Wikipedia article viewer\n\n");
montauk::print("Usage: wiki <title> Show article summary\n");
montauk::print(" wiki -f <title> Show full article\n");
montauk::print(" wiki -s <query> Search for articles\n");
montauk::print("\nExamples:\n");
montauk::print(" wiki Linux\n");
montauk::print(" wiki -f C programming language\n");
montauk::print(" wiki -s operating system\n");
montauk::exit(0);
}
// Parse mode flag
Mode mode = MODE_SUMMARY;
if (arg[0] == '-' && arg[1] == 'f' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_FULL;
arg = skip_spaces(arg + 2);
} else if (arg[0] == '-' && arg[1] == 's' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_SEARCH;
arg = skip_spaces(arg + 2);
} else if (arg[0] == '-' && arg[1] == 'd' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_DUMP;
arg = skip_spaces(arg + 2);
}
if (*arg == '\0') {
montauk::print("\033[1;31mError:\033[0m no article title or search query\n");
montauk::exit(1);
}
// Trim trailing spaces from query
static char query[512];
int qlen = 0;
while (arg[qlen] && qlen < 511) { query[qlen] = arg[qlen]; qlen++; }
while (qlen > 0 && query[qlen - 1] == ' ') qlen--;
query[qlen] = '\0';
// Initialize: resolve DNS and load certs
if (mode != MODE_DUMP)
montauk::print("\033[1;33mConnecting to Wikipedia...\033[0m\n");
g_serverIp = montauk::resolve(WIKI_HOST);
if (g_serverIp == 0) {
if (mode == MODE_DUMP) { montauk::print("\x01"); montauk::sleep_ms(100); montauk::exit(1); }
montauk::print("\033[1;31mError:\033[0m could not resolve en.wikipedia.org\n");
montauk::exit(1);
}
g_tas = tls::load_trust_anchors();
if (g_tas.count == 0) {
if (mode == MODE_DUMP) { montauk::print("\x01"); montauk::sleep_ms(100); montauk::exit(1); }
montauk::print("\033[1;31mError:\033[0m no CA certificates loaded\n");
montauk::exit(1);
}
// Allocate shared buffers
char* respBuf = (char*)malloc(RESP_MAX);
WikiLine* lines = (WikiLine*)malloc(MAX_LINES * sizeof(WikiLine));
char* extractBuf = (char*)malloc(RESP_MAX);
if (!respBuf || !lines || !extractBuf) {
montauk::print("\033[1;31mError:\033[0m out of memory\n");
montauk::exit(1);
}
if (mode == MODE_DUMP) {
// ---- Dump mode: output raw JSON body for desktop GUI ----
static char path[2048], encoded[1024];
url_encode_title(query, encoded, sizeof(encoded));
snprintf(path, sizeof(path),
"/w/api.php?action=query&format=json&formatversion=2"
"&prop=extracts&explaintext=1&titles=%s", encoded);
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
montauk::print("\x01"); // error sentinel
montauk::sleep_ms(100);
montauk::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("\x01");
montauk::sleep_ms(100);
montauk::exit(1);
}
int statusCode = parse_status_code(respBuf, headerEnd);
if (statusCode == 404) {
montauk::print("\x01");
montauk::sleep_ms(100);
montauk::exit(1);
}
// Output raw JSON body in chunks to avoid overflowing
// the 4KB kernel ring buffer (parent polls at ~60fps)
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
static char chunk[2049];
int sent = 0;
while (sent < bodyLen) {
int n = bodyLen - sent;
if (n > 2048) n = 2048;
for (int ci = 0; ci < n; ci++) chunk[ci] = body[sent + ci];
chunk[n] = '\0';
montauk::print(chunk);
sent += n;
if (sent < bodyLen) montauk::sleep_ms(20);
}
montauk::putchar('\x04'); // EOT sentinel
// Brief delay so parent can drain the ring buffer before we exit
montauk::sleep_ms(100);
montauk::exit(0);
} else if (mode == MODE_SEARCH) {
// ---- Search mode ----
static char path[2048], encoded[1024];
url_encode_query(query, encoded, sizeof(encoded));
snprintf(path, sizeof(path),
"/w/api.php?action=opensearch&search=%s&limit=10&format=json",
encoded);
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
montauk::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("\033[1;31mError:\033[0m malformed response\n");
montauk::exit(1);
}
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
static char titles[MAX_SEARCH_RESULTS][256];
int titleCount = parse_search_titles(body, bodyLen, titles, MAX_SEARCH_RESULTS);
if (titleCount == 0) {
montauk::print("\033[33mNo results found for \"");
montauk::print(query);
montauk::print("\"\033[0m\n");
montauk::exit(0);
}
int cols = 80, rows = 25;
montauk::termsize(&cols, &rows);
// Enter alternate screen for interactive search
montauk::print("\033[?1049h");
montauk::print("\033[?25l");
bool searchRunning = true;
while (searchRunning) {
int sel = run_search(titles, titleCount, query, rows, cols);
if (sel < 0) {
searchRunning = false;
break;
}
// Show loading message on search screen
sb_reset();
int infoRow = 6 + titleCount + 2;
sb_cursor_to(infoRow, 3);
sb_puts("\033[2K\033[1;33mFetching \"");
sb_puts(titles[sel]);
sb_puts("\"...\033[0m");
sb_flush();
// Fetch selected article summary
static char articlePath[2048], articleEncoded[1024];
url_encode_title(titles[sel], articleEncoded, sizeof(articleEncoded));
snprintf(articlePath, sizeof(articlePath),
"/api/rest_v1/page/summary/%s", articleEncoded);
respLen = wiki_fetch(articlePath, respBuf, RESP_MAX);
if (respLen <= 0) {
sb_reset();
sb_cursor_to(infoRow, 3);
sb_puts("\033[2K\033[1;31mFetch failed. Press any key.\033[0m");
sb_flush();
while (!montauk::is_key_available()) montauk::yield();
montauk::abi::KeyEvent ev; montauk::getkey(&ev);
continue;
}
respBuf[respLen] = '\0';
headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) continue;
int statusCode = parse_status_code(respBuf, headerEnd);
body = respBuf + headerEnd;
bodyLen = respLen - headerEnd;
if (statusCode == 404) {
sb_reset();
sb_cursor_to(infoRow, 3);
sb_puts("\033[2K\033[1;31mArticle not found. Press any key.\033[0m");
sb_flush();
while (!montauk::is_key_available()) montauk::yield();
montauk::abi::KeyEvent ev; montauk::getkey(&ev);
continue;
}
static char title[512], description[512];
extract_json_string(body, bodyLen, "title", title, sizeof(title));
extract_json_string(body, bodyLen, "description", description, sizeof(description));
int extractLen = extract_json_string(body, bodyLen, "extract",
extractBuf, RESP_MAX - 1);
if (extractLen > 0) {
int totalLines = build_lines(lines, MAX_LINES,
title, description, extractBuf, extractLen, cols, false);
// Run pager without alt screen (we're already in one)
run_pager(lines, totalLines, title, "Summary", false);
}
// Loop back to re-render search results
}
// Exit alternate screen
montauk::print("\033[?25h");
montauk::print("\033[?1049l");
} else {
// ---- Summary or Full Article mode ----
static char path[2048], encoded[1024];
if (mode == MODE_SUMMARY) {
url_encode_title(query, encoded, sizeof(encoded));
snprintf(path, sizeof(path),
"/api/rest_v1/page/summary/%s", encoded);
} else {
url_encode_title(query, encoded, sizeof(encoded));
snprintf(path, sizeof(path),
"/w/api.php?action=query&format=json&formatversion=2"
"&prop=extracts&explaintext=1&titles=%s", encoded);
}
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
montauk::print("\033[1;31mError:\033[0m no response from Wikipedia\n");
montauk::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
montauk::print("\033[1;31mError:\033[0m malformed response\n");
montauk::exit(1);
}
int statusCode = parse_status_code(respBuf, headerEnd);
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
if (statusCode == 404) {
montauk::print("\033[1;31mArticle not found:\033[0m ");
montauk::print(query);
montauk::putchar('\n');
montauk::exit(1);
}
static char title[512], description[512];
extract_json_string(body, bodyLen, "title", title, sizeof(title));
if (mode == MODE_SUMMARY)
extract_json_string(body, bodyLen, "description", description, sizeof(description));
else
description[0] = '\0';
int extractLen = extract_json_string(body, bodyLen, "extract",
extractBuf, RESP_MAX - 1);
if (extractLen == 0) {
montauk::print("\033[1;31mArticle not found:\033[0m ");
montauk::print(query);
montauk::putchar('\n');
montauk::exit(1);
}
int cols = 80;
montauk::termsize(&cols, nullptr);
int totalLines = build_lines(lines, MAX_LINES,
title, description, extractBuf, extractLen,
cols, mode == MODE_FULL);
const char* modeLabel = (mode == MODE_FULL) ? "Full Article" : "Summary";
run_pager(lines, totalLines, title, modeLabel, true);
}
montauk::exit(0);
}