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:
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;
+}