payredu

Cross-platform ledger GUI written in c99
git clone git@getsh.org:payredu.git
Log | Files | Refs | README

commit 16ed12c0750f417facc9e2b5c2d096240016af4f
parent 4981c3dbef0b5a7e92806ea2c3c37f64098fe5e2
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date:   Mon, 18 Dec 2023 01:45:23 +0530

Use separate book.c and account.c for definitions

Fix Makefile test setup

Rename to payredu

Update README

Update .gitigonre

Diffstat:
M.gitignore | 5+++++
MCHANGELOG | 1+
MMakefile | 27+++++++++++----------------
MREADME | 19++++++++++---------
Aaccount.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Maccount.h | 65++---------------------------------------------------------------
Mbook.c | 72++++++++++++++++++++++++++++++++----------------------------------------
Mbook.h | 25+++++++------------------
Dhotbook.c | 56--------------------------------------------------------
Apayredu.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/account_tree.c | 47+++++++++++++++++++++++++++++++++++++++++++++++
Atests/csv_export.c | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/tests.mk | 11+++++++++++
13 files changed, 321 insertions(+), 202 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -4,3 +4,8 @@ hotbook *.so *.o tags + +tests/* +!tests/tests.mk +!tests/account_tree.c +!tests/csv_export.c diff --git a/CHANGELOG b/CHANGELOG @@ -3,3 +3,4 @@ Dark Ages - Starting keeping CHANGELOG file for payredo - Basic combined parser/lexer for the ledger format has been implemented in book.c +- Fix Makefile issue with tests/tests.mk diff --git a/Makefile b/Makefile @@ -1,13 +1,14 @@ GENERAL_FLAGS=-fPIC LDFLAGS:=$(GENERAL_FLAGS) -lglfw -lGL -lm CFLAGS:=$(GENERAL_FLAGS) -O0 -I. -g -Werror #-Wpedantic +export LD_LIBRARY_PATH=. -.DEFAULT_GOAL=book +.DEFAULT_GOAL=payredu CC=gcc -%: %.c account.h - $(CC) -o $@ $(CFLAGS) $(LDFLAGS) $< +%: %.c libbook.so + $(CC) -o $@ $(CFLAGS) $(LDFLAGS) libbook.so $< %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ @@ -15,16 +16,16 @@ CC=gcc bal: ledger -f october-2023.txt bal -lib%.so: %.o - $(CC) -shared -Wl,-soname,$@ -o $@ $< - balance: balance.c ledger.h hot: hot.c libbalance.so -libbook.so: book.c book.h +libbook.a: book.c book.h account.c account.h + +libbook.so: book.o account.o + $(CC) -shared -Wl,-soname,$@ -o $@ $^ -hotbook: hotbook.c +payredu: payredu.c libbook.so refresh: git ls-files | entr sh hot.sh @@ -32,13 +33,7 @@ refresh: format: indent -psl -pal --use-tabs -ts4 -br -brs -ce -cli0 book.c -awk_query = $$(awk '/\/\* $1/{flag=1; next}/$1 \*\//{flag=0}flag' $2.c) -test_cmd = if [ "$$($< $(call awk_query,TEST_INPUT,$<))" = "$(call awk_query,TEST_OUTPUT,$<)" ]; \ - then echo Passed; \ - else echo Failed; \ - fi - -#include tests/tests.mk +include tests/tests.mk clean: - -rm *.so *.o hotbook libbook.so test/account_tree + -rm *.so *.o hotbook libbook.so $(TESTS) diff --git a/README b/README @@ -1,24 +1,25 @@ -payeredo -======== +payredu +======= பேரேடு ====== -payeredo is a cross-platform frontend to ledger(pta) with emphasis on simplicity. perudu means ledger in Tamil. It is written in c99 and works on top of nuklear making it lightweight and fast. +payredu is a cross-platform frontend to ledger(pta) with emphasis on simplicity. perudu means ledger in Tamil. It is written in c99 and works on top of nuklear making it lightweight and fast. NOTE: The quality of the software is beta in the least, it's still in development. -For now the following commands work, +To build and run, - $ ./hotbook + $ make + $ ./payredu -Why payeredo when ledger-cli exists? +Why payredu when ledger-cli exists? ------------------------------------ -ledger-cli itself pretty lightweight but it has a handful of dependencies and features which I don't particularly use. +ledger-cli itself pretty lightweight but it has a handful of dependencies and features which I don't particularly care about. -payeredo follows a very suckless approach to ledger and does NOT provide some of the advanced features ledger provides. +payredu follows a very suckless approach to ledger and does NOT provide some of the advanced features ledger provides. -It should be noted that payeredo is usually faster than ledger-cli, it does not provide some of the niceties that ledger-cil provides. +It should be noted that payredu is usually faster than ledger-cli as it does not provide some of the niceties that ledger-cil provides. Goals ----- diff --git a/account.c b/account.c @@ -0,0 +1,72 @@ +#include <account.h> + +#include <stdlib.h> + +// TODO make this dynamic +size_t tree_depth = 4; + + +map_tree_t *account_search(map_tree_t *rootp, char *acc, size_t acc_size) +{ + assert(rootp != NULL); + // we hit leaf node, return rootp + if (rootp->children == NULL) return rootp; + + // return rootp when the 'acc' matches exactly with rootp->value + // acc: this, rootp->value: this + vstr_t *rk = rootp->value; + if (rk != NULL && acc_size == rk->len && (strncmp(acc, rk->str, acc_size) == 0)) { + return rootp; + } + + // search the string in it's children + for (size_t i = 0; i < rootp->children_len; i++) { + vstr_t *val = rootp->children[i].value; + if (val != NULL && acc_size == val->len && (strncmp(acc, val->str, acc_size) == 0)) { + return rootp->children + i; + } + } + return NULL; +} + +int account_add(map_tree_t **rootp, char *acc, size_t acc_size) +{ + size_t records_needed = tree_depth * 4; + if (*rootp == NULL) { + *rootp = malloc(sizeof(map_tree_t)); + } + if ((*rootp)->children == NULL) { + (*rootp)->children = + (map_tree_t *) calloc(records_needed, sizeof(map_tree_t)); + (*rootp)->children_len = 0; + (*rootp)->children_cap = records_needed; + } + map_tree_t* _rootp = *rootp; + size_t i = 0; + while (i < acc_size) { + if (acc[i] == ':' || i + 1 == acc_size) { + size_t j = i + 1; + map_tree_t *current_node = account_search(_rootp, acc, j); + if (current_node == NULL) { + // return the previously allocated child + current_node = _rootp->children + _rootp->children_len++; + // current_node->value is NULL when the search fails + // we have to set the value now + // TODO maybe save vstrs in a pool and use them, would provide a sane way to free memory + vstr_t *vstr = (vstr_t *) malloc(sizeof(vstr_t)); + vstr->str = acc; + vstr->len = j; + current_node->value = vstr; + //printf("%zu : %zu %d %.*s\n", current_node, vstr, j, j, acc); + } else { + //printf("Present already= %d %.*s\n", j, j, acc); + } + if (j != acc_size) { + return account_add(&current_node, acc + j, + acc_size - j); + } + } + i++; + } + return -1; +} diff --git a/account.h b/account.h @@ -6,8 +6,6 @@ #include <vstr.h> -size_t tree_depth = 4; - struct map_tree; struct map_tree { @@ -27,69 +25,10 @@ typedef struct map_tree map_tree_t; // TODO handle both rootp,this:is:us case and rootp->children,is:us case // Currently only the rootp->value and acc are compared -map_tree_t *account_search(map_tree_t *rootp, char *acc, size_t acc_size) -{ - assert(rootp != NULL); - // we hit leaf node, return rootp - if (rootp->children == NULL) return rootp; - // return rootp when the 'acc' matches exactly with rootp->value - // acc: this, rootp->value: this - vstr_t *rk = rootp->value; - if (rk != NULL && acc_size == rk->len && (strncmp(acc, rk->str, acc_size) == 0)) { - return rootp; - } +map_tree_t *account_search(map_tree_t *rootp, char *acc, size_t acc_size); - // search the string in it's children - for (size_t i = 0; i < rootp->children_len; i++) { - vstr_t *val = rootp->children[i].value; - if (val != NULL && acc_size == val->len && (strncmp(acc, val->str, acc_size) == 0)) { - return rootp->children + i; - } - } - return NULL; -} -int account_add(map_tree_t **rootp, char *acc, size_t acc_size) -{ - size_t records_needed = tree_depth * 4; - if (*rootp == NULL) { - *rootp = malloc(sizeof(map_tree_t)); - } - if ((*rootp)->children == NULL) { - (*rootp)->children = - (map_tree_t *) calloc(records_needed, sizeof(map_tree_t)); - (*rootp)->children_len = 0; - (*rootp)->children_cap = records_needed; - } - map_tree_t* _rootp = *rootp; - size_t i = 0; - while (i < acc_size) { - if (acc[i] == ':' || i + 1 == acc_size) { - size_t j = i + 1; - map_tree_t *current_node = account_search(_rootp, acc, j); - if (current_node == NULL) { - // return the previously allocated child - current_node = _rootp->children + _rootp->children_len++; - // current_node->value is NULL when the search fails - // we have to set the value now - // TODO maybe save vstrs in a pool and use them, would provide a sane way to free memory - vstr_t *vstr = (vstr_t *) malloc(sizeof(vstr_t)); - vstr->str = acc; - vstr->len = j; - current_node->value = vstr; - //printf("%zu : %zu %d %.*s\n", current_node, vstr, j, j, acc); - } else { - //printf("Present already= %d %.*s\n", j, j, acc); - } - if (j != acc_size) { - return account_add(&current_node, acc + j, - acc_size - j); - } - } - i++; - } - return -1; -} +int account_add(map_tree_t **rootp, char *acc, size_t acc_size); #endif diff --git a/book.c b/book.c @@ -27,8 +27,8 @@ #include <time.h> #include "vstr.h" -#include "account.h" -#include "book.h" +#include <account.h> +#include <book.h> #define BUFFER_SIZE 256 @@ -159,30 +159,31 @@ void ledger_parse_data(char *text, size_t text_len) case '\r': line_no++; n_count++; - printf("\n%d| ", line_no); switch (state) { // after parsing the amount seq, we set the state to ENTRY_WHO case ENTRY_WHO: case ENTRY_END: - warning("----- Entry End Marked -----\n"); hold_sign = -1; hold_amount = LONG_MAX; // if entry_count <= 1 throw error if (text[i - 1] == '\n') { state = DATE; // TODO push the entries to stack or somethin - warning("----- Posting End Marked -----\n"); + warning("\n==\n"); // state = POSTING_END; } else { state = ENTRY_WHO; + warning(","); } break; case COMMENT: + state = ENTRY_START; + break; case ENTRY_SIGN_DENOM_AMOUNT: state = ENTRY_WHO; break; case ENTRY_DENOM: - warningf("%s\n", "denom not found, setting state WHO"); + //warningf("%s", "denom not found, setting state WHO"); state = ENTRY_WHO; break; case ENTRY_AMOUNT: @@ -205,8 +206,7 @@ void ledger_parse_data(char *text, size_t text_len) if (isdigit(c)) { // try to parse a date time_t tn = ledger_timestamp_from_ledger_date(text + i); - warningf("date str: %.*s\n", 10, text + i); - warningf("date: %ld\n", tn); + warningf("%.*s: %ld ", 10, text + i, tn); // date is expected to have the form DD/MM/YYYY (10) i += 10; if (tn == (time_t) - 1) goto ledger_parse_error_handle; @@ -226,23 +226,12 @@ void ledger_parse_data(char *text, size_t text_len) comment_len++; } comment.len = comment_len; - warningf("Comment: %.*s\n", comment_len, + warningf("Comment: %.*s", comment_len, comment); - state = ENTRY_WHO; - } - break; - case ENTRY_SPACE: - { - size_t original_i = i; - while (i < text_len && isspace(text[i])) i++; - int wsc = i - original_i; - warningf("i: %ld, Spaces: %d\n", i, wsc); - if (wsc < 2) { - goto ledger_parse_error_handle; - } - state = ENTRY_WHO; + state = ENTRY_START; } break; + case ENTRY_START: case ENTRY_WHO: { // add this to register @@ -268,9 +257,8 @@ void ledger_parse_data(char *text, size_t text_len) } ledger_who_parsed: who_len = i - who_len; - warningf("parsed: i=%d\n", i); account_add(&rootp, who.str, who_len); - warningf("i=%d, Who: %.*s\n", i, who_len, who); + warningf("\n(%d) Who: %.*s", i, who_len, who); state = ENTRY_SIGN_DENOM_AMOUNT; // add to tags here } @@ -302,7 +290,7 @@ ledger_who_parsed: } break; case ENTRY_DENOM: { char _c; - warningf("denom-i: %d\n", i + 1); + warningf(" %d: D:", i + 1); char *denom = text + i; size_t denom_len = 0; while (i < text_len && @@ -313,12 +301,12 @@ ledger_who_parsed: state = hold_sign? ENTRY_AMOUNT: ENTRY_SIGN_AMOUNT; else state = ENTRY_END; - warningf("%d> len: %d, denom: %.*s\n", i, denom_len, denom_len, denom); + warningf(" %.*s(%d)", denom_len, denom, denom_len); break; } case ENTRY_AMOUNT: { char _c; - warningf("amount-i: %d\n", i + 1); + warningf(" %d A:", i + 1); char *amount = text + i; size_t amount_len = 0; while (i < text_len && (_c = *(text + i)) == '.' || isdigit(_c) || _c == ',') i++; @@ -326,7 +314,7 @@ ledger_who_parsed: // TODO convert amount to hold_amount integer hold_amount = 0; state = hold_denom_id == 0? ENTRY_DENOM : ENTRY_END; - warningf("%d> len: %d, amount: %.*s\n", i, amount_len, amount_len, amount); + warningf(" %.*s(%d)", amount_len, amount, amount_len); } break; default: @@ -339,19 +327,23 @@ ledger_parse_error_handle: warningf("Parse failed at %ld b:(%d), Expected %s, got '%c'", line_no, i, states_str[state], text[i]); } - -int main(int argc, char* argv[]) { - FILE* in = fopen("october-2023.txt", "r"); - char* data = (char*)malloc(2048 * sizeof(char)); - size_t data_size = 0; - size_t c_read = 0; - while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) { - data_size += c_read; +Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end) { + Entity me = {"Account:Income"}; + // list population, read from + FILE* file = fopen(filename, "r"); + if (file == NULL) { + printf("Failed to open file %s\n", filename); + } + Entry** new_list = (Entry**)malloc(sizeof(Entry*) * 12); + for(int i = 0; i < 12; i++) { + Entry* entry = (Entry*)malloc(sizeof(Entry)); + new_list[i] = entry; + entry->from = &me; + entry->to = (Entity*)malloc(sizeof(Entity)); + entry->to->name = (char*)malloc(sizeof(char*) * 20); + strcpy(entry->to->name, "Man"); } - if (ferror(in)) fprintf(stderr, "Error reading file\n"); - fprintf(stdout, "Startig loop\n"); - ledger_parse_data(data, data_size); - return 0; + return new_list; } void *module_main(char *data, size_t data_len) diff --git a/book.h b/book.h @@ -1,3 +1,6 @@ +#ifndef _LEDGER_BOOK_H +#define _LEDGER_BOOK_H + #include <string.h> #include <stdlib.h> @@ -11,23 +14,9 @@ typedef struct { long long amt; } Entry; -Entity me = {"Account:Income"}; -Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end) { - // list population, read from - FILE* file = fopen(filename, "r"); - if (file == NULL) { - printf("Failed to open file %s\n", filename); - } - Entry** new_list = (Entry**)malloc(sizeof(Entry*) * 12); - for(int i = 0; i < 12; i++) { - Entry* entry = (Entry*)malloc(sizeof(Entry)); - new_list[i] = entry; - entry->from = &me; - entry->to = (Entity*)malloc(sizeof(Entity)); - entry->to->name = (char*)malloc(sizeof(char*) * 20); - strcpy(entry->to->name, "Man"); - } - return new_list; -} +void ledger_parse_data(char *text, size_t text_len); + +Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end); +#endif diff --git a/hotbook.c b/hotbook.c @@ -1,56 +0,0 @@ - -#include <stdio.h> -#include <stdlib.h> -#include <dlfcn.h> -#include <signal.h> -#include <unistd.h> - -#include "common.h" - -#define MAX_MEMORY 4064 -#define WINDOW_WIDTH 512 -#define WINDOW_HEIGHT 512 -#define BUFFER_SIZE 256 - -int should_exit = 0; - -void sig_handle() { - printf("Reloaded\n"); - system("date"); - should_exit = 1; -} - -typedef void* (*module_main_func)(const char*, size_t); - -int main(int argc, char* argv[]) { - signal(SIGQUIT, sig_handle); - //while(1) { - void* module = dlopen("./libbook.so", RTLD_NOW); - while(module == NULL){ - fprintf(stderr, "Failed to load module. (%s)\n", dlerror()); - fprintf(stderr, "Press return to try again.\n"); - getchar(); - module = dlopen("./libbook.so", RTLD_NOW); - } - module_main_func module_main = dlsym(module, "module_main"); - FILE* in = fopen("october-2023.txt", "r"); - char* data = (char*)malloc(2048 * sizeof(char)); - size_t data_size = 0; - size_t c_read = 0; - while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) { - data_size += c_read; - } - if (ferror(in)) fprintf(stderr, "Error reading file\n"); - fprintf(stdout, "Startig loop\n"); - module_main(data, data_size); - - while(should_exit == 0) { - sleep(1); - } - should_exit = 0; - dlclose(module); - fprintf(stderr, "Continue?\n"); - //} - - return 0; -} diff --git a/payredu.c b/payredu.c @@ -0,0 +1,72 @@ +#include <stdio.h> +#include <stdlib.h> +#include <dlfcn.h> +#include <signal.h> +#include <unistd.h> + +#include <book.h> + +#define MAX_MEMORY 4064 +#define WINDOW_WIDTH 512 +#define WINDOW_HEIGHT 512 +#define BUFFER_SIZE 256 + +int should_exit = 0; + +void sig_handle() { + printf("Reloaded\n"); + system("date"); + should_exit = 1; +} + +typedef void* (*module_main_func)(const char*, size_t); + +int main(int argc, char* argv[]) { + FILE* in = fopen("october-2023.txt", "r"); + char* data = (char*)malloc(2048 * sizeof(char)); + size_t data_size = 0; + size_t c_read = 0; + while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) { + data_size += c_read; + } + if (ferror(in)) fprintf(stderr, "Error reading file\n"); + fprintf(stdout, "Startig loop\n"); + ledger_parse_data(data, data_size); + return 0; +} + + +/* +int main(int argc, char* argv[]) { + signal(SIGQUIT, sig_handle); + //while(1) { + void* module = dlopen("./libbook.so", RTLD_NOW); + while(module == NULL){ + fprintf(stderr, "Failed to load module. (%s)\n", dlerror()); + fprintf(stderr, "Press return to try again.\n"); + getchar(); + module = dlopen("./libbook.so", RTLD_NOW); + } + module_main_func module_main = dlsym(module, "module_main"); + FILE* in = fopen("october-2023.txt", "r"); + char* data = (char*)malloc(2048 * sizeof(char)); + size_t data_size = 0; + size_t c_read = 0; + while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) { + data_size += c_read; + } + if (ferror(in)) fprintf(stderr, "Error reading file\n"); + fprintf(stdout, "Startig loop\n"); + module_main(data, data_size); + + while(should_exit == 0) { + sleep(1); + } + should_exit = 0; + dlclose(module); + fprintf(stderr, "Continue?\n"); + //} + + return 0; +} +*/ diff --git a/tests/account_tree.c b/tests/account_tree.c @@ -0,0 +1,47 @@ +#include <stdio.h> +#include <stdlib.h> + +#include <account.h> + +/* TEST_INPUT +this:is:a:account +virtualaccount: +virtualaccount +this:is: +TEST_INPUT */ + +void account_walk (map_tree_t* rootp) +{ + static int tab_acc; + if (rootp == NULL) return; + for(int i = 0; i < tab_acc; i++) { + printf(" "); + } + vstr_t *val = rootp->value; + if (val != NULL) { + printf("%.*s\n", val->len, val->str); + tab_acc++; + } + for (int i = 0; i < rootp->children_len; i++) { + account_walk(rootp->children + i); + } + tab_acc--; +} + + +int main(int argc, char* argv[]) { + map_tree_t* account_tree = NULL; + for(int i = 1; i < argc; i++) { + account_add(&account_tree, argv[i], strlen(argv[i])); + } + account_walk(account_tree); +} + +/* TEST_OUTPUT +this: + is: + a: + account +virtualaccount: +virtualaccount +TEST_OUTPUT */ diff --git a/tests/csv_export.c b/tests/csv_export.c @@ -0,0 +1,51 @@ +#include <stdio.h> +#include <book.h> + +/* TEST_INPUT +october-2023.txt +TEST_INPUT */ + +int main(int argc, char* argv[]) { + char* content = (char*)malloc(10000); + size_t content_len = 0; + //if (argc < 2) { + // fprintf(stderr, "Usage: %s filename.txt\n", argv[0]); + // return 1; + //} + //FILE* fp = fopen(argv[1], "r"); + FILE* fp = fopen("october-2023.txt", "r"); + if (fp == NULL) { + printf("Cannot open file\n"); + goto export_csv_cleanup; + } + size_t content_read = 0; + char* content_ptr = content; + while ( (content_read = fread(content_ptr, 1, 1, fp)) != 0) { + content_ptr += content_read; + } + content_len = content_ptr - content; + + printf("%s\n%ld", content, content_len); + + ledger_parse_data(content, content_len); + +export_csv_cleanup: + if (fp != NULL) fclose(fp); + return 0; +} + +/* TEST_OUTPUT +"2015/10/12","","Exxon","Liabilities:MasterCard","$","-10","","" +"2015/10/12","","Exxon","Expenses:Auto:Gas","$","10","","" +"2015/10/12","","Donna's Cake World","Liabilities:Credit","$","-5","","" +"2015/10/12","","Donna's Cake World","Expenses:Food:Dessert","$","5","","" +"2024/10/25","","Zaitoon","Liabilities:Credit","$","-5","","" +"2024/10/25","","Zaitoon","Expenses:Food:Dinner","$","5","","" +"2024/10/24","","Donna's Cake World","Expenses:Food:Dessert","$","5","","" +"2024/10/24","","Donna's Cake World","Expenses:Food:Dinner","$","19","","" +"2024/10/24","","Donna's Cake World","Assets:Cash","$","-24","","" +"2015/10/12","","Zoho","Income:Salary","$","-10000","","" +"2015/10/12","","Zoho","Assets:Bank:Checking","$","10000","","" +"2025/10/20","","Old loan from friend","Assets:Cash","$","200","","" +"2025/10/20","","Old loan from friend","Friend:Cash","$","-200","","" +TEST_OUTPUT */ diff --git a/tests/tests.mk b/tests/tests.mk @@ -0,0 +1,11 @@ +awk_query = $$(awk '/\/\* $1/{flag=1; next}/$1 \*\//{flag=0}flag' $2.c) +test_cmd = if [ "$$($< $(call awk_query,TEST_INPUT,$<))" = "$(call awk_query,TEST_OUTPUT,$<)" ]; \ + then echo Passed; \ + else echo Failed; \ + fi + +%.test: % + @printf "Test: $<... " + @$(call test_cmd) + +test: $(addsuffix .test,$(basename $(wildcard tests/*.c)))