commit 9e3340b32018b3c616013e6aec8c1ad6f5c77023
Author: typable <contact@typable.dev>
Date: Thu, 23 May 2024 13:57:38 +0200
Initial commit
Diffstat:
A | Makefile | | | 11 | +++++++++++ |
A | README.md | | | 3 | +++ |
A | src/main.c | | | 276 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/post.c | | | 276 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/util.c | | | 75 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/util.h | | | 9 | +++++++++ |
6 files changed, 650 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,11 @@
+default: build
+
+install: build
+ cp poet ~/.local/bin/poet
+ rm poet
+
+build:
+ cc -o poet -lpq -lterm -Wall -Wextra -Werror -pedantic src/main.c src/util.c
+
+clean:
+ rm poet
diff --git a/README.md b/README.md
@@ -0,0 +1,3 @@
+# poet
+
+A terminal-based blogging interface
diff --git a/src/main.c b/src/main.c
@@ -0,0 +1,276 @@
+#include <libterm.h>
+#include <stdio.h>
+
+#include "util.h"
+#include "post.c"
+
+#define UNUSED(x) (void)(x)
+#define DELIMITER "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\r\n"
+
+char *POET_DB = NULL;
+
+int post_read(char *filename, post_t **post) {
+ char *content = util_read_file(filename);
+ int len = strlen(content);
+ int delimiter_start = 0;
+ for (int i = 0; i < len; i++) {
+ if (strncmp(content + i, DELIMITER, strlen(DELIMITER)) == 0) {
+ delimiter_start = i;
+ break;
+ }
+ }
+ int delimiter_end = delimiter_start + strlen(DELIMITER);
+ char head[delimiter_start + 1];
+ memcpy(head, content, delimiter_start);
+ head[delimiter_start] = '\0';
+ char *body = malloc(sizeof(char) * (len - delimiter_end + 1));
+ memcpy(body, content + delimiter_end, len - delimiter_end);
+ body[len - delimiter_end] = '\0';
+ free(content);
+ *post = malloc(sizeof(post_t));
+ post_init(*post);
+ int line_nr = 0;
+ int line_start = 0;
+ for (int i = 0; i < (int) strlen(head); i++) {
+ char c = head[i];
+ if (c == '\n') {
+ int line_end = i;
+ if (i > 0 && head[i - 1] == '\r') {
+ line_end--;
+ }
+ char *line = malloc(sizeof(char) * (line_end - line_start + 1));
+ memcpy(line, head + line_start, line_end - line_start);
+ line[line_end - line_start] = '\0';
+ switch (line_nr) {
+ case 0: {
+ (*post)->title = line;
+ break;
+ }
+ case 1: {
+ (*post)->summary = line;
+ break;
+ }
+ case 2: {
+ if (strcmp(line, "true") == 0) {
+ (*post)->is_public = malloc(sizeof(bool));
+ *(*post)->is_public = true;
+ }
+ else {
+ (*post)->is_public = malloc(sizeof(bool));
+ *(*post)->is_public = false;
+ }
+ free(line);
+ break;
+ }
+ default:
+ free(line);
+ break;
+ }
+ line_start = i + 1;
+ line_nr++;
+ }
+ }
+ (*post)->content = body;
+ remove(filename);
+ return 0;
+}
+
+int post_write(char *filename, post_t *post) {
+ FILE *fp = fopen(filename, "aw+");
+ char title[500];
+ snprintf(title, 500, "%s\r\n", post->title);
+ fwrite(title, sizeof(char), strlen(title), fp);
+ char summary[500];
+ snprintf(summary, 500, "%s\r\n", post->summary);
+ fwrite(summary, sizeof(char), strlen(summary), fp);
+ char is_public[500];
+ snprintf(is_public, 500, "%s\r\n", *post->is_public ? "true" : "false");
+ fwrite(is_public, sizeof(char), strlen(is_public), fp);
+ fwrite(DELIMITER, sizeof(char), strlen(DELIMITER), fp);
+ int content_len = strlen(post->content) + 2;
+ char content[content_len];
+ snprintf(content, content_len, "%s", post->content);
+ fwrite(content, sizeof(char), strlen(content), fp);
+ fclose(fp);
+ return 0;
+}
+
+bool post_edit(int id) {
+ post_t *post = db_post_get(POET_DB, id);
+ char filename[100];
+ snprintf(filename, 100, "poet-%d.md", post->id);
+ post_write(filename, post);
+ term_write("\x1b[?25h");
+ term_flush();
+ char *editor = getenv("EDITOR");
+ if (editor == NULL) {
+ editor = "vim";
+ }
+ char command[200];
+ snprintf(command, 200, "%s %s", editor, filename);
+ system(command);
+ term_write("\x1b[?25l");
+ term_flush();
+ post_t *changes = NULL;
+ post_read(filename, &changes);
+ bool changed = post_changed(post, changes);
+ if (changed) {
+ db_post_update(POET_DB, id, post, changes);
+ }
+ free(changes);
+ post_free(post);
+ return changed;
+}
+
+int main(void) {
+ POET_DB = getenv("POET_DB");
+ if (POET_DB == NULL) {
+ printf("Environment variable 'POET_DB' needs to be set!\n");
+ return 1;
+ }
+ term_enable_raw_mode();
+ term_write("\x1b[?25l");
+ bool quit = false;
+ bool update = true;
+ bool render = true;
+ int index = 0;
+ int offset = 0;
+ int height = 0;
+ post_result_t *post_result = NULL;
+ while (!quit) {
+ if (update) {
+ post_result = db_post_search(POET_DB, "");
+ update = false;
+ }
+ if (render) {
+ int rows = 0;
+ int cols = 0;
+ term_read_window_size(&rows, &cols);
+ // TODO: adjust offset after resize
+ height = rows - 4;
+ term_write("\x1b[3J");
+ term_write("\x1b[2J");
+ term_write("\x1b[H");
+ term_write("\r\n q = quit, j/k = move, g = goto, n = create, i = edit, d = delete, x = public/private\r\n\r\n");
+ for (int i = 0; i < post_result->len; i++) {
+ if (i == height) {
+ break;
+ }
+ post_t *post = &post_result->result[i + offset];
+ if (i + offset == index) {
+ term_write(" \x1b[38;2;255;221;71m>>\x1b[39m ");
+ }
+ else {
+ term_write(" ");
+ }
+ if (*post->is_public) {
+ term_writef("\x1b[38;2;255;255;255m%s\x1b[39m", post->title);
+ }
+ else {
+ term_writef("%s", post->title);
+ }
+ char *date = util_format_json_date(post->date, "%Y-%m-%d");
+ term_writef(" \x1b[38;2;124;182;84m(%s)\x1b[39m\r\n", date);
+ free(date);
+ }
+ term_flush();
+ render = false;
+ }
+ int c = term_read_key();
+ switch (c) {
+ case 'q': {
+ quit = true;
+ break;
+ }
+ case 'j': {
+ if (index + 1 < post_result->len) {
+ index++;
+ if (index - offset == height) {
+ offset++;
+ }
+ render = true;
+ }
+ break;
+ }
+ case 'k': {
+ if (index > 0) {
+ index--;
+ if (index < offset) {
+ offset--;
+ }
+ render = true;
+ }
+ break;
+ }
+ case 'i': {
+ if (post_edit(post_result->result[index].id)) {
+ update = true;
+ render = true;
+ }
+ break;
+ }
+ case 'd': {
+ // TODO delete
+ break;
+ }
+ case 'n': {
+ // TODO create
+ break;
+ }
+ case '/': {
+ // TODO search
+ break;
+ }
+ case 'x': {
+ post_t *post = &post_result->result[index];
+ post_t *changes = malloc(sizeof(post_t));
+ post_init(changes);
+ changes->is_public = malloc(sizeof(bool));
+ *changes->is_public = !*post->is_public;
+ if (db_post_update(POET_DB, post->id, post, changes)) {
+ render = true;
+ update = true;
+ }
+ free(changes);
+ break;
+ }
+ case 'g': {
+ int c = term_read_key();
+ switch (c) {
+ case 'e': {
+ if (post_result->len > 1) {
+ index = post_result->len - 1;
+ offset = index - height + 1;
+ }
+ else {
+ index = 0;
+ offset = 0;
+ }
+ render = true;
+ break;
+ }
+ case 'g': {
+ index = 0;
+ offset = 0;
+ render = true;
+ break;
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ if (post_result != NULL) {
+ post_result_free(post_result);
+ }
+ term_write("\x1b[3J");
+ term_write("\x1b[2J");
+ term_write("\x1b[?25h");
+ term_write("\x1b[H");
+ term_flush();
+ return 0;
+}
diff --git a/src/post.c b/src/post.c
@@ -0,0 +1,276 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <libpq-fe.h>
+
+#include "util.h"
+
+typedef struct {
+ int id;
+ char *title;
+ char *summary;
+ char *content;
+ char *date;
+ bool *is_public;
+} post_t;
+
+typedef struct {
+ post_t *result;
+ int len;
+} post_result_t;
+
+void post_init(post_t *post) {
+ post->title = NULL;
+ post->summary= NULL;
+ post->content= NULL;
+ post->date = NULL;
+ post->is_public = NULL;
+}
+
+void post_free(post_t *post) {
+ if (post->title != NULL) {
+ free(post->title);
+ }
+ if (post->summary != NULL) {
+ free(post->summary);
+ }
+ if (post->content != NULL) {
+ free(post->content);
+ }
+ if (post->date != NULL) {
+ free(post->date);
+ }
+ if (post->is_public != NULL) {
+ free(post->is_public);
+ }
+}
+
+void post_result_free(post_result_t *post_result) {
+ if (post_result->result != NULL) {
+ for (int i = 0; i < post_result->len; i++) {
+ post_t *post = &post_result->result[i];
+ post_free(post);
+ }
+ free(post_result->result);
+ }
+}
+
+bool post_changed(post_t *post, post_t *changes) {
+ if (changes->title != NULL && strcmp(post->title, changes->title) != 0) {
+ return true;
+ }
+ if (changes->summary != NULL && strcmp(post->summary, changes->summary) != 0) {
+ return true;
+ }
+ if (changes->content != NULL && strcmp(post->content, changes->content) != 0) {
+ return true;
+ }
+ if (changes->date != NULL && strcmp(post->date, changes->date) != 0) {
+ return true;
+ }
+ if (post->is_public != NULL && *post->is_public != *changes->is_public) {
+ return true;
+ }
+ return false;
+}
+
+post_result_t *db_post_search(char *db, char *title) {
+ PGconn *conn = PQconnectdb(db);
+ if (PQstatus(conn) != CONNECTION_OK) {
+ printf("error: %s\n", PQerrorMessage(conn));
+ PQfinish(conn);
+ return NULL;
+ }
+ char *query = "SELECT id, title, summary, created_at, is_public FROM posts WHERE lower(title) LIKE lower($1) ORDER BY created_at DESC;";
+ char id_param[10];
+ snprintf(id_param, 10, "%%%s%%", title);
+ const char *values[1] = { id_param };
+ PGresult *result = PQexecParams(conn, query, 1, NULL, values, NULL, NULL, 0);
+ ExecStatusType status = PQresultStatus(result);
+ if (status != PGRES_TUPLES_OK) {
+ printf("error: %s\n", PQerrorMessage(conn));
+ PQclear(result);
+ PQfinish(conn);
+ return NULL;
+ }
+ int rows = PQntuples(result);
+ int cols = PQnfields(result);
+ if (rows > 0) {
+ post_result_t *post_result = calloc(1, sizeof(post_result_t));
+ for (int i = 0; i < rows; i++) {
+ post_result->result = realloc(post_result->result, sizeof(post_t) * (post_result->len + 1));
+ post_t *post = &post_result->result[post_result->len];
+ post_init(post);
+ post_result->len++;
+ for (int j = 0; j < cols; j++) {
+ char *value = PQgetvalue(result, i, j);
+ int value_len = strlen(value);
+ if (j == 0) {
+ int id = strtoimax(value, NULL, 10);
+ post->id = id;
+ }
+ if (j == 1) {
+ post->title = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->title, value, value_len);
+ post->title[value_len] = '\0';
+ }
+ if (j == 2) {
+ post->summary = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->summary, value, value_len);
+ post->summary[value_len] = '\0';
+ }
+ if (j == 3) {
+ post->date = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->date, value, value_len);
+ post->date[value_len] = '\0';
+ }
+ if (j == 4) {
+ if (strcmp(value, "t") == 0) {
+ post->is_public = malloc(sizeof(bool));
+ *post->is_public = true;
+ }
+ else {
+ post->is_public = malloc(sizeof(bool));
+ *post->is_public = false;
+ }
+ }
+ }
+ }
+ PQclear(result);
+ PQfinish(conn);
+ return post_result;
+ }
+ PQclear(result);
+ PQfinish(conn);
+ return NULL;
+}
+
+post_t *db_post_get(char *db, int id) {
+ PGconn *conn = PQconnectdb(db);
+ if (PQstatus(conn) != CONNECTION_OK) {
+ printf("error: %s\n", PQerrorMessage(conn));
+ PQfinish(conn);
+ return NULL;
+ }
+ char *query = "SELECT title, summary, content, created_at, is_public FROM posts WHERE id = $1;";
+ char id_param[10];
+ snprintf(id_param, 10, "%d", id);
+ const char *values[1] = { id_param };
+ PGresult *result = PQexecParams(conn, query, 1, NULL, values, NULL, NULL, 0);
+ ExecStatusType status = PQresultStatus(result);
+ if (status != PGRES_TUPLES_OK) {
+ printf("error: %s\n", PQerrorMessage(conn));
+ PQclear(result);
+ PQfinish(conn);
+ return NULL;
+ }
+ int rows = PQntuples(result);
+ int cols = PQnfields(result);
+ if (rows > 0) {
+ post_t *post = calloc(1, sizeof(post_t));
+ post->id = id;
+ for (int j = 0; j < cols; j++) {
+ char *value = PQgetvalue(result, 0, j);
+ int value_len = strlen(value);
+ if (j == 0) {
+ post->title = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->title, value, value_len);
+ post->title[value_len] = '\0';
+ }
+ if (j == 1) {
+ post->summary = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->summary, value, value_len);
+ post->summary[value_len] = '\0';
+ }
+ if (j == 2) {
+ post->content = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->content, value, value_len);
+ post->content[value_len] = '\0';
+ }
+ if (j == 3) {
+ post->date = malloc(sizeof(char) * (value_len + 1));
+ memcpy(post->date, value, value_len);
+ post->date[value_len] = '\0';
+ }
+ if (j == 4) {
+ if (strcmp(value, "t") == 0) {
+ post->is_public = malloc(sizeof(bool));
+ *post->is_public = true;
+ }
+ else {
+ post->is_public = malloc(sizeof(bool));
+ *post->is_public = false;
+ }
+ }
+ }
+ PQclear(result);
+ PQfinish(conn);
+ return post;
+ }
+ PQclear(result);
+ PQfinish(conn);
+ return NULL;
+}
+
+bool db_post_update(char *db, int id, post_t *post, post_t *changes) {
+ PGconn *conn = PQconnectdb(db);
+ if (PQstatus(conn) != CONNECTION_OK) {
+ printf("error: %s\n", PQerrorMessage(conn));
+ PQfinish(conn);
+ return false;
+ }
+ char *query = NULL;
+ const char *params[10];
+ int count = 0;
+ str_append(&query, "UPDATE posts SET");
+ if (changes->title != NULL && strcmp(post->title, changes->title) != 0) {
+ if (count > 0) {
+ str_append(&query, ",");
+ }
+ str_appendf(&query, " title = $%d", count + 1);
+ params[count] = changes->title;
+ count++;
+ }
+ if (changes->summary != NULL && strcmp(post->summary, changes->summary) != 0) {
+ if (count > 0) {
+ str_append(&query, ",");
+ }
+ str_appendf(&query, " summary = $%d", count + 1);
+ params[count] = changes->summary;
+ count++;
+ }
+ if (changes->content != NULL && strcmp(post->content, changes->content) != 0) {
+ if (count > 0) {
+ str_appendf(&query, ",");
+ }
+ str_appendf(&query, " content = $%d", count + 1);
+ params[count] = changes->content;
+ count++;
+ }
+ if (post->is_public != NULL && *post->is_public != *changes->is_public) {
+ if (count > 0) {
+ str_appendf(&query, ",");
+ }
+ str_appendf(&query, " is_public = $%d", count + 1);
+ params[count] = *changes->is_public ? "true" : "false";
+ count++;
+ }
+ str_appendf(&query, " WHERE id = $%d", count + 1);
+ char id_param[10];
+ snprintf(id_param, 10, "%d", id);
+ params[count] = id_param;
+ count++;
+ PGresult *result = PQexecParams(conn, query, count, NULL, params, NULL, NULL, 0);
+ free(query);
+ ExecStatusType status = PQresultStatus(result);
+ if (status != PGRES_COMMAND_OK) {
+ printf("error: %s\n", PQerrorMessage(conn));
+ PQclear(result);
+ PQfinish(conn);
+ return false;
+ }
+ PQclear(result);
+ PQfinish(conn);
+ return true;
+}
diff --git a/src/util.c b/src/util.c
@@ -0,0 +1,75 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <time.h>
+
+char *util_read_file(char *filename) {
+ FILE *fp = fopen(filename, "r+");
+ fseek(fp, 0, SEEK_END);
+ long len = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+ char *content = malloc(sizeof(char) * (len + 1));
+ fread(content, sizeof(char), len, fp);
+ content[len] = '\0';
+ fclose(fp);
+ return content;
+}
+
+char *util_format_json_date(char *json_date, char *format) {
+ time_t now = time(NULL);
+ struct tm *tm = localtime(&now);
+ int year;
+ int month;
+ int day;
+ sscanf(json_date, "%d-%d-%d", &year, &month, &day);
+ tm->tm_year = year - 1900;
+ tm->tm_mon = month - 1;
+ tm->tm_mday = day;
+ char buffer[100];
+ int len = strftime(buffer, 100, format, tm);
+ char *date = malloc(sizeof(char) * (len + 1));
+ memcpy(date, buffer, len);
+ date[len] = '\0';
+ return date;
+}
+
+void str_append_len(char **buffer, char *value, int len) {
+ int buffer_len = 0;
+ if (*buffer != NULL) {
+ buffer_len = strlen(*buffer);
+ }
+ *buffer = realloc(*buffer, sizeof(char) * (buffer_len + len + 1));
+ memcpy(*buffer + buffer_len, value, len);
+ (*buffer)[buffer_len + len] = '\0';
+}
+
+void str_append(char **buffer, char *value) {
+ int len = strlen(value);
+ str_append_len(buffer, value, len);
+}
+
+void str_appendf(char **buffer, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ char str[1000];
+ vsnprintf(str, sizeof(str), format, args);
+ str_append(buffer, str);
+ va_end(args);
+}
+
+char *str_replace_all(char *buffer, char *str, char *replacement) {
+ char *result = NULL;
+ int str_len = strlen(str);
+ for (int i = 0; i < (int) strlen(buffer); i++) {
+ char *ptr = &buffer[i];
+ if (strncmp(ptr, str, str_len) == 0) {
+ str_append(&result, replacement);
+ i += str_len - 1;
+ }
+ else {
+ str_append_len(&result, &buffer[i], 1);
+ }
+ }
+ return result;
+}
diff --git a/src/util.h b/src/util.h
@@ -0,0 +1,9 @@
+#pragma once
+
+char *util_read_file(char *filename);
+char *util_format_json_date(char *json_date, char *format);
+
+void str_append_len(char **buffer, char *value, int len);
+void str_append(char **buffer, char *value);
+void str_appendf(char **buffer, const char *format, ...);
+char *str_replace_all(char *buffer, char *str, char *replacement);