readr

Minimal Terminal RSS Reader
Log | Files | Refs | README | LICENSE

commit fb943984c903d7136cc3d90dc09fde5914d535e5
parent 6e37c6749648df9cf106c68ee038e0680b41873e
Author: citbl <citbl@citbl.org>
Date:   Sat, 13 Dec 2025 21:57:49 +1000

1.10, add suffix such as [video] or [pdf] and don't overlap domains

Diffstat:
Msrc/config.h | 4++--
Msrc/render.c | 24++++++++++++++++++++----
Msrc/utils.h | 3++-
Asrc/utils_derive.c | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 146 insertions(+), 7 deletions(-)

diff --git a/src/config.h b/src/config.h @@ -1,6 +1,6 @@ #pragma once -#define VERSION "v1.9" +#define VERSION "v1.10" // recommended, pass as chrome for reddit feeds #define USER_AGENT \ @@ -47,6 +47,6 @@ // allocation caps, best not to fiddle with this #define POSTS_CAP 128 #define FEEDS_CAP 32 -#define TITLE_CAP 128 +#define TITLE_CAP 128 + 10 #define DOMAIN_CAP 64 #define URL_CAP 8192 diff --git a/src/render.c b/src/render.c @@ -28,7 +28,7 @@ draw_bars(void) } tb_print(1, 0, LOGO_COLOR, BACK_COLOR, "readr"); - tb_print(width - 5, 0, TEXT_COLOR, BACK_COLOR, VERSION); + tb_print(width - 6, 0, TEXT_COLOR, BACK_COLOR, VERSION); tb_print(width - 66, height - 1, TEXT_COLOR, @@ -87,18 +87,34 @@ render(app_t* app) // posts int visible_len = MIN(buffer_height, selected_feed->posts_len); + char title_suffix[10]; for (int j = 0; j < visible_len; j++) { post = selected_feed->posts[j]; - // title - added space for when title and domain overlap - snprintf(title, TITLE_CAP, "%s ", post->title ? post->title : "(bad title"); + const char* title_src = post->title ? post->title : "(bad title)"; + const char* url_filetype = filetype_from_url(post->link); + + if (url_filetype) { + snprintf(title_suffix, 10, "[%s]", url_filetype); + // don't add [video] if the title already ends with [video] etc. + if (!contains(title_src, title_suffix)) { + snprintf(title, TITLE_CAP, "%s %s", title_src, title_suffix); + } else { + snprintf(title, TITLE_CAP, "%s", title_src); + } + } else { + snprintf(title, TITLE_CAP, "%s", title_src); + } // domain const char* domain_host = host_from_url(post->link); - snprintf(domain, DOMAIN_CAP, "%s", domain_host ? domain_host : "(bad title)"); + snprintf(domain, DOMAIN_CAP, "%s", domain_host ? domain_host : "(bad domain)"); int domain_len = (int)strlen(domain); + // chop title to fit within its column and not overlap the domain + snprintf(title, width - domain_len - FEED_CAP - 6, "%s", title); + if (app->selected_panel == 1 && j == app->selected_post) if (post->seen) color = (POST_COLOR | TB_REVERSE | TB_DIM); diff --git a/src/utils.h b/src/utils.h @@ -15,7 +15,8 @@ const char* get_home_dir(void); const char* expand_tilde(const char*); char* read_file(const char*); char* host_from_url(const char*); - +const char* filetype_from_url(const char* url); +int contains(const char* haystack, const char* needle); void nonascii_replace(char*, char); void remove_all_chars(char*, char); void remove_all_tags(char*); diff --git a/src/utils_derive.c b/src/utils_derive.c @@ -0,0 +1,122 @@ +// C11 +#include <ctype.h> +#include <stdlib.h> +#include <string.h> +#include "utils.h" + +static int +ieq_n(const char* a, const char* b, size_t n) +{ + for (size_t i = 0; i < n; i++) { + unsigned char ca = (unsigned char)a[i]; + unsigned char cb = (unsigned char)b[i]; + if (tolower(ca) != tolower(cb)) return 0; + if (ca == '\0') return 1; + } + return 1; +} + +int +contains(const char* haystack, const char* needle) +{ + size_t nlen = strlen(needle); + if (nlen == 0) return 1; + for (const char* p = haystack; *p; p++) { + if (tolower((unsigned char)*p) == tolower((unsigned char)needle[0])) { + if (ieq_n(p, needle, nlen)) return 1; + } + } + return 0; +} + +static const char* +ext_to_type(const char* ext) +{ + // Documents + if (!strcmp(ext, "pdf")) return "pdf"; + if (!strcmp(ext, "doc") || !strcmp(ext, "docx")) return "doc"; + if (!strcmp(ext, "ppt") || !strcmp(ext, "pptx")) return "ppt"; + if (!strcmp(ext, "xls") || !strcmp(ext, "xlsx")) return "xls"; + if (!strcmp(ext, "txt")) return "txt"; + if (!strcmp(ext, "rtf")) return "rtf"; + if (!strcmp(ext, "csv")) return "csv"; + + // Images + if (!strcmp(ext, "jpg") || !strcmp(ext, "jpeg")) return "jpg"; + if (!strcmp(ext, "png")) return "png"; + if (!strcmp(ext, "gif")) return "gif"; + if (!strcmp(ext, "webp")) return "webp"; + if (!strcmp(ext, "heic") || !strcmp(ext, "heif")) return "heic"; + if (!strcmp(ext, "bmp")) return "bmp"; + if (!strcmp(ext, "tif") || !strcmp(ext, "tiff")) return "tif"; + if (!strcmp(ext, "svg")) return "svg"; + + // Video (common on social platforms) + if (!strcmp(ext, "mp4")) return "mp4"; + if (!strcmp(ext, "mov")) return "mov"; + if (!strcmp(ext, "m4v")) return "m4v"; + if (!strcmp(ext, "webm")) return "webm"; + if (!strcmp(ext, "mkv")) return "mkv"; + if (!strcmp(ext, "avi")) return "avi"; + + // Audio + if (!strcmp(ext, "mp3")) return "mp3"; + if (!strcmp(ext, "m4a")) return "m4a"; + if (!strcmp(ext, "aac")) return "aac"; + if (!strcmp(ext, "wav")) return "wav"; + if (!strcmp(ext, "ogg")) return "ogg"; + if (!strcmp(ext, "flac")) return "flac"; + + // Archives (often shared) + if (!strcmp(ext, "zip")) return "zip"; + if (!strcmp(ext, "rar")) return "rar"; + if (!strcmp(ext, "7z")) return "7z"; + + return NULL; +} + +const char* +filetype_from_url(const char* url) +{ + if (!url) { return NULL; } + + // Rule: any youtube.com in domain => "video" + // (Simple heuristic: look for "://...youtube.com" or "youtube.com" early-ish) + if (contains(url, "youtube.com")) return "video"; + if (contains(url, "youtu.be")) return "video"; + if (contains(url, "vimeo.com")) return "video"; + + // Find end of path segment (before ? or #) + const char* q = strchr(url, '?'); + const char* h = strchr(url, '#'); + const char* end = url + strlen(url); + if (q && q < end) end = q; + if (h && h < end) end = h; + + // If URL ends with '/', there's no filename extension + if (end > url && end[-1] == '/') return NULL; + + // Find last '.' in the last path segment + const char* slash = url; + for (const char* p = url; p < end; p++) { + if (*p == '/') slash = p + 1; + } + const char* dot = NULL; + for (const char* p = slash; p < end; p++) { + if (*p == '.') dot = p; + } + if (!dot || dot + 1 >= end) return NULL; + + // Extract extension, lowercase it + size_t ext_len = (size_t)(end - (dot + 1)); + if (ext_len == 0 || ext_len > 16) return NULL; + + char ext[17]; + for (size_t i = 0; i < ext_len; i++) { + ext[i] = (char)tolower((unsigned char)dot[1 + i]); + } + ext[ext_len] = '\0'; + + const char* type = ext_to_type(ext); + return type ? type : NULL; +}