From 6f01a277d7330296684622e3bd1c6a80e8b5d8fc Mon Sep 17 00:00:00 2001 From: wrmr Date: Wed, 8 Oct 2025 18:04:02 -0400 Subject: margins, datetime, pfp/pronouns/time visibility in .cbinkrc! --- main.c | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 164 insertions(+), 44 deletions(-) diff --git a/main.c b/main.c index 3255b93..f9adb85 100644 --- a/main.c +++ b/main.c @@ -63,20 +63,21 @@ /* colors */ #define CPAIR_ENUM_X(cp, fg, bg, name) cp, +#define CPAIR_NAME_X(cp, fg, bg, name) S(name), #define CPAIR_INIT_X(cp, fg, bg, name) init_pair(cp, fg, bg); #define COLOR_NORM -1 #define COLOR_FG COLOR_NORM #define COLOR_BG COLOR_NORM #define CPAIR_LIST(X)\ - X(CPAIR_TEXT , COLOR_FG , COLOR_BG, "text")\ - X(CPAIR_MENTION , COLOR_BG , COLOR_FG, "mention")\ - X(CPAIR_USER , COLOR_YELLOW , COLOR_BG, "user")\ - X(CPAIR_PRONOUNS , COLOR_CYAN , COLOR_BG, "pronouns")\ - X(CPAIR_TIME , COLOR_BLUE , COLOR_BG, "time")\ - X(CPAIR_PFP , COLOR_BLUE , COLOR_BG, "pfp")\ - X(CPAIR_PFP_SELF , COLOR_YELLOW , COLOR_BG, "pfp.self")\ - X(CPAIR_BANNER , COLOR_FG , COLOR_BG, "banner")\ - X(CPAIR_BORDER , COLOR_BLUE , COLOR_BG, "border")\ + X(CPAIR_TEXT , COLOR_FG , COLOR_BG, "text")\ + X(CPAIR_MENTION , COLOR_BG , COLOR_FG, "mention")\ + X(CPAIR_USER , COLOR_YELLOW , COLOR_BG, "user")\ + X(CPAIR_PRONOUNS , COLOR_CYAN , COLOR_BG, "pronouns")\ + X(CPAIR_TIME , COLOR_BLUE , COLOR_BG, "time")\ + X(CPAIR_PFP , COLOR_BLUE , COLOR_BG, "pfp")\ + X(CPAIR_PFP_SELF , COLOR_YELLOW , COLOR_BG, "pfp.self")\ + X(CPAIR_BANNER , COLOR_FG , COLOR_BG, "banner")\ + X(CPAIR_BORDER , COLOR_BLUE , COLOR_BG, "border")\ X(CPAIR_MSG_OK , COLOR_GREEN , COLOR_BG, "msg.ok")\ X(CPAIR_MSG_WARN , COLOR_YELLOW , COLOR_BG, "msg.warn")\ X(CPAIR_MSG_ERR , COLOR_RED , COLOR_BG, "msg.err") @@ -88,6 +89,11 @@ typedef enum { CPAIR_MAX } ColorPair; +Str cpair_name[CPAIR_MAX] = { + S("N/A"), + CPAIR_LIST(CPAIR_NAME_X) +}; + /* dynamic arrays */ #define DYNARR(T) struct { ptrdiff_t len, cap; T *data; } @@ -145,10 +151,12 @@ typedef struct { typedef struct { struct { - int pfp, pronouns; - } see; + int pfp, pronouns, date; + const char *datefmt; + } post; struct { int left, right, top, bottom; + int text_x, text_y; } margin; int default_colors; ColorOpt color[CPAIR_MAX]; @@ -158,13 +166,19 @@ typedef struct { regex_t re_mention; Options opt = { - .see = { + .post = { .pfp = 1, - .pronouns = 1 + .pronouns = 1, + .date = 1, + .datefmt = NULL }, .margin = { - .left = 0, - .right = 0 + .left = GFX_MARGIN_X, + .right = GFX_MARGIN_X, + .top = GFX_MARGIN_Y, + .bottom = GFX_MARGIN_Y, + .text_x = GFX_TEXT_MARGIN_X, + .text_y = GFX_TEXT_MARGIN_Y, }, .default_colors = 1 }; @@ -437,16 +451,21 @@ static inline int ch_space(char c) { return c <= 0x20; } +static inline int ch_break_at(char c) { + return ch_space(c) || c == '-' || c == '/'; +} + 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])) { + if (ch_break_at(s.s[i])) { int w = 0; - for (int j = i + 1; j < s.n && !ch_space(s.s[j]); j++) w++; + for (int j = i + 1; j < s.n && !ch_break_at(s.s[j]); j++) w++; if (x + w >= width || s.s[i] == '\n') { x = 1; str_catc(out, '\n', a); + if (!ch_space(s.s[i])) str_catc(out, s.s[i], a); lines++; } else if (x < width) { str_catc(out, s.s[i], a); @@ -480,16 +499,16 @@ typedef struct { int post_left_margin(Post *post) { int lm = opt.margin.left; - if (opt.see.pfp && post->user->pfp.cols >= lm) lm = post->user->pfp.cols + GFX_PFP_MARGIN; + if (opt.post.pfp) lm += post->user->pfp.cols + GFX_PFP_MARGIN; return lm; } int gfx_post_width(Post *post) { - return getmaxx(stdscr) - GFX_MARGIN_X * 2 - post_left_margin(post) - opt.margin.right; + return getmaxx(stdscr) - (post_left_margin(post) + opt.margin.right); } int gfx_wrap_width(Post *post) { - return gfx_post_width(post) - GFX_TEXT_MARGIN_X * 2; + return gfx_post_width(post) - opt.margin.text_x * 2 - 1; } void gfx_load_post(GfxPost *post, Post *src, int width, Arena *a) { @@ -522,13 +541,13 @@ void gfx_predraw_post(GfxPost *post) { } int gfx_post_text_height(GfxPost *post) { - return post->lines + 2 + GFX_TEXT_MARGIN_Y * 2; + return post->lines + 2 + opt.margin.text_y * 2; } int gfx_post_height(GfxPost *post) { int base = gfx_post_text_height(post); int l = MIN(post->src->user->pfp.lines, GFX_PFP_MAX_LINES); - if (opt.see.pfp && l > base) return l; + if (opt.post.pfp && l > base) return l; return base; } @@ -589,7 +608,7 @@ void gfx_draw_post(GfxPost *post, int y, int x, int width, Arena *scratch) { post->drawn = 1; } - int left = x + post_left_margin(post->src), top = y + opt.margin.top; + int left = x, top = y; int right = left + width - 1; int self = is_post_mine(post->src); User *u = post->src->user; @@ -601,11 +620,23 @@ void gfx_draw_post(GfxPost *post, int y, int x, int width, Arena *scratch) { gfx_draw_box(top, left, height, width); - color_set(CPAIR_TIME, 0); - Str s = str_trim(str_from_cstr(ctime(&(time_t){post->src->timestamp.tv_sec}))); - mvaddnstr(top, right - s.n, s.s, s.n); + if (opt.post.date) { + color_set(CPAIR_TIME, 0); + char time_buf[256] = { 0 }; + Str time_str = { 0 }; + time_t timep = post->src->timestamp.tv_sec; + if (opt.post.datefmt) { + struct tm *p = localtime(&timep); + strftime(time_buf, 255, opt.post.datefmt, p); + time_str = str_from_cstr(time_buf); + } else { + time_str = str_from_cstr(ctime(&timep)); + } + time_str = str_trim(time_str); + mvaddnstr(top, right - fast_utf8_width(time_str), time_str.s, time_str.n); + } - if (opt.see.pfp) { + if (opt.post.pfp) { color_set(self ? CPAIR_PFP_SELF : CPAIR_PFP, 0); Pic *pfp = &u->pfp; int pfpy = y; @@ -622,7 +653,7 @@ void gfx_draw_post(GfxPost *post, int y, int x, int width, Arena *scratch) { color_set(CPAIR_USER, 0); mvaddnstr(top, left + 2, u->name.s, u->name.n); - if (opt.see.pronouns && u->pronouns.n) { + if (opt.post.pronouns && u->pronouns.n) { color_set(CPAIR_PRONOUNS, 0); mvaddstr(top, left + 2 + fast_utf8_width(u->name) + GFX_PRONOUNS_MARGIN, cstr_fmt(scratch, GFX_PRONOUNS_FMT, u->pronouns)); } @@ -632,15 +663,15 @@ void gfx_draw_post(GfxPost *post, int y, int x, int width, Arena *scratch) { 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); + mvaddnstr(top + i + 1 + opt.margin.text_y, left + 1 + opt.margin.text_x, c.head.s, c.head.n); txt = c.tail; } } void gfx_draw(Gfx *gfx, int cur, Arena *scratch) { 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, gfx_post_width(gfx->posts[i].src), scratch); + for (int i = cur, y = opt.margin.top; i < gfx->len && y < getmaxy(stdscr) - opt.margin.bottom; i++) { + gfx_draw_post(&gfx->posts[i], y, post_left_margin(gfx->posts[i].src), gfx_post_width(gfx->posts[i].src), scratch); y += gfx_post_height(&gfx->posts[i]) + GFX_POST_SPACING; } } @@ -691,15 +722,15 @@ void gfx_draw_user(User *user, Arena *scratch) { int cy = getmaxy(stdscr) >> 1, cx = getmaxx(stdscr) >> 1; const char *binks = cstr_fmt(scratch, "%u binks", user->post_count); - int box_height = 3 + GFX_TEXT_MARGIN_Y; + int box_height = 3 + opt.margin.text_y; int box_width = MAX((int)strlen(binks), MAX(fast_utf8_width(user->name), fast_utf8_width(user->pronouns))); - int bh = MIN(user->banner.lines, getmaxy(stdscr) - 3 - GFX_TEXT_MARGIN_Y * 2 - 4); - int bw = MIN(user->banner.cols, getmaxx(stdscr) - GFX_TEXT_MARGIN_X * 2 - 4); + int bh = MIN(user->banner.lines, getmaxy(stdscr) - 3 - opt.margin.text_y * 2 - 4); + int bw = MIN(user->banner.cols, getmaxx(stdscr) - opt.margin.text_x * 2 - 4); - int total_height = (bh ? bh + 1 : 0) + MAX(box_height, user->pfp.lines) + GFX_TEXT_MARGIN_Y * 2; - int total_width = MAX(bw, user->pfp.cols + 2 + box_width) + GFX_TEXT_MARGIN_X * 2; - gfx_cleared_box(cy - (total_height >> 1) - 2 - GFX_TEXT_MARGIN_Y, cx - (total_width >> 1) - 2 - GFX_TEXT_MARGIN_X, + int total_height = (bh ? bh + 1 : 0) + MAX(box_height, user->pfp.lines) + opt.margin.text_y * 2; + int total_width = MAX(bw, user->pfp.cols + 2 + box_width) + opt.margin.text_x * 2; + gfx_cleared_box(cy - (total_height >> 1) - 2 - opt.margin.text_y, cx - (total_width >> 1) - 2 - opt.margin.text_x, total_height + 4, total_width + 4); cy -= (total_height >> 1); @@ -727,13 +758,13 @@ void gfx_draw_user(User *user, Arena *scratch) { } int y = cy; - mvaddnstr(y++, left + GFX_TEXT_MARGIN_X, user->name.s, user->name.n); + mvaddnstr(y++, left + opt.margin.text_x, user->name.s, user->name.n); if (user->pronouns.n > 0) { color_set(CPAIR_PRONOUNS, 0); - mvaddnstr(y++, left + GFX_TEXT_MARGIN_X, user->pronouns.s, user->pronouns.n); + mvaddnstr(y++, left + opt.margin.text_x, user->pronouns.s, user->pronouns.n); } color_set(CPAIR_TEXT, 0); - mvaddstr(y++, left + GFX_TEXT_MARGIN_X, binks); + mvaddstr(y++, left + opt.margin.text_x, binks); } /* will skip at least one post, so not suitable for just scrolling one line at @@ -856,8 +887,96 @@ void fini_curses(void) { endwin(); } -void load_config(void) { - /* TODO: load cbinkrc */ +/* configuration */ + +FILE *open_config_file(Arena *a) { + const char *s = cstr_fmt(a, "/home/%s/.cbinkrc", getlogin()); + FILE *f = fopen(s, "r/o"); + if (f) return f; + s = cstr_fmt(a, "/home/%s/.config/cbink/cbinkrc", getlogin()); + f = fopen(s, "r/o"); + if (f) return f; + return NULL; +} + +typedef struct { + const char *name; + int (*fn)(Str key, Str value, Arena *perm); +} Section; + +int sect_margin(Str key, Str value, Arena *perm) { + (void)perm; + uint64_t u; + if (str_to_u64(value, &u)) return -1; + if (str_eql(key, S("left"))) { opt.margin.left = u; return 0; } + if (str_eql(key, S("right"))) { opt.margin.right = u; return 0; } + if (str_eql(key, S("top"))) { opt.margin.top = u; return 0; } + if (str_eql(key, S("bottom"))) { opt.margin.bottom = u; return 0; } + if (str_eql(key, S("text.x"))) { opt.margin.text_x = u; return 0; } + if (str_eql(key, S("text.y"))) { opt.margin.text_y = u; return 0; } + return -1; +} + +int sect_post(Str key, Str value, Arena *perm) { + (void)perm; + if (str_eql(key, S("datefmt"))) { opt.post.datefmt = str_to_cstr(value, perm); return 0; } + /* bools */ + uint64_t u; + if (str_to_u64(value, &u)) return -1; + if (u != !!u) return -1; + if (str_eql(key, S("pfp"))) { opt.post.pfp = u; return 0; } + if (str_eql(key, S("pronouns"))) { opt.post.pronouns = u; return 0; } + if (str_eql(key, S("date"))) { opt.post.date = u; return 0; } + return 0; +} + +Section sections[] = { + { "margin", §_margin }, + { "post", §_post }, +}; + +int load_config(Arena *perm, Arena *temp) { + FILE *f = open_config_file(temp); + Str buf = { 0 }; + if (!f) return -1; + if (read_all(f, &buf, temp)) { + fclose(f); + return -1; + } + + int err = 0; + Str line = { 0 }; + int (*fn)(Str key, Str value, Arena *perm) = NULL; + while (next_line(&buf, &line)) { + if (line.n > 2 && line.s[0] == '[' && line.s[line.n - 1] == ']') { + Str sect_name = (Str) { line.s + 1, line.n - 2 }; + fn = NULL; + for (size_t i = 0; i < sizeof sections / sizeof *sections; i++) { + if (str_eql(str_from_cstr(sections[i].name), sect_name)) { + fn = sections[i].fn; + break; + } + } + continue; + } + if (!fn) continue; + line = str_trim(line); + if (!line.n) continue; + if (line.s[0] == '#') continue; + Cut c = str_cut(line, '='); + c.head = str_trim(c.head); + c.tail = str_trim(c.tail); + if (!c.head.n || !c.tail.n) { + err = -1; + continue; + } + if (fn(c.head, c.tail, perm)) { + err = -1; + } + } + + fclose(f); + return err; } /* main */ @@ -865,6 +984,7 @@ void load_config(void) { int main(void) { /* init */ + Arena cfg_arena = { 0 }; Arena post_arena = { 0 }; Arena gfx_arena = { 0 }; Arena temp_arena = { 0 }; @@ -881,7 +1001,7 @@ int main(void) { "([ \t\n]+|^)[@~]?%s([:;,.!? \t\n]|$)", getlogin()), REG_EXTENDED | REG_ICASE | REG_NOSUB); - load_config(); + load_config(&cfg_arena, &temp_arena); init_curses(); posts_refresh(&posts, &post_arena); @@ -962,7 +1082,7 @@ refresh: post_stats.gather_ns / 1000000); goto resize; case '\t': - opt.see.pfp = !opt.see.pfp; + opt.post.pfp = !opt.post.pfp; /* fallthrough */ case KEY_RESIZE: -- cgit v1.2.3