diff options
-rw-r--r-- | .gitconfig | 2 | ||||
-rw-r--r-- | Makefile | 38 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | arena.h | 149 | ||||
-rw-r--r-- | main.c | 518 | ||||
-rw-r--r-- | str.h | 152 | ||||
-rw-r--r-- | strio.h | 217 |
7 files changed, 1100 insertions, 0 deletions
diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..ef0b399 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +*.o +cbink diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..47dcbdc --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +EXE = cbink +RUNARGS = + +PREFIX ?= ${HOME}/.local +BINDIR = ${PREFIX}/bin + +CFLAGS = -std=c17 -Wall -Wextra -Wpedantic -Os ${CFLAGS_${DEBUG}} +LDFLAGS = -flto ${LDFLAGS_${DEBUG}} +LDLIBS = -lncursesw + +DEBUG = 0 +GDB != { [ "$DISPLAY" ] && which gf2 2> /dev/null; } || which gdb + +CFLAGS_1 = -g -fsanitize=undefined +LDFLAGS_1 = -g -fsanitize=undefined +LDFLAGS_0 = -s + +OBJ != find -type f -name '*.c' | sed 's/\.c$$/.o/' + +.PHONY: run all clean + +all: ${EXE} +run: ${EXE} + ./${EXE} ${RUNARGS} +debug: ${EXE} + ${GDB} -ex start --args ./${EXE} ${RUNARGS} + +clean: + rm -fv ${EXE} ${OBJ} + +${EXE}: ${OBJ} + ${CC} ${LDFLAGS} ${OBJ} -o ${EXE} ${LDLIBS} + +install: ${EXE} + cp ${EXE} ${BINDIR}/${EXE} + +uninstall: + rm ${BINDIR}/${EXE} diff --git a/README.md b/README.md new file mode 100644 index 0000000..15c4a0b --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# cbink + +a simple c client for the [bink](https://git.tilde.town/nebula/bink) +microblogging platform. there is no central server, only files in +`/home/*/.bink`, named after the number of nanoseconds since the unix +epoch at the time of their creation. + +## dependencies + +* a c17-compatible c compiler +* libncursesw (with unicode support) + +## keyboard shortcuts + +* `k` / `Up` --- up +* `j` / `Down` -- down +* `d` / `Space` / `PageDown` / `Ctrl-D` --- page down +* `b` / `u` / `PageUp` / `Ctrl-U` --- page up +* `g` / `Home` -- top of feed +* `G` / `End` -- bottom of feed +* `r` -- refresh feed +* `c` -- create new post +* `v` -- view post at top of window in `less(1)` +* `e` -- edit post at top of window (must be yours) diff --git a/arena.h b/arena.h new file mode 100644 index 0000000..045cde0 --- /dev/null +++ b/arena.h @@ -0,0 +1,149 @@ +#ifndef ARENA_H +#define ARENA_H + +#include <stdint.h> +#include <stddef.h> +#include <string.h> + +typedef struct ArenaPg { + struct ArenaPg *prev, *next; + char *beg, *end; + char data[]; +} ArenaPg; + +typedef struct { + ArenaPg *pg; + char *beg; +} ArenaMark; + +typedef struct { + ArenaPg *cur, *tail; +} Arena; + +#define new(a, t)\ + (t*)arena_zeroed(arena_alloc(a, sizeof(t), _Alignof(t)), sizeof(t)) + +#define new_arr(a, t, n)\ + arena_alloc(a, sizeof(t) * n, _Alignof(t)) + +#define resize(a, p, old, new)\ + arena_realloc(a, p, (old) * sizeof(*(p)), (new) * sizeof(*(p)),\ + _Alignof(__typeof__(*(p)))) + +void arena_free(Arena *a); + +void arena_save(Arena *a, ArenaMark *m); +void arena_load(Arena *a, ArenaMark *m); + +void arena_reset(Arena *a); +void arena_reserve(Arena *a, ptrdiff_t n); + +void *arena_alloc(Arena *a, ptrdiff_t n, ptrdiff_t align); +void *arena_realloc(Arena *a, void *ptr, ptrdiff_t old, ptrdiff_t new, ptrdiff_t align); +void *arena_zeroed(void *p, size_t n); + +#define ARENA_BACKEND_MALLOC 0 +#define ARENA_BACKEND_MMAP 1 + +#ifndef ARENA_BACKEND +#if defined(__linux__) +# define ARENA_BACKEND ARENA_BACKEND_MMAP +#else +# define ARENA_BACKEND ARENA_BACKEND_MALLOC +#endif +#endif + +#ifdef ARENA_IMPL + +#include <stdio.h> +#include <stdlib.h> +static void arena_pg_alloc_fail(void) { + fprintf(stderr, "failed to allocate arena page\n"); + abort(); +} + +#if ARENA_BACKEND == ARENA_BACKEND_MMAP +#include <sys/mman.h> +#include <unistd.h> +#define ARENA_PG_SIZE sysconf(_SC_PAGESIZE) +static inline void *arena_pg_alloc(ptrdiff_t n) { + void *p = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return p == MAP_FAILED ? NULL : p; +} +static inline void arena_pg_free(void *ptr, ptrdiff_t n) { munmap(ptr, n); } +#elif ARENA_BACKEND == ARENA_BACKEND_MALLOC +#define ARENA_PG_SIZE 8192 +static inline void *arena_pg_alloc(ptrdiff_t n) { return malloc(n); } +static inline void arena_pg_free(void *ptr, ptrdiff_t n) { free(ptr); (void)n; } +#endif + +void arena_free(Arena *a) { + while (a->tail) { + a->cur = a->tail->prev; + arena_pg_free(a->tail, (uintptr_t)(a->tail->end - (char*)a->tail)); + a->tail = a->cur; + } +} + +void arena_reserve(Arena *a, ptrdiff_t n) { + while (a->cur && a->cur->beg + n >= a->cur->end) a->cur = a->cur->next; + if (a->cur) return; + ptrdiff_t cap = n + sizeof(ArenaPg); + cap += (uintptr_t)-cap & (ARENA_PG_SIZE - 1); + ArenaPg *p = arena_pg_alloc(cap); + if (!p) arena_pg_alloc_fail(); + p->next = NULL; + p->prev = a->tail; + p->beg = p->data; + p->end = (char*)p + cap; + if (a->tail) a->tail->next = p; + a->cur = (a->tail = p); +} + +void *arena_alloc(Arena *a, ptrdiff_t n, ptrdiff_t align) { + arena_reserve(a, n + (align - 1)); + char *ptr = a->cur->beg + (-(uintptr_t)a->cur->beg & (align - 1)); + a->cur->beg = ptr + n; + return ptr; +} + +void *arena_realloc(Arena *a, void *ptr, ptrdiff_t old, ptrdiff_t new, ptrdiff_t align) { + if (a->cur && ptr == a->cur->beg - old && (char*)ptr + new < a->cur->end) { + a->cur->beg += new - old; + return ptr; + } else { + void *p = arena_alloc(a, new, align); + if (ptr) memcpy(p, ptr, old); + return p; + } +} + +void *arena_zeroed(void *p, size_t n) { + memset(p, 0, n); + return p; +} + +void arena_reset(Arena *a) { + if (!a->cur) return; + while (a->cur->prev) { + a->cur->beg = a->cur->data; + a->cur = a->cur->prev; + } + a->cur->beg = a->cur->data; +} + +void arena_save(Arena *a, ArenaMark *m) { + m->pg = a->cur; + if (a->cur) m->beg = a->cur->beg; +} + +void arena_load(Arena *a, ArenaMark *m) { + while (a->cur && a->cur != m->pg) { + a->cur->beg = a->cur->data; + a->cur = a->cur->prev; + } + if (a->cur) a->cur->beg = m->beg; +} + +#endif +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..38ada6e --- /dev/null +++ b/main.c @@ -0,0 +1,518 @@ +/* cbink.c -- -lncursesw + * simple C client for bink + * https://git.tilde.town/nebula/bink + */ + +#define _POSIX_C_SOURCE 202506L + +#include <stdio.h> +#include <locale.h> +#include <stdlib.h> +#include <dirent.h> +#include <curses.h> +#include <stdarg.h> +#include <linux/limits.h> /* PATH_MAX */ +#include <time.h> +#include <regex.h> +#include <err.h> + +#define STR_IMPL +#define STRIO_IMPL +#define ARENA_IMPL + +#include "str.h" +#include "strio.h" +#include "arena.h" + +#define POST_LIMIT 200 /* see bink.py */ +#define PAGE_LEN 10 +#define GFX_MARGIN_X 2 +#define GFX_MARGIN_Y 1 +#define GFX_TEXT_MARGIN_X 1 +#define GFX_TEXT_MARGIN_Y 0 + +/* colors */ + +#define CPAIR_TEXT 0 +#define CPAIR_USER 1 +#define CPAIR_TIME 2 +#define CPAIR_MENTION 3 +#define CPAIR_BORDER 4 + +/* dynamic arrays */ + +#define DYNARR(T) struct { ptrdiff_t len, cap; T *data; } + +#define DA_FIT(da, n) do {\ + if (n >= (da)->cap) {\ + (da)->cap += !(da)->cap;\ + while (n >= (da)->cap) (da)->cap <<= 1;\ + void *da_fit_ptr = realloc((da)->data, (da)->cap * sizeof((da)->data[0]));\ + if (!da_fit_ptr) { fprintf(stderr, "dynamic array reallocation failed\n"); abort(); }\ + (da)->data = da_fit_ptr;\ + }\ +} while(0) + +#define DA_PUSH(da, ...) do {\ + DA_FIT(da, (da)->len + 1);\ + (da)->data[(da)->len++] = (__VA_ARGS__);\ +} while(0) + +#define DA_PUSH_MULT(da, buf, n) do {\ + DA_FIT(da, (da)->len + n);\ + memcpy(&(da)->data[(da)->len], buf, n * sizeof((da)->data[0]));\ + (da)->len += n;\ +} while(0) + +/* globals */ + +regex_t re_mention; + +/* logging */ +/* TODO: change to put in a proper log file or something */ + +void log_warn(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); +} + +/* string conversion */ + +int str_to_timespec(Str s, struct timespec *ts) { + uint64_t sec, nsec; + /* timestamps are in nanoseconds-since-epoch */ + /* timespec is tv_sec and tv_nsec, the latter ranging from 0 - 999'999'999 */ + /* reject any timestamps that wouldn't reach one second */ + if (s.n < 9) return -1; + /* no negative timestamps */ + if (s.s[0] == '-') return -1; + /* both must be valid numbers */ + if (str_to_u64((Str) { s.s, s.n - 9 }, &sec) || str_to_u64(str_skip(s, s.n - 9), &nsec)) return -1; + ts->tv_sec = sec; + ts->tv_nsec = nsec; + return 0; +} + +int timestamp_invalid(struct timespec *ts) { + /* reject posts from more than a minute into the future */ + if (ts->tv_sec > time(NULL) + 60) return 1; + return 0; +} + +/* formatting */ + +/* posts */ + +typedef struct Post { + struct timespec timestamp; + const char *path; + Str user, text; +} Post; + + +typedef struct { + ArenaMark mark; + Post *data; + ptrdiff_t len, cap; +} PostList; + +/* sorting */ + +int ts_cmp(const struct timespec *a, const struct timespec *b) { + if (a->tv_sec < b->tv_sec) return -1; + if (a->tv_sec > b->tv_sec) return 1; + if (a->tv_nsec < b->tv_nsec) return -1; + if (a->tv_nsec > b->tv_nsec) return 1; + return 0; +} + +int post_cmp(const void *a, const void *b) { + const Post *pa = (const Post *)a; + const Post *pb = (const Post *)b; + return -ts_cmp(&pa->timestamp, &pb->timestamp); +} + +void posts_gather_from(PostList *posts, Str username, const char *path, Arena *a) { + if (chdir(path)) return; + + DIR *bink = opendir(".bink"); + if (!bink) return; + + if (chdir(".bink")) { + log_warn("couldn't cd %s/.bink", path); + return; + } + + char cwd[PATH_MAX]; + getcwd(cwd, PATH_MAX); + + struct dirent *de_bink; + while ((de_bink = readdir(bink))) { + if (!strcmp(de_bink->d_name, ".") || !strcmp(de_bink->d_name, "..")) continue; + + Post p = { 0 }; + + if (str_to_timespec(str_from_cstr(de_bink->d_name), &p.timestamp)) continue; + if (timestamp_invalid(&p.timestamp)) continue; + + p.user = str_dup(username, a); + p.path = cstr_fmt(a, "%s/%s", cwd, de_bink->d_name); + + DA_PUSH(posts, p); + } + closedir(bink); +} + +void posts_gather(PostList *posts, Arena *a) { + struct dirent *de_homedir; + DIR *home = opendir("/home/"); + if (!home) err(1, "couldn't open /home/"); + while ((de_homedir = readdir(home))) { + if (!strcmp(de_homedir->d_name, ".") || !strcmp(de_homedir->d_name, "..")) continue; + if (chdir("/home/")) err(1, "failed to change directory"); + posts_gather_from(posts, str_from_cstr(de_homedir->d_name), de_homedir->d_name, a); + } + closedir(home); + posts_gather_from(posts, S("our"), "/town/our/data", a); +} + +void posts_load(PostList *posts, Arena *a) { + qsort(posts->data, posts->len, sizeof(Post), post_cmp); + if (posts->len > POST_LIMIT) posts->len = POST_LIMIT; + for (int i = 0; i < posts->len; i++) { + Post *p = &posts->data[i]; + FILE *f = fopen(p->path, "r/o"); + if (!f) { + log_warn("couldn't open %s", p->path); + continue; + } + if (read_all(f, &p->text, a)) { + log_warn("couldn't read %s", p->path); + fclose(f); + continue; + } + fclose(f); + } +} + +void posts_refresh(PostList *posts, Arena *a) { + posts->len = 0; + posts_gather(posts, a); + posts_load(posts, a); +} + +/* display */ + +typedef struct { + Post *src; + Str text; + int lines; + struct { + unsigned drawn : 1; + unsigned has_mention : 1; + }; +} GfxPost; + +typedef struct { + int len; + GfxPost *posts; +} Gfx; + +static inline int ch_space(char c) { + return c <= 0x20; +} + +int str_cat_wrap(Str *out, Str s, int width, Arena *a) { + int lines = 1; + int x = 1; + for (int i = 0; i < s.n; i++) { + if (ch_space(s.s[i])) { + int w = 0; + for (int j = i + 1; j < s.n && !ch_space(s.s[j]); j++) w++; + if (x + w >= width || s.s[i] == '\n') { + x = 1; + str_catc(out, '\n', a); + lines++; + } else if (x < width) { + str_catc(out, s.s[i], a); + x++; + } + } else { + str_catc(out, s.s[i], a); + x++; + } + } + if (x == 1) lines--; + return lines; +} + +int gfx_wrap_width(void) { + return getmaxx(stdscr) - 2 - GFX_MARGIN_X * 2 - GFX_TEXT_MARGIN_X * 2; +} + +void gfx_load_post(GfxPost *post, Post *src, int width, Arena *a) { + memset(post, 0, sizeof(GfxPost)); + post->src = src; + /* subtract two to make room for border */ + post->lines = str_cat_wrap(&post->text, src->text, width - 2, a); + str_catc(&post->text, '\0', a); + post->text.n--; +} + +void gfx_load(Gfx *gfx, PostList *posts, Arena *a) { + int width = gfx_wrap_width(); + gfx->len = posts->len; + gfx->posts = new_arr(a, GfxPost, gfx->len); + for (int i = 0; i < gfx->len; i++) { + gfx_load_post(&gfx->posts[i], &posts->data[i], width, a); + } +} + +void gfx_vline(int y, int x, int len, int c) { + for (int i = y; i < y + len; i++) mvaddch(i, x, c); +} + +void gfx_hline(int y, int x, int len, int c) { + for (int i = x; i < x + len; i++) mvaddch(y, i, c); +} + +void gfx_predraw_post(GfxPost *post) { + post->has_mention = !regexec(&re_mention, post->text.s, 0, NULL, 0); +} + +int gfx_post_height(GfxPost *post) { + return post->lines + 2 + GFX_TEXT_MARGIN_Y * 2; +} + +void gfx_draw_post(GfxPost *post, int y, int x, int width) { + int height = gfx_post_height(post); + + if (!post->drawn) { + gfx_predraw_post(post); + post->drawn = 1; + } + + int left = x, top = y, right = x + width, bottom = y + height - 1; + + color_set(CPAIR_BORDER, 0); + gfx_hline(top, left, width, ACS_HLINE); + gfx_vline(top, left, height - 1, ACS_VLINE); + gfx_vline(top, right, height - 1, ACS_VLINE); + gfx_hline(bottom, left, width, ACS_HLINE); + + mvaddch(bottom, right, ACS_LRCORNER); + mvaddch(bottom, left, ACS_LLCORNER); + mvaddch(top, left, ACS_ULCORNER); + mvaddch(top, right, ACS_URCORNER); + + color_set(CPAIR_TIME, 0); + char *tstamp = ctime(&(time_t){post->src->timestamp.tv_sec}); + mvaddnstr(y, x + width - strlen(tstamp) - 1, tstamp, strlen(tstamp) - 1); + + color_set(CPAIR_USER, 0); + mvaddnstr(y, x + 2, post->src->user.s, post->src->user.n); + + color_set(post->has_mention ? CPAIR_MENTION : CPAIR_TEXT, 0); + Str txt = post->text; + for (int i = 0; i < post->lines; i++) { + Cut c = str_cut(txt, '\n'); + mvaddnstr(top + i + 1 + GFX_TEXT_MARGIN_Y, left + 1 + GFX_TEXT_MARGIN_X, c.head.s, c.head.n); + txt = c.tail; + } +} + +void gfx_draw(Gfx *gfx, int cur) { + int wrap_width = gfx_wrap_width(); + erase(); + for (int i = cur, y = GFX_MARGIN_Y; i < gfx->len && y < getmaxy(stdscr) - GFX_MARGIN_Y; i++) { + gfx_draw_post(&gfx->posts[i], y, GFX_MARGIN_X, wrap_width); + y += gfx_post_height(&gfx->posts[i]) + 1; + } +} + +/* main */ + +void init_curses(void) { + setlocale(LC_ALL, ""); + initscr(); + start_color(); + init_color(COLOR_BLACK, 0, 0, 0); + cbreak(); + noecho(); + intrflush(stdscr, FALSE); + keypad(stdscr, TRUE); + curs_set(0); + + init_pair(CPAIR_BORDER, COLOR_BLUE, COLOR_BLACK); + init_pair(CPAIR_USER, COLOR_YELLOW, COLOR_BLACK); + init_pair(CPAIR_TIME, COLOR_BLUE, COLOR_BLACK); + init_pair(CPAIR_TEXT, COLOR_WHITE, COLOR_BLACK); + init_pair(CPAIR_MENTION, COLOR_BLACK, COLOR_WHITE); +} + +void fini_curses(void) { + curs_set(1); + endwin(); +} + +char *get_editor(void) { + char *editor = getenv("EDITOR"); + if (!editor) editor = "nano"; + return editor; +} + +int my_post(Post *post) { + return str_eql(post->user, str_from_cstr(getlogin())); +} + +void edit_post(Post *post, Arena *temp) { + system(cstr_fmt(temp, "%s %s", get_editor(), post->path)); +} + +void new_post(Arena *temp) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts)) { + err(1, "clock_gettime failure"); + } + + Str t = str_fmt(temp, "%U%09U", (uint64_t)ts.tv_sec, (uint64_t)ts.tv_nsec); + const char *tmpf = cstr_fmt(temp, "/tmp/cbink_%s_%S.txt", getlogin(), t); + const char *outf = cstr_fmt(temp, "/home/%s/.bink/%S", getlogin(), t); + if (system(cstr_fmt(temp, "%s %s", get_editor(), tmpf))) return; + + Str body = { 0 }; + FILE *f = fopen(tmpf, "r/o"); + if (!f) return; + if (read_all(f, &body, temp)) { + fclose(f); + log_warn("couldn't read %s", tmpf); + return; + } + fclose(f); + if (remove(tmpf)) log_warn("failed to remove %s", tmpf); + body = str_trim(body); + if (body.n < 1) return; + + f = fopen(outf, "w/o"); + if (!f) { + log_warn("failed to open %s", outf); + return; + } + if (fwrite(body.s, 1, body.n, f) != (size_t)body.n) { + err(1, "write error in %s", outf); + fclose(f); + return; + } + fclose(f); +} + +/* this will probably crash if there are zero posts */ + +int main(void) { + init_curses(); + + /* init */ + + Arena post_arena = { 0 }; + Arena gfx_arena = { 0 }; + Arena temp_arena = { 0 }; + + arena_reserve(&post_arena, 128 << 10L); + arena_reserve(&gfx_arena, 128 << 10L); + + PostList posts = { 0 }; + Gfx gfx = { 0 }; + + regcomp(&re_mention, cstr_fmt(&temp_arena, + "([ \t\n]+|^)[@~]?%s([: \t\n]|$)", getlogin()), + REG_EXTENDED | REG_ICASE | REG_NOSUB); + + posts_refresh(&posts, &post_arena); + gfx_load(&gfx, &posts, &gfx_arena); + + int cur = 0; + for (;;) { + if (cur > posts.len - 1) cur = posts.len - 1; + if (cur < 0) cur = 0; + + arena_reset(&temp_arena); + gfx_draw(&gfx, cur); + + int ch = getch(); + if (ch == 'q') break; + switch (ch) { + case KEY_UP: + case 'k': + if (cur > 0) cur--; + break; + case KEY_DOWN: + case 'j': + if (cur + 1 < posts.len) cur++; + break; + case 'g': + case KEY_HOME: + cur = 0; + break; + case 'G': + case KEY_END: + cur = posts.len - 1; + break; + case ' ': + case 'd': + case KEY_NPAGE: + case 0x04 /* ^D */: + cur += PAGE_LEN; + if (cur > posts.len - 1) cur = posts.len - 1; + break; + case 'b': + case 'u': + case KEY_PPAGE: + case 0x15 /* ^U */: + cur -= PAGE_LEN; + if (cur < 0) cur = 0; + break; + case 'e': + if (my_post(&posts.data[cur])) { + fini_curses(); + edit_post(&posts.data[cur], &temp_arena); + init_curses(); + } else { + beep(); + flash(); + } + goto refresh; + case 'c': + fini_curses(); + new_post(&temp_arena); + init_curses(); + /* fallthrough */ + case 'r': +refresh: arena_reset(&post_arena); + posts_refresh(&posts, &post_arena); + /* fallthrough */ + case KEY_RESIZE: + arena_reset(&gfx_arena); + gfx_load(&gfx, &posts, &gfx_arena); + break; + case 'v': + fini_curses(); + system(cstr_fmt(&temp_arena, "less %s", posts.data[cur].path)); + init_curses(); + break; + } + } + + free(posts.data); + regfree(&re_mention); + + arena_free(&post_arena); + arena_free(&gfx_arena); + arena_free(&temp_arena); + + fini_curses(); + + return 0; +} diff --git a/str.h b/str.h new file mode 100644 index 0000000..b10bc64 --- /dev/null +++ b/str.h @@ -0,0 +1,152 @@ +#ifndef STR_H +#define STR_H + +#include <string.h> +#include <stddef.h> + +#include "arena.h" + +typedef struct { + char *s; + ptrdiff_t n; +} Str; + +typedef struct { + Str head, tail; +} Cut; + +#define S(s) (Str){s,sizeof(s)-1} + +char *str_to_cstr(Str s, Arena *a); +Str str_from_cstr(const char *s); +int str_eql(Str a, Str b); +int str_starts(Str a, Str b); +int str_ends(Str a, Str b); +void str_catc(Str *a, char b, Arena *m); +Str str_skip(Str a, ptrdiff_t n); +int is_space(char c); +Str str_trim_left(Str a); +Str str_trim_right(Str a); +Str str_trim(Str a); +Cut str_cut(Str s, char c); +Str str_findc(Str s, char c); +Str str_find(Str haystack, Str needle); +int str_contains(Str a, Str b); +Str str_dup(Str a, Arena *m); +void str_cat(Str *a, Str b, Arena *m); +Str str_replace_end(Str s, Str a, Str b, Arena *m); + +#ifdef STR_IMPL + +/* conversions */ + +char *str_to_cstr(Str s, Arena *a) { + char *r = new_arr(a, char, s.n + 1); + memcpy(r, s.s, s.n); + r[s.n] = 0; + return r; +} + +Str str_from_cstr(const char *s) { + return (Str) { (char*)s, strlen(s) }; +} + +/* pure functions */ + +int str_eql(Str a, Str b) { + return a.n == b.n && !memcmp(a.s, b.s, b.n); +} + +int str_starts(Str a, Str b) { + return a.n >= b.n && !memcmp(a.s, b.s, b.n); +} + +int str_ends(Str a, Str b) { + return a.n >= b.n && !memcmp(&a.s[a.n - b.n], b.s, b.n); +} + +void str_catc(Str *a, char b, Arena *m) { + a->s = resize(m, a->s, a->n, a->n + 1); + a->s[a->n++] = b; +} + +Str str_skip(Str a, ptrdiff_t n) { + return (Str) { a.s + n, a.n - n }; +} + +int is_space(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +Str str_trim_left(Str a) { + while (a.n > 0 && is_space(a.s[0])) a.s++, a.n--; + return a; +} + +Str str_trim_right(Str a) { + while (a.n > 0 && is_space(a.s[a.n - 1])) a.n--; + return a; +} + +Str str_trim(Str a) { + return str_trim_left(str_trim_right(a)); +} + +/* splitting, searching */ + +Cut str_cut(Str s, char c) { + char *p = memchr(s.s, c, s.n); + if (!p) { + return (Cut) { s, { &s.s[s.n], 0 } }; + } else { + return (Cut) { + { s.s, p - s.s }, + { p + 1, &s.s[s.n] - (p + 1) } + }; + } +} + +Str str_findc(Str s, char c) { + char *p = memchr(s.s, c, s.n); + return p ? (Str) { p, s.n - (p - s.s) } : (Str) { &s.s[s.n], 0 }; +} + +Str str_find(Str haystack, Str needle) { + if (needle.n < 1) return haystack; + while (haystack.n > 0) { + haystack = str_findc(haystack, needle.s[0]); + if (str_starts(haystack, needle)) break; + if (haystack.n > 0) haystack = str_skip(haystack, 1); + } + return haystack; +} + +int str_contains(Str a, Str b) { + return str_find(a, b).n > 0; +} + +/* allocating */ + +Str str_dup(Str a, Arena *m) { + char *s = new_arr(m, char, a.n); + memcpy(s, a.s, a.n); + a.s = s; + return a; +} + +void str_cat(Str *a, Str b, Arena *m) { + a->s = resize(m, a->s, a->n, a->n + b.n); + memcpy(&a->s[a->n], b.s, b.n); + a->n += b.n; +} + +Str str_replace_end(Str s, Str a, Str b, Arena *m) { + if (!str_ends(s, a)) return s; + char *p = new_arr(m, char, s.n + b.n - a.n); + memcpy(p, s.s, s.n - a.n); + memcpy(p + s.n - a.n, b.s, b.n); + return (Str) { p, s.n + b.n - a.n }; +} + +#endif +#endif diff --git a/strio.h b/strio.h new file mode 100644 index 0000000..c8bb21f --- /dev/null +++ b/strio.h @@ -0,0 +1,217 @@ +#ifndef STRIO_H +#define STRIO_H + +#include "str.h" +#include "arena.h" + +int read_all(FILE *f, Str *buf, Arena *a); +int next_line(Str *src, Str *line); +void str_putf(Str s, FILE *f); +void str_put(Str s); + +int str_to_u64(Str s, uint64_t *out); +void str_cat_i64(Str *out, int64_t c, char pad_char, int min_width, Arena *a); +void str_cat_u64(Str *out, uint64_t c, char pad_char, int min_width, Arena *a); +void str_cat_fmtv(Str *out, Arena *arena, const char *fmt, va_list ap); +void str_cat_fmt(Str *out, Arena *arena, const char *fmt, ...); +Str str_fmtv(Arena *arena, const char *fmt, va_list ap); +Str str_fmt(Arena *arena, const char *fmt, ...); +const char *cstr_fmt(Arena *arena, const char *fmt, ...); + +#ifdef STRIO_IMPL + +#include <stdio.h> +#include <stdint.h> + +static inline long read_all_file_size(FILE *f) { + fseek(f, 0, SEEK_END); + long t = ftell(f); + fseek(f, 0, SEEK_SET); + return t > 0 ? t : -1; +} + +int read_all(FILE *f, Str *buf, Arena *a) { + if (!f) return -1; + long sz = read_all_file_size(f); + if (sz < 1) { + ptrdiff_t cap = 4096; + buf->s = new_arr(a, char, cap); + buf->n = 0; + while (!feof(f)) { + size_t n = fread(&buf->s[buf->n], 1, cap - buf->n, f); + if (n < 1) break; + buf->n += n; + if (buf->n >= cap) { + size_t c = cap; + while (buf->n >= cap) cap <<= 1; + buf->s = resize(a, buf->s, c, cap); + } + } + } else { + buf->n = sz; + buf->s = new_arr(a, char, sz); + size_t sz = fread(buf->s, 1, buf->n, f); + if (sz < (size_t)buf->n) return -1; + } + return ferror(f) ? -1 : 0; +} + +int next_line(Str *src, Str *line) { + if (src->n < 1) return 0; + line->s = src->s; + char *newln = memchr(src->s, '\n', src->n); + line->n = newln ? newln - src->s : src->n; + src->s += line->n + 1; + src->n -= line->n + 1; + if (line->n > 0 && line->s[line->n-1] == '\r') line->n--; + return 1; +} + +void str_putf(Str s, FILE *f) { + fwrite(s.s, 1, s.n, f); +} + +void str_put(Str s) { + str_putf(s, stdout); +} + +/* formatted conversion */ + +int str_to_u64(Str s, uint64_t *out) { + if (s.n < 1) return -1; + uint64_t acc = 0; + for (int i = 0; i < s.n; i++) { + char c = s.s[i]; + if (!(c >= '0' && c <= '9')) return -1; + acc = (acc * 10) + (c - '0'); + } + *out = acc; + return 0; +} + +static void str_cat_u64_(char buf[32], int *n, uint64_t c) { + int i = 0; + buf[31] = '\0'; + do { + buf[32 - ++i] = (c % 10) + '0'; + c /= 10; + } while (c); + *n = i; +} + +void str_cat_u64(Str *out, uint64_t c, char pad_char, int min_width, Arena *a) { + int n; + /* more than enough for the largest 64-bit number + * log_10(1 << 64) ~= 19.3 digits max */ + char buf[32]; + str_cat_u64_(buf, &n, c); + while (n < min_width && ++n < 32) buf[32-n] = pad_char; + str_cat(out, (Str) { &buf[sizeof(buf) - n], n }, a); +} + +void str_cat_i64(Str *out, int64_t c, char pad_char, int min_width, Arena *a) { + /* more than enough for the largest 64-bit number + * log_10(1 << 64) ~= 19.3 digits max */ + int n, neg = 0; + char buf[32]; + if (c < 0) neg = 1, c = -c; + str_cat_u64_(buf, &n, c); + if (neg) buf[sizeof(buf) - ++n] = '-'; + while (n < min_width && ++n < 32) buf[32-n] = pad_char; + str_cat(out, (Str) { &buf[sizeof(buf) - n], n }, a); +} + +/* IMPORTANT: this is not and will not be printf() compatible + * + * %s - c string + * %S - Str + * %i - int32 + * %I - int64 + * %u - uint32 + * %U - uin64 + * + **/ +void str_cat_fmtv(Str *out, Arena *arena, const char *fmt, va_list ap) { + size_t n = strlen(fmt); + for (size_t i = 0; i < n; i++) { + const char *mch = memchr(&fmt[i], '%', n - i); + if (!mch) { + str_cat(out, (Str) { (char*)&fmt[i], n - i }, arena); + break; + } + size_t skip = mch - &fmt[i]; + if (mch != &fmt[i]) { + str_cat(out, (Str) { (char*)&fmt[i], skip }, arena); + i += skip; + } + if (i + 1 < n) { + int zero_pad = 0, min_width = 0; + i++; + if (fmt[i] == '0') { + zero_pad = 1; + i++; + } + while (i < n && fmt[i] >= '0' && fmt[i] <= '9') { + min_width = min_width * 10 + (fmt[i] - '0'); + i++; + } + if (i >= n) break; + switch (fmt[i]) { + case 's': + str_cat(out, str_from_cstr(va_arg(ap, const char *)), arena); + break; + case 'S': + str_cat(out, va_arg(ap, Str), arena); + break; + case 'i': + str_cat_i64(out, va_arg(ap, int32_t), zero_pad?'0':' ', min_width, arena); + break; + case 'I': + str_cat_i64(out, va_arg(ap, int64_t), zero_pad?'0':' ', min_width, arena); + break; + case 'u': + str_cat_u64(out, va_arg(ap, uint32_t), zero_pad?'0':' ', min_width, arena); + break; + case 'U': + str_cat_u64(out, va_arg(ap, uint64_t), zero_pad?'0':' ', min_width, arena); + break; + default: + str_catc(out, fmt[i], arena); + break; + } + } + } +} + +void str_cat_fmt(Str *out, Arena *arena, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + str_cat_fmtv(out, arena, fmt, ap); + va_end(ap); +} + +Str str_fmtv(Arena *arena, const char *fmt, va_list ap) { + Str s = { 0 }; + str_cat_fmtv(&s, arena, fmt, ap); + return s; +} + +Str str_fmt(Arena *arena, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + Str r = str_fmtv(arena, fmt, ap); + va_end(ap); + return r; +} + +const char *cstr_fmt(Arena *arena, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + Str r = str_fmtv(arena, fmt, ap); + str_catc(&r, '\0', arena); + va_end(ap); + return r.s; +} + +#endif +#endif |