git.y1.nz

bro

URI-based multiplexer
download: http://git.y1.nz/archives/bro.tar.gz
README | Files | Log | Refs

commit 5a74e9adf3c23687c419de2dfdaca0e63d9afbab
Author: Emma Weaver <emma@y1.nz>
Date:   Sat, 18 Jul 2026 20:00:16 -0500

Minimum functional rewrite in C

Diffstat:
A.gitignore1+
AMakefile16++++++++++++++++
AREADME27+++++++++++++++++++++++++++
ATODO38++++++++++++++++++++++++++++++++++++++
Aconfig.h7+++++++
Aconfig.mk4++++
Amain.c215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 308 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +bro diff --git a/Makefile b/Makefile @@ -0,0 +1,16 @@ +include config.mk + +all: bro + +bro: main.c config.h + $(CC) main.c -lokra -o bro + +install: bro + cp -f bro $(PREFIX)/bin + echo "man page too" + +uninstall: + rm -f $(PREFIX)/bin/bro + +clean: + rm -f bro diff --git a/README b/README @@ -0,0 +1,27 @@ +* BRO is the standard web browser. * +(very much a work-in-progress still. Development continues!) + +The program, bro, is a simple URI-based multiplexer. +It takes in urls, and decides how to retrieve and +view them based on the url's protocol and the type +of the resultant file. For now, it only supports the +http(s) and gopher protocols and html, gph, and +plaintext file types. See the TODO. + +By default, bro uses: + - [wget](http://www.gnu.org/software/wget/): GNU http(s) client + - [hurl](http://git.codemadness.org/hurl/files.html): Basic gopher client + - [lwl](http://git.y1.nz/lwl/files.html): aka "less with links". + - [digest-html](git.y1.nz/digest-html/files.html) html-to-plaintext-with-links converter. + - [digest-gopher](git.y1.nz/digest-html/files.html) gopher-to-plaintext-with-links converter. +This is the minimum set of components needed to make a +functional web browser. It easy to update the configuration +to include other features, for example to add support for +protocols other than http(s). + +Build: + First, update config.mk and config.h to match your system & + preferences. Then, run "make" to build, and then "make + install" with the necessary priveleges to install. There + are no build dependencies, but it does rely on other programs + existing on your system. diff --git a/TODO b/TODO @@ -0,0 +1,38 @@ +[x] convert to C program instead of shell script, + + make it exec into lwl instead of calling it via a shell +[x] Find/implement a gopher client + digester + +[ ] figure out how to deal with retrieved filenames accurately + - gopher, you can use the selector (i think..?) + - http/https, it's a messy part of the protocol. Which sucks a lot :( + +[ ] Use temporary files for saving any files + - use open() with O_TMPFILE + - session dir default in config.h is /tmp, + or specified with -C (standard flag for this) + +[ ] Use tmpfiles for session history + - keep all pages in the current session saved to files under a tmp dir (?) + *or w tmp filename prefix? + - with a "session" file that lists them in order, newline-separated? + (we gucci, the max number is like 600,000 (/proc/sys/fs/file-max)) + still need to work out a few details on this.... + +[ ] Use a more suckless HTTP/HTTPS implementation than wget + -> perhaps hurl? Shorter, but kinda ugly of code + + doesn't actually implement important http things, like redirects. + +[/] interpret protocol + viewer from URI + [x] Default to "file" scheme when no url is provided + [x] file: try to open local file relative to pwd + [x] http,https: download with wget + [ ] ftp? + [x] gopher: TODO.... make "wget but for gopher"? (A: hurl, for now...) + +[ ] (ACTUALLY) determine filetype and determine behavior based on that + [ ] .html: digest then view with lwl + [ ] .gph: digest then view with lwl + [ ] .txt/.md/(no ext): view directly with lwl/less + [ ] .png/.jpg/etc: use feh? (as an example, not in my config) + +[ ] extract scheme- and extension-based behavior into config.h diff --git a/config.h b/config.h @@ -0,0 +1,7 @@ + +/** + * Data pertaining to the current session is saved + * to this directory. All files are temporary and + * will disappear when bro closes. + */ +#define DEFAULT_SESSION_DIR "/tmp" diff --git a/config.mk b/config.mk @@ -0,0 +1,4 @@ +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man +CC = cc -Wextra -Wpedantic -std=c99 \ + -I /usr/local/include -L /usr/local/lib diff --git a/main.c b/main.c @@ -0,0 +1,215 @@ +#include <stdio.h> /* printf */ +#include <string.h> /* strlen */ +#include <unistd.h> /* execlp, access */ +#include <sys/wait.h> /* waitpid */ +#include <stdlib.h> /* exit */ +#include <errno.h> /* errno */ +#include <fcntl.h> /* creat */ + +#include "okra.h" +#include "config.h" + +#define FATAL(message) { \ + printf("\033[1;31m" "Fatal: " message "\033[0m" "\n"); \ + exit(1); \ +} +#define FORKEXEC(arg1, ...) { \ + pid = fork(); \ + if (!pid) { \ + int result; \ + result = execlp(arg1, arg1, __VA_ARGS__, NULL); \ + exit(0); \ + } \ + waitpid(pid, &child_status, 0); \ +} +#define FFORKEXEC(fname, arg1, ...) { \ + pid = fork(); \ + if (!pid) { \ + int result; \ + int fd = creat( \ + fname, \ + O_CREAT | O_TRUNC | O_WRONLY | \ + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH \ + ); \ + if (fd == -1) { \ + printf("Failed to create [%s]" "\n", fname); \ + FATAL("couldnt open/create file :("); \ + } \ + dup2(fd, STDOUT_FILENO); \ + result = execlp(arg1, arg1, __VA_ARGS__, NULL); \ + if (result == -1) { \ + int err = errno; \ + FATAL("Uh oh exec dint work :(("); \ + } \ + exit(0); \ + } \ + waitpid(pid, &child_status, 0); \ +} + +pid_t pid; +int child_status; + +void +ask_to_continue() +{ + char ch; + + // TODO set termios so it's "press y to %s, n or q to quit" + printf("Try again?"); fflush(stdout); + for (;;) { + switch (getchar()) { + case 'y': return; + case 'n': + case 'q': FATAL("Cancelled."); + } + } +} + +void +ensure_file_exists(const char *path) +{ + if (!access(path, O_RDONLY)) return; + printf("Couldn't access local file [%s]" "\n", path); + FATAL("File is inaccessible"); +} + +int +streq(const char *a, const char *b) +{ + if (a == NULL && b == NULL) return 1; + if (a == NULL || b == NULL) return 0; + int i = 0; + for (;;) { + if (a[i] != b[i]) return 0; + if (a[i] == '\0') return 1; + i++; + } +} + +const char * +get_extension(const char *filename) +{ + int length = strlen(filename); + int i = length-1; + for (;;) { + i--; + if (i < 0) return NULL; + if (filename[i] == '.') + return filename + (i+1); + } +} + +const char * +get_raw_file(OkraUri *uri) +{ + const char *ret = NULL; + + /* default to "file" scheme */ + if (uri->scheme == NULL || streq(uri->scheme, "file")) + return uri->path; + + if (streq(uri->scheme, "mailto")) + FATAL("TODO open a mail client then exit?"); + + if (streq(uri->scheme, "http") || streq(uri->scheme, "https")) { + child_status = 1; + ret = DEFAULT_SESSION_DIR "/current.html"; + for (;;) { + if (child_status == 0) break; + FFORKEXEC(ret, "wget", "-O", "-", uri->uri); + if (child_status != 0) { + printf("Exit status was %d" "\n", child_status); + ask_to_continue(); + } + } + return ret; + } + + if (streq(uri->scheme, "gopher")) { + ret = DEFAULT_SESSION_DIR "/current.gph"; + FFORKEXEC(ret, "hurl", uri->uri); + return ret; + } + + if (streq(uri->scheme, "ftp")) + FATAL("TODO retrieve ftp page."); + + printf("Scheme: [%s]" "\n", uri->scheme); + FATAL("Unhandled scheme."); + exit(1); +} + +const char * +get_digested_file(OkraUri *uri, const char *raw_file) +{ + const char *ret = NULL; + const char *ext = get_extension(uri->uri); + + if (streq(ext, "txt")) return raw_file; + + if (streq(ext, "html")) { + ret = DEFAULT_SESSION_DIR "/current.md"; + FORKEXEC( + "digest-html", + "-b", uri->uri, + "-o", ret, + raw_file + ); + if (child_status != 0) FATAL("Failed to digest HTML."); + return ret; + } + + if (streq(ext, "gph")) { + ret = DEFAULT_SESSION_DIR "/current.md"; + FORKEXEC("digest-gopher", "-o", ret, raw_file); + if (child_status != 0) FATAL("Failed to digest gohper."); + return ret; + } + + /* default based on scheme */ + // TODO is there a better way? + if (streq(uri->scheme, "http") || streq(uri->scheme, "https")) { + ret = DEFAULT_SESSION_DIR "/current.md"; + FORKEXEC( + "digest-html", + "-b", uri->uri, + "-o", ret, + raw_file + ); + if (child_status != 0) FATAL("Failed to digest HTML."); + return ret; + } + if (streq(uri->scheme, "gopher")) { + ret = DEFAULT_SESSION_DIR "/current.md"; + FORKEXEC("digest-gopher", "-o", ret, raw_file); + if (child_status != 0) FATAL("Failed to digest gopher."); + return ret; + } + + printf("No digester, defaulting to raw" "\n"); + return raw_file; +} + +int +main(int argc, const char *argv[]) +{ + if (argc != 2) { + printf("Usage: %s URI" "\n", argv[0]); + return 1; + } + + OkraUri *uri = okra_parse(argv[1]); + if (uri == NULL) FATAL("Invalid URI."); + + FORKEXEC("rm", "-f", "current.md", "current.gph", "current.html"); + + const char *raw_file = get_raw_file(uri); + if (raw_file == NULL) FATAL("No file to view."); + ensure_file_exists(raw_file); + + const char *lwl_file = get_digested_file(uri, raw_file); + ensure_file_exists(lwl_file); + FORKEXEC("lwl", lwl_file); + + okra_free(uri); +}

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.