NEWS: Welcome to my new homepage! <3

Initial commit - poet - A terminal-based blogging interface

poet

A terminal-based blogging interface
git clone git://192.168.2.2/poet
Log | Files | Refs | README

commit 9e3340b32018b3c616013e6aec8c1ad6f5c77023
Author: typable <contact@typable.dev>
Date:   Thu, 23 May 2024 13:57:38 +0200

Initial commit

Diffstat:
AMakefile | 11+++++++++++
AREADME.md | 3+++
Asrc/main.c | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/post.c | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/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);