From 1db945f98b96ec503897ddd274844e0dcec85760 Mon Sep 17 00:00:00 2001 From: mbays Date: Fri, 19 Mar 2021 00:00:00 +0000 Subject: [PATCH] add output format options --- main.c | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 15 deletions(-) diff --git a/main.c b/main.c index e02fdcb..ea36e80 100644 --- a/main.c +++ b/main.c @@ -26,9 +26,17 @@ typedef struct Child { bool nolink; } Child; +typedef enum output_format + { gemtext + , pre + , unwrapped + , raw +} output_format; + typedef struct State { const char *command; char *const *args; + output_format format; int max_children; int read_timeout; @@ -99,14 +107,22 @@ static void set_child_last_active(Child *child) child->last_active = clock_mono.tv_sec; } -/* Write anything written timelily on `in` to `out`, converting \n to \r\n. - * If `gemtext` is true, also space-stuff lines beginning with "```". +/* Write anything written timelily on `in` to `out`, + * optionally converting \n to \r\n and space-stuffing gemini-magic lines. * Return -1 on read error, 0 on HUP, else 1. */ -static int stream_text(int in, int out, bool gemtext, int read_timeout, int pause_timeout) { +static int stream_text(int in, int out, + bool convert_newlines, + bool escape_pre, + bool escape_all, + int read_timeout, int pause_timeout) { char buf[256]; struct pollfd pfd = { in, POLLIN | POLLHUP, 0 }; int backticks = 0; + char escape = 0; + // TODO: timeouts should really be based on cpu time of child process, not + // wall clock time. This is particularly important for raw output, where + // an unfortunately timed sleep could lead to invalid responses. poll(&pfd, 1, read_timeout); while (pfd.revents & POLLIN) { const int r = read(in, buf, 256 - 1); @@ -115,27 +131,50 @@ static int stream_text(int in, int out, bool gemtext, int read_timeout, int paus const char *b = buf; while (*b) { - if (gemtext && backticks >= 0) { + if ((escape_pre || escape_all) && backticks >= 0) { if (*b == '`') { + escape = 0; ++backticks; if (backticks == 3) { write(out, " ```", 4); - backticks = 0; + backticks = -1; } ++b; continue; } else while (--backticks >= 0) write(out, "`", 1); } - if (*b == '\n') { + if (escape_all && escape > 0) { + if (escape == '\n') { + if (*b == '#' || *b == '>') { + write(out, " ", 1); + } else if (*b == '=' || *b == '*') { + escape = *b; + ++b; + continue; + } + } else { + if ((escape == '=' && *b == '>') + || (escape == '*' && *b == ' ')) { + write(out, " ", 1); + } + write(out, &escape, 1); + } + escape = 0; + } + + if (convert_newlines && *b == '\n') { write(out, "\r\n", 2); backticks = 0; + escape = '\n'; } else write(out, b, 1); ++b; } poll(&pfd, 1, pause_timeout); } + while (--backticks >= 0) write(out, "`", 1); + if (escape > 0 && escape != '\n') write(out, &escape, 1); return (!(pfd.revents & POLLHUP)); } @@ -251,12 +290,14 @@ void respond(void *object, const Request_Info *request_info, int socket) } } - put("20 text/gemini\r\n"); - if (spawned) { - put("[gemrepl child spawned. Input \"!help\" for meta-commands]\r\n"); - } + if (state->format != raw) { + put("20 text/gemini\r\n"); + if (spawned) { + put("[gemrepl child spawned. Input \"!help\" for meta-commands]\r\n"); + } - if (!child->nolink) put("=> ?!? Input command\r\n"); + if (!child->nolink) put("=> ?!? Input command\r\n"); + } if (!spawned) kill(child->pid, SIGCONT); @@ -276,9 +317,14 @@ void respond(void *object, const Request_Info *request_info, int socket) } } - put("```\r\n"); - const int succ = stream_text(child->out, socket, true, state->read_timeout, state->pause_timeout); - put("\r\n```\r\n"); + if (state->format == pre) put("```\r\n"); + const int succ = stream_text(child->out, socket, + state->format != raw, + state->format == pre, + state->format == unwrapped, + state->read_timeout, state->pause_timeout); + if (state->format == pre) put("\r\n```\r\n"); + if (succ < 0) put("[gemrepl: error when reading from child]\r\n"); else if (succ == 0) { // got HUP; sleep briefly to give child a chance to exit @@ -309,6 +355,11 @@ static void usage() printf("Usage: gemrepl [OPTION]... -s PATH COMMAND [ARG]...\n"); printf(" -h --help This help\n"); printf(" -s PATH --socket=PATH Path for socket file, which will be created\n"); + printf(" -f FMT --format=FMT Format of output of command. Possible formats:\n"); + printf(" gemtext: text/gemini (default)\n"); + printf(" pre: preformatted text\n"); + printf(" unwrapped: plain text without hard wrapping\n"); + printf(" raw: gemini protocol output, including response headers\n"); printf(" -m NUM --max-children=NUM Max concurrent children to spawn (%d)\n", MAX_CHILDREN); printf(" -t MS --read-timeout=MS Time to wait for child to start writing (%d)\n", DEF_READ_TIMEOUT); printf(" -T MS --pause-timeout=MS Silence period after which child is paused (%d)\n", DEF_PAUSE_TIMEOUT); @@ -331,18 +382,21 @@ int main(int argc, char **argv) state->max_children = MAX_CHILDREN; state->read_timeout = DEF_READ_TIMEOUT; state->pause_timeout = DEF_PAUSE_TIMEOUT; + state->format = gemtext; const struct option longoptions[] = { { "help", 0, NULL, 'h' } , { "socket", 1, NULL, 's' } + , { "format", 1, NULL, 'f' } , { "max-children", 1, NULL, 'm' } , { "read-timeout", 1, NULL, 't' } , { "pause-timeout", 1, NULL, 'T' } + // TODO: option to not convert newlines , { 0,0,0,0 } }; int o; const char *socketname = NULL; - while (-1 != (o = getopt_long(argc, argv, "+hs:m:t:T:", longoptions, NULL))) { + while (-1 != (o = getopt_long(argc, argv, "+hs:f:m:t:T:", longoptions, NULL))) { switch (o) { case 'h': case '?': @@ -351,6 +405,16 @@ int main(int argc, char **argv) case 's': socketname = optarg; break; + case 'f': + if (0 == strcmp(optarg, "gemtext")) state->format=gemtext; + else if (0 == strcmp(optarg, "pre")) state->format=pre; + else if (0 == strcmp(optarg, "unwrapped")) state->format=unwrapped; + else if (0 == strcmp(optarg, "raw")) state->format=raw; + else { + printf("Unknown format.\n"); + exit(1); + } + break; case 'm': state->max_children = atoi(optarg); if (state->max_children <= 0 || state->max_children > MAX_CHILDREN) { -- 2.11.4.GIT