diff --git a/Makefile b/Makefile index 94d0b78..e4f379b 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \ CFLAGS += -I$(SYSEXITS) .PHONY: all -all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true +all: dj false fop hru intcmp mm npc rpn scroll scrut str strcmp swab true # keep build/include until bindgen(1) has stdin support # https://github.com/rust-lang/rust-bindgen/issues/2703 @@ -120,7 +120,12 @@ build/bin/mm: src/mm.rs build rustlibs .PHONY: npc npc: build/bin/npc build/bin/npc: src/npc.c build - $(CC) $(CFLAGAS) -o $@ src/npc.c + $(CC) $(CFLAGS) -o $@ src/npc.c + +.PHONY: scroll +scroll: build/bin/scroll +build/bin/scroll: src/scroll.c build + $(CC) $(CFLAGS) -o $@ src/scroll.c .PHONY: rpn rpn: build/bin/rpn diff --git a/docs/scroll.1 b/docs/scroll.1 new file mode 100644 index 0000000..3fd9e8e --- /dev/null +++ b/docs/scroll.1 @@ -0,0 +1,52 @@ +.\" Copyright (c) 2024 DTB +.\" Copyright (c) 2024 Emma Tebibyte +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . +.\" +.TH PG 1 2024-07-31 "Harakit X.X.X" +.SH NAME +scroll \(en present output +.\" +.SH SYNOPSIS + +scroll +.RB ( -p +.RB [ prompt ]) +.\" +.SH DESCRIPTION + +Print standard input to standard output, accepting commands between pages. +.\" +.SH OPTIONS + +.IP -p +Replace the default prompt (\(lq: \(rq) with the option argument. +.\" +.SH DIAGNOSTICS + +In the event of an error, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. +.\" +.SH RATIONALE + +Plan 9 from Bell Labs had +.BR p (1), +a similar \(lqcooked\(rq-mode paginator (as opposed to \(lqraw\(rq mode, which a +vast majority of paginators use). +.\" +.SH AUTHOR + +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" +.SH COPYRIGHT + +Copyright \(co 2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +. +.\" +.SH SEE ALSO +.BR more (1p) diff --git a/src/scroll.c b/src/scroll.c new file mode 100644 index 0000000..14c8da1 --- /dev/null +++ b/src/scroll.c @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2024 DTB + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +#include /* assert(3) */ +#include /* errno */ +#include /* bool */ +#include /* fclose(3), feof(3), fgetc(3), fgets(3), fopen(3), + * fprintf(3), fputc(3), perror(3), stderr, stdin, stdout, + * EOF, FILE, NULL */ +#include /* size_t */ +#include /* strchr(3), strcmp(3) */ +#include /* getopt(3) */ + +/* Commands start with cmd_. They take an argc and NULL-terminated argv, like + * main, and return a status from . Return values other than EX_OK + * and EX_USAGE cause pg(1) to exit with that value, except EX_UNAVAILABLE, + * which causes pg(1) to exit with the status EX_OK. */ +#include + +#define CMDLINE_MAX 99+1 /* Maximum length of command line. */ + +static char *whitespace = " \n\r\t\v"; + +//static +struct Tube { + char *name; // command line + FILE *in; // process stdin + FILE *out; // process stdoout + size_t index; // in pipeline +}; + +static struct { + size_t quantity; + enum { LINES = 0, BYTES = 1 } type; +} default_page_unit = { 22, LINES } /* Plan 9 default */; + +static char *prompt = ": "; +static char *program_name = "pg"; + +static char * +permute_out(char *s, size_t i) { + for ( + ; + s[i] != '\0'; + s[i] = s[i + 1], ++i + ); + + return s; +} + +/* strtok(3p), but supports double-quotes and escapes (but only for escaping + * quotes). Unmatched quotes in str are considered literal. The behavior of + * strtok_quoted when '\'', '"', or '\\' are in sep is undefined. Use of UTF-8 + * separators with strtok_quoted is undefined. */ +static char * +strtok_quoted(char *str, char *sep) { + static char *s; + + if (str != NULL) { s = str; } + + while (strchr(sep, *s) != NULL) { // skip beginning whitespace + if(*++s == '\0') { return NULL; } // no remaining except seps + } + + { + bool in_escape = 0; // previous char was '\\' + char quote = '\0'; // quotation mark used, or '\0' if none + + for (int i = 0; s[i] != '\0'; ++i) + switch (s[i]) { + case '\\': + // if literal "\\", permute out a backslash + if (in_escape) { (void)permute_out(s, i--); } + in_escape = !in_escape; + break; + case '\'': case '"': + if (in_escape) { // \" + s[i] = s[i - 1]; + (void)permute_out(s, i--); // permute out backslash + } else if (s[i] == quote) { + quote = '\0'; + (void)permute_out(s, i--); // second quote + } else { + quote = s[i]; + if (strchr(&s[i + 1], quote) != NULL) { // has a match + (void)permute_out(s, i--); // permute out lquote + } + } + break; + case '\0': return s; + default: + if (!in_escape + && quote == '\0' + && (strchr(sep, s[i]) != NULL || s[i] == '\0')) { + char *t; // start of current token + + t = s; + s = s[i] != '\0' + ? &t[i + 1] // store start of next token, + : &t[i]; // or the address of the nul if found + s[i] = '\0'; // NUL terminate current token + + return t; + } + } + } + + return s; +} + +/* Page at most l bytes from f without consideration of a buffer (print them to + * stdout). */ +static int +pg_b_u(FILE *f, size_t l) { + int c; + + while ((c = fgetc(f)) != EOF) { + if ((c = fputc(c, stdout)) == EOF || --l == 0) { break; } + } + + return c; +} + +/* Page at most l lines, which are lengths of characters terminated by nl, from + * f without consideration of a buffer (print them to stdout). */ +static int +pg_l_u(FILE *f, size_t l, char nl) { + int c; + + while ((c = fgetc(f)) != EOF) { + if ((c = fputc(c, stdout)) == EOF || (l -= (c == nl)) == 0) { break; } + } + + return c; +} + +static int +cmd_quit(int argc, char **argv, char **envp) { return EX_UNAVAILABLE; } + +static int +cmd_default_page(int argc, char **argv, char **envp) { + if (argc > 1) { return EX_USAGE; } // should be impossible + else if (default_page_unit.type == BYTES) { + return pg_b_u(stdin, default_page_unit.quantity) == EOF + ? EX_UNAVAILABLE + : EX_OK; + } else if (default_page_unit.type == LINES) { + return pg_l_u(stdin, default_page_unit.quantity, '\n') == EOF + ? EX_UNAVAILABLE + : EX_OK; + } else { return EX_SOFTWARE; } +} + +static int +cmd_page_down_lines(int argc, char **argv, char **envp) { + switch (argc) { + case 1: return cmd_default_page(argc, argv, envp); + case 2: /* not implemented */ + default: + (void)fprintf(stderr, "Usage: %s" /*" (lines)"*/ "\n", argv[0]); + return EX_USAGE; + } +} + +/* A CmdMap must be NULL-terminated. */ +static struct CmdMap{ char *name; int (*fn)(int, char **, char **); } +builtins[] = { + /* don't make the user feel trapped */ + { ":q", cmd_quit }, { ":q!", cmd_quit }, + { "exit", cmd_quit }, { "q", cmd_quit }, { "Q", cmd_quit }, + { "quit", cmd_quit }, { "ZZ", cmd_quit }, + + { NULL, NULL } +}; + +#define ARGV_MAX 10 +/* Find and execute the command in the command map, given a corresponding + * command line. */ +static int +cmdline_exec(struct CmdMap *map, char *cmdline, char **envp) { + static int argc; + static char *argv[ARGV_MAX]; + + if ((argv[(argc = 0)] = strtok_quoted(cmdline, whitespace)) == NULL) { + while (cmdline[0] != '\0') { cmdline = &cmdline[1]; } + argv[argc] = cmdline; + argv[++argc] = NULL; + } else { + while ( + (argv[++argc] = strtok_quoted(NULL, whitespace)) != NULL + && argc < ARGV_MAX + ); + } + + for (; map->name != NULL; map = &map[1]) { + if(strcmp(map->name, argv[0]) == 0) { + return map->fn(argc, argv, envp); + } + } + + (void)fprintf(stderr, "%s: %s: not found\n", program_name, argv[0]); + return EX_USAGE; +} + +static int +ioerr(char *argv0) { + perror(argv0); + + return EX_IOERR; +} + +static int +usage(char *argv0) { + (void)fprintf(stderr, "Usage: %s [-p prompt]\n", argv0); + + return EX_USAGE; +} + +int main(int argc, char *argv[]) { + unsigned char cmd[CMDLINE_MAX]; + FILE *t; + + if (argc > 0) { + int c; + + program_name = argv[0]; + + while ((c = getopt(argc, argv, "p:")) != -1) { + switch (c) { + case 'p': prompt = optarg; break; + default: return usage(program_name); + } + } + } + + if (argc > optind) { return usage(program_name); } + + + if ((t = fopen("/dev/tty", "rb")) == NULL) { + perror(program_name); + + return EX_OSERR; + } + + for (;;) { + if (fputs(prompt, stderr) == EOF) { return ioerr(program_name); } + + // if the line... + if (fgets((char *)cmd, (sizeof cmd) / (sizeof *cmd), t) != NULL) { + if (strchr((char *)cmd, '\n') == NULL) { // was taken incompletely + int c; + + while ((c = fgetc(t)) != '\n') { // ...fast-forward stream + if (c == EOF) { break; } + } + } + } else { fputc('\n', stdout); } // EOF at start of line; finish prompt + + if (feof(t)) { return EX_OK; } + + { + int r; + + switch ((r = cmdline_exec(builtins, (char *)cmd, NULL))){ + case EX_OK: case EX_USAGE: break; + case EX_UNAVAILABLE: return EX_OK; + default: return r; + } + } + } + + /* UNREACHABLE */ assert(0); +}