diff --git a/README b/README new file mode 100644 index 0000000..148ad11 --- /dev/null +++ b/README @@ -0,0 +1,58 @@ +This is a prototype (I'd even say a mockup) of an application I am developping as +well as a custom gui lib, so please keep that in mind. +I'm aware that this code is pure spagetti garbage, mostly because I was just throwing +shit at the wall to see what sticks and also to quickly get an MVP (somewhat) for +personal usage. + +!!! +This software is provided as is and should be treated as INCOMPLETE, BUGGY and +UNMAINTAINED. +!!! + +This is just a peak for those that are interested. + +I am currently in the process of slowly redoing everything from scratch the +right way. And except for the core ideas, nothing will be left in the new codebase, +not even the name of the project. + +No keyboard controls are implemented except for text fields (recognisable with a red +underscore in the text, arrows, pgup/down, home/end, only ascii letters - I didn't +bother with utf8 in this for now - it will be done in the real release tho). + +Use your mouse to navigate, left click to activate stuff. +For the chart in the main program you will need to be able to use mouse button 6-7 to +pan it left-right, usually it will be the left-right gestures on a trackpad (I made +this shit on a laptop). Mouse wheel (or button 4-5) to scroll up and down. You can also +left-right click on the table header to grow-shrink a column. +Double left cliking on a list or table row will activate it. +Windows can be resized. + +If you wish to fuck around with the appearance of the program, you can edit the +btk/btk-config.h file. +Btw the font is set to Terminus, if its not installed on your system the rendering +will be fucked up, so either install that font or change the btk_font_name[] property +to your font of choice - note that the internal font parameters have antialias disabled, +so preferably use a bitmap font or it will look like shit. + +There is a test "database" that the program takes cares of in ./htpt-db-example navigate +to it from the open button in the main program (in the file list if you want to go up to a +parent folder double click the topmost ".." row), a folder that has been populated with this +specific "db" (its a hidden ".htpt" file in the target dir) will be highlighed in green +by default, select it (without double click) then click the open button. +The "add file" button will copy whatever you select into the location the of "db" + +to compile: + make hello (just a hello world window) +or: + make hotpot (the actual program) + +dependencies : + xcb + cairo + libc + pthread + c99 compiler + +Again, this is a mockup of a program and lib, please don't scorn me for this, I promise +that in the actual release everything should be done as one would expect form a normal +library. diff --git a/hello.c b/hello.c new file mode 100644 index 0000000..eb1b95f --- /dev/null +++ b/hello.c @@ -0,0 +1,27 @@ +#include +#include + +#include "btk/btk.h" + +btk_session_t *s = NULL; +btk_window_t *w = NULL; +btk_window_t *W[1]; + +int +main(int argc, char **argv) +{ + s = btk_open(); + w = btk_window_create(s->x_con, s->x_scr, s->x_vis, 1, 1, 0, 0, 200, 0, 1, NULL); + + btk_window_set_name(w, "hello world"); + btk_cell_set_mark(&(w->cells[0]), 0, 0, 1, BTK_JUSTIFY_LEFT, "hello world"); + + W[0] = w; + btk_map(s, w); + btk_loop(s, W, 1); + + btk_close(s); + + return 0; +} + diff --git a/main.c b/main.c new file mode 100644 index 0000000..cb554f3 --- /dev/null +++ b/main.c @@ -0,0 +1,1393 @@ +#define _DEFAULT_SOURCE +#include +#include +#include +#include +#include +#include + +#include "btk/btk.h" + +/* for filesystem navigation */ +/* change depending on your system */ +#define NAME_SIZE 255 +#define PATH_SIZE 4096 + +/* buffer increments */ +#define DIR_BUFFER_INC 100 +#define DATA_BUFFER_INC 2 + +#define READ_SIZE 2048 +#define TEXT_SIZE 4096 +#define ITEM_SIZE 256 + +#define TAG_COL 0 +#define UID_COL 1 +#define NAME_COL 2 +#define DATA_COLS_N 9 /* excludes id, filename and tag */ + +enum tag_type { + TAG_NONE = 0, + TAG_STRAY = 1U << 0, + TAG_NEW = 1U << 1, + TAG_DEL = 1U << 2, + TAG_EDITED = 1U << 3, + TAG_VISIBILITY = 1U << 4 +}; + +enum status { + STATUS_NONE, + STATUS_ERROR, + STATUS_LATEST, + STATUS_EDITED, + STATUS_BUSY +}; + +enum dir_mode { + DIR_FILES = 1U << 0, + DIR_HIDDEN = 1U << 1 +}; + +typedef struct { + char uid[ITEM_SIZE]; + char filename[ITEM_SIZE]; + char misc[DATA_COLS_N][ITEM_SIZE]; + char notes[TEXT_SIZE]; + unsigned int notes_w; +} data_entry_t; + +/* window creation functions */ +void build_entry_form (); +void build_file_picker (); +void build_main_window (); +void build_settings (); + +/* functions */ +void clean_string_copy (char *, char *, unsigned int, int); +int cmp_str_for_qsort (const void *, const void*); +void entry_form_apply (); +void entry_form_close (); +void entry_form_open (); +int file_exists (); +void file_picker_close (); +void file_picker_open (int); +void file_picker_open_entry (); +void file_picker_open_open (); +void file_picker_open_new (); +void file_picker_select (); +int find_dir_spot (char *, char *); +char* get_data_item (unsigned int, unsigned int); +void hotpot_close (); +void hotpot_load (); +void hotpot_reload (); +void hotpot_sync (); +void hotpot_unload (); +void move_dir (int); +void open_ext_dir (); +void open_ext_entry (int); +void quit (); +int parse_dir (char *, int); +void push_entry (int, data_entry_t); +void settings_close (); +void settings_open (); +void set_activity_status (int, int); +void set_entry_tag_col (unsigned int, int); +void set_main_win_states (int); +void set_file_picker_states (int); +void str_to_lower_case (char *); +void toggle_del_tag (); +void toggle_col (int, btk_arg_t); +void toggle_hidden (int, btk_arg_t); +void toggle_notes (int, btk_arg_t); +void toggle_visibility_tag (); +void update_select_data (int); + +/* btk windows */ +btk_session_t *session; +btk_window_t *entry_form; +btk_window_t *file_picker; +btk_window_t *main_win; +btk_window_t *settings; +btk_window_t *windows[4]; + +/* btk important cells */ +btk_cell_t *table_cell; +btk_cell_t *dir_list_cell; +btk_cell_t *notes_view_cell; +btk_cell_t *file_select_cell; +btk_cell_t *activity_cell; +btk_cell_t *edited_note_cell; +unsigned int table_cell_id; +unsigned int dir_list_cell_id; +unsigned int notes_view_cell_id; +unsigned int file_select_cell_id; +unsigned int activity_cell_id; +unsigned int edited_note_cell_id; + +/* file navigation */ +char dir_current[PATH_SIZE]; +char *dir_list = NULL; +int *dir_spot = NULL; +unsigned int dir_list_n = 0; +int nav_select = -1; +int nav_mode; + +/* hotpot data */ +int file_picker_mode; +char open_dir[PATH_SIZE] = ""; +int hotpot_open = 0; +char *data = NULL; +char *data_notes = NULL; +int *data_tags = NULL; +int *data_spot = NULL; +char *data_rename = NULL; +unsigned int data_n = 0; +unsigned int data_buffer_n = 0; +unsigned int last_uid = 0; + +/* selected data */ +int select_data = -1; +char select_note[TEXT_SIZE]; + +/* edited data */ +char edited_uid[ITEM_SIZE]; +char edited_filename[ITEM_SIZE]; +char edited_misc[DATA_COLS_N][ITEM_SIZE]; +char edited_note[TEXT_SIZE]; + +/* misc gui stuff */ +char status_open_file[PATH_SIZE + NAME_SIZE]; +char status_files_n[15]; +char status_activity[9]; +char misc_titles[DATA_COLS_N][ITEM_SIZE]; +char misc_switches[DATA_COLS_N][ITEM_SIZE]; +unsigned int data_cols_cw[DATA_COLS_N + 3] = {1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2}; +int data_cols_ena[DATA_COLS_N + 3] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; +char data_header[DATA_COLS_N + 3][ITEM_SIZE] = { + "TAGS", + "UID", + "FILENAME", + "TOPICS", + "AUTHORS", + "INSTITUTIONS", + "PUBLISHER", + "YEAR", + "VOL", + "NO", + "PAGES", + "ACCESSED" +}; + +/* misc */ +btk_arg_t dummy_arg = {.i = 0, .f = 0.0f, .c = NULL, .p = NULL}; + +/*******************************************************************/ + +void +build_entry_form() +{ + entry_form = btk_window_create(session->x_con, + session->x_scr, + session->x_vis, + 8, 6 + DATA_COLS_N, + 4, 3 + DATA_COLS_N, + 100, 0, + 10 + 2 * DATA_COLS_N, + entry_form_close); + btk_window_set_name(entry_form, "hotpot - entry form"); + + edited_note_cell_id = 0; + edited_note_cell = &(entry_form->cells[edited_note_cell_id]); + btk_cell_set_editor(edited_note_cell, 2, 2 + DATA_COLS_N, 6, 4, (char*)&edited_note, TEXT_SIZE); + + btk_cell_set_mark (&(entry_form->cells[1]), 0, 0, 2, BTK_JUSTIFY_RIGHT, "uid :"); + btk_cell_set_mark (&(entry_form->cells[2]), 0, 1, 2, BTK_JUSTIFY_RIGHT, "filename :"); + btk_cell_set_mark (&(entry_form->cells[3]), 2, 0, 6, BTK_JUSTIFY_LEFT, edited_uid); + btk_cell_set_input (&(entry_form->cells[4]), 2, 1, 6, (char*)edited_filename, NAME_SIZE); + btk_cell_set_mark (&(entry_form->cells[5]), 0, 2 + DATA_COLS_N, 2, BTK_JUSTIFY_RIGHT, "notes :"); + btk_cell_set_empty (&(entry_form->cells[6]), 0, 3 + DATA_COLS_N, 2, 1); + btk_cell_set_button (&(entry_form->cells[7]), 0, 4 + DATA_COLS_N, 2, "apply", entry_form_apply); + btk_cell_set_button (&(entry_form->cells[8]), 0, 5 + DATA_COLS_N, 2, "close", entry_form_close); + + /* generate input fields for misc data */ + for (int i = 0; i < DATA_COLS_N; i++) { + + strcpy(misc_titles[i], data_header[i + 3]); + str_to_lower_case(misc_titles[i]); + strcat(misc_titles[i], " :"); + + btk_cell_set_mark (&(entry_form->cells[9 + i]), 0, 2 + i, 2, BTK_JUSTIFY_RIGHT, misc_titles[i]); + btk_cell_set_input (&(entry_form->cells[10 + DATA_COLS_N + i]), 2, 2 + i, 6, (char*)edited_misc[i], NAME_SIZE); + } +} + +void +build_file_picker() +{ + file_picker = btk_window_create(session->x_con, + session->x_scr, + session->x_vis, + 4, 9, + 2, 2, + 0, 300, + 5, + file_picker_close); + btk_window_set_name(file_picker, "hotpot - file picker"); + + dir_list_cell_id = 0; + dir_list_cell = &(file_picker->cells[dir_list_cell_id]); + btk_cell_set_list (dir_list_cell, + 0, 1, 4, 5, + &dir_list, + &dir_list_n, + NAME_SIZE, + NULL, NULL, + &dir_spot, + move_dir, + set_file_picker_states); + + file_select_cell_id = 1; + file_select_cell = &(file_picker->cells[file_select_cell_id]); + btk_cell_set_button (file_select_cell, 0, 7, 4, "select", file_picker_select); + + btk_cell_set_mark (&(file_picker->cells[2]), 0, 0, 4, BTK_JUSTIFY_RIGHT, (char*)&dir_current); + btk_cell_set_switch (&(file_picker->cells[3]), 0, 6, 4, "show hidden", toggle_hidden, dummy_arg); + btk_cell_set_button (&(file_picker->cells[4]), 0, 8, 4, "cancel", file_picker_close); +} + +void +build_main_window() +{ + main_win = btk_window_create(session->x_con, + session->x_scr, + session->x_vis, + 13, 16, + 4, 7, + 400, 0, + 21, + quit); + btk_window_set_name(main_win, "hotpot"); + + table_cell_id = 0; + table_cell = &(main_win->cells[table_cell_id]); + btk_cell_set_table(table_cell, + 3, 0, 10, 15, + (char*)data_header, + &data, + ITEM_SIZE, + DATA_COLS_N + 3, + &data_n, + (int*)data_cols_ena, + data_cols_cw, + 2, + NULL, NULL, + &data_spot, + open_ext_entry, + update_select_data); + + strcpy(select_note, ""); + notes_view_cell_id = 1; + notes_view_cell = &(main_win->cells[notes_view_cell_id]); + btk_cell_set_editor(notes_view_cell, + 3, 9, 10, 6, + select_note, + TEXT_SIZE); + main_win->cells[notes_view_cell_id].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[notes_view_cell_id].state |= BTK_CELL_STATE_HIDDEN; + + activity_cell_id = 18; + activity_cell = &(main_win->cells[activity_cell_id]); + btk_cell_set_mark (activity_cell, + 11, 15, 2, + BTK_JUSTIFY_RIGHT, + (char*)&status_activity); + + btk_cell_set_button (&(main_win->cells[2]), 0, 0, 3, "open", file_picker_open_open); + btk_cell_set_button (&(main_win->cells[3]), 0, 1, 3, "new", file_picker_open_new); + btk_cell_set_button (&(main_win->cells[4]), 0, 2, 3, "sync", hotpot_sync); + btk_cell_set_button (&(main_win->cells[5]), 0, 3, 3, "view directory", open_ext_dir); + btk_cell_set_button (&(main_win->cells[6]), 0, 4, 3, "reload", hotpot_reload); + btk_cell_set_button (&(main_win->cells[7]), 0, 5, 3, "close", hotpot_close); + btk_cell_set_button (&(main_win->cells[8]), 0, 6, 3, "quit", quit); + btk_cell_set_empty (&(main_win->cells[9]), 0, 7, 3, 1); + btk_cell_set_button (&(main_win->cells[10]), 0, 8, 3, "add file", file_picker_open_entry); + btk_cell_set_button (&(main_win->cells[11]), 0, 9, 3, "edit entry", entry_form_open); + btk_cell_set_button (&(main_win->cells[12]), 0, 10, 3, "tag for deletion", toggle_del_tag); + btk_cell_set_button (&(main_win->cells[19]), 0, 11, 3, "tag for visibility", toggle_visibility_tag); + btk_cell_set_button (&(main_win->cells[20]), 0, 12, 3, "export citation", NULL); + btk_cell_set_empty (&(main_win->cells[13]), 0, 13, 3, 1); + btk_cell_set_switch (&(main_win->cells[14]), 0, 14, 3, "show notes", toggle_notes, dummy_arg); + btk_cell_set_button (&(main_win->cells[15]), 0, 15, 3, "settings", settings_open); + btk_cell_set_mark (&(main_win->cells[16]), 3, 15, 6, BTK_JUSTIFY_RIGHT, (char*)&status_open_file); + btk_cell_set_mark (&(main_win->cells[17]), 9, 15, 2, BTK_JUSTIFY_RIGHT, (char*)&status_files_n); + + main_win->cells[15].state |= BTK_CELL_STATE_ON; + main_win->cells[20].state |= BTK_CELL_STATE_DISABLED; + set_main_win_states(0); +} + +void +build_settings() +{ + settings = btk_window_create(session->x_con, + session->x_scr, + session->x_vis, + 3, 3 + DATA_COLS_N, + -1, -1, + 0, 0, + 3 + DATA_COLS_N, + settings_close); + btk_window_set_name(settings, "hotpot - settings"); + + btk_arg_t col_arg= {.i = TAG_COL}; + + btk_cell_set_switch (&(settings->cells[0]), 0, 0, 3, "show tags", toggle_col, col_arg); + btk_cell_set_empty (&(settings->cells[1 + DATA_COLS_N]), 0, 1 + DATA_COLS_N, 3, 1); + btk_cell_set_button (&(settings->cells[2 + DATA_COLS_N]), 0, 2 + DATA_COLS_N, 3, "close", settings_close); + + settings->cells[0].state |= BTK_CELL_STATE_ON; + + /* generate col toggles for misc data */ + for (int i = 0; i < DATA_COLS_N; i++) { + + sprintf(misc_switches[i], "show %s", data_header[i + 3]); + str_to_lower_case(misc_switches[i]); + + btk_arg_t col_arg = {.i = i + 3}; + btk_cell_set_switch (&(settings->cells[1 + i]), 0, 1 + i, 3, misc_switches[i], toggle_col, col_arg); + settings->cells[1 + i].state |= BTK_CELL_STATE_ON; + } +} + +/****************************************************************************************/ + +int +main() +{ + getcwd(dir_current, PATH_SIZE); + strcat(dir_current, "/"); + + /* btk init */ + session = btk_open(); + build_entry_form(); + build_file_picker(); + build_main_window(); + build_settings(); + windows[0] = main_win; + windows[1] = file_picker; + windows[2] = entry_form; + windows[3] = settings; + + btk_map(session, main_win); + btk_loop(session, windows, 4); + + btk_close(session); + + quit(); + return 0; +} + +/****************************************************************************************/ + +void +clean_string_copy(char* dest, char* src, unsigned int max_size, int del_newline) +{ + if (!max_size) + return; + + char mid[max_size]; + + strncpy(mid, src, max_size); + if (del_newline) + *strchr(mid, '\n') = '\0'; + mid[max_size - 1] = '\0'; + strcpy(dest, mid); +} + +int +cmp_str_for_qsort(const void *a, const void *b) +{ + return strcmp((char*)a, (char*)b); +} + +/* check if any data changed, copy it and update status if so */ +void +entry_form_apply() +{ + int changed = 0; + + if (strcmp(edited_filename, get_data_item(NAME_COL, select_data))) { + strcpy(data_rename + select_data * ITEM_SIZE, get_data_item(NAME_COL, select_data)); + strcpy(get_data_item(NAME_COL, select_data), edited_filename); + changed = 1; + } + + for (int i = 0; i < DATA_COLS_N; i++) { + if (strcmp(edited_misc[i], get_data_item(i + 3, select_data))) { + strcpy(get_data_item(i + 3, select_data), edited_misc[i]); + changed = 1; + } + } + if (strcmp(edited_note, select_note)) { + strcpy(data_notes + select_data * TEXT_SIZE, edited_note); + strcpy(select_note, edited_note); + btk_cell_editor_update_text(notes_view_cell); + changed = 1; + } + + if (changed) { + data_tags[select_data] |= TAG_EDITED; + set_entry_tag_col(select_data, data_tags[select_data]); + set_activity_status(STATUS_EDITED, 1); + btk_window_redraw(main_win); + } +} + +void +entry_form_open() +{ + if (select_data < 0 || select_data >= data_n) + return; + + strcpy(edited_note, select_note); + btk_cell_editor_reset_caret(edited_note_cell); + + strcpy(edited_uid, get_data_item(1, select_data)); + strcpy(edited_filename, get_data_item(2, select_data)); + btk_cell_input_reset_caret(&(entry_form->cells[4])); + + for (int i = 0; i < DATA_COLS_N; i++) { + strcpy(edited_misc[i], get_data_item(i + 3, select_data)); + btk_cell_input_reset_caret(&(entry_form->cells[10 + DATA_COLS_N + i])); + } + + btk_cell_editor_update_text(edited_note_cell); + btk_window_disable(main_win); + btk_map(session, entry_form); +} + +void +entry_form_close() +{ + btk_window_enable(main_win); + btk_unmap(session, entry_form); +} + +/* checks if currently selected file in the file picker + * exists in the open hotpot directory */ +int +file_exists() +{ + DIR *d; + struct dirent *dir; + + d = opendir(open_dir); + if (!d) { + printf("ERROR - couldn't read directory"); + return 0; + } + + while ((dir = readdir(d))) { + if (!strcmp(dir->d_name, dir_list + nav_select * NAME_SIZE)) + return 1; + } + + return 0; +} + +void +file_picker_close() +{ + btk_window_enable(main_win); + btk_unmap(session, file_picker); +} + +void +file_picker_open(int mode) +{ + btk_cell_list_deselect(dir_list_cell); + set_file_picker_states(-1); + + file_picker_mode = mode; + if (!file_picker_mode) { + nav_mode |= DIR_FILES; + } else { + nav_mode &= ~DIR_FILES; + } + + btk_window_disable(main_win); + btk_map(session, file_picker); + parse_dir(dir_current, nav_mode); +} + +void file_picker_open_entry() { file_picker_open(0); } +void file_picker_open_open() { file_picker_open(1); } +void file_picker_open_new() { file_picker_open(2); } + +void +file_picker_select() +{ + if (nav_select < 0 || nav_select >= dir_list_n) { + printf("ERROR -> tried to open out of bound selection\n"); + return; + } + + switch(file_picker_mode) { + + /* copy selected file into open hotpot directory, then add it to data */ + case 0:; + /* compose command */ + char cmd[12 + 2 * PATH_SIZE + NAME_SIZE]; + sprintf(cmd, + "cp -n \'%s%s\' \'%s\'", + dir_current, + dir_list + nav_select * NAME_SIZE, + open_dir); + system(cmd); + /* create new entry */ + data_entry_t new_entry; + new_entry.notes_w = 0; + new_entry.notes[0] = '\0'; + new_entry.uid[0] = '\0'; + for (int i = 0; i < DATA_COLS_N; i++) + new_entry.misc[i][0] = '\0'; + strcpy(new_entry.filename, dir_list + nav_select * NAME_SIZE); + push_entry(TAG_NEW, new_entry); + /* update window */ + set_activity_status(STATUS_EDITED, 0); + set_entry_tag_col(data_n - 1, data_tags[data_n - 1]); + set_main_win_states(1); + break; + + /* open hotpot */ + case 1: + strcpy(open_dir, dir_current); + strcat(open_dir, dir_list + nav_select * NAME_SIZE); + hotpot_load(); + break; + + /* create hotpot file, then open it */ + case 2: + strcpy(open_dir, dir_current); + strcat(open_dir, dir_list + nav_select * NAME_SIZE); + char hotpot_file[PATH_SIZE + NAME_SIZE]; + strcpy(hotpot_file, open_dir); + strcat(hotpot_file, "/.htpt"); + FILE *f = fopen(hotpot_file, "w"); + if (!f) { + set_activity_status(STATUS_ERROR, 1); + printf("ERROR - couldn't create new hotpot file"); + return; + } + fclose(f); + hotpot_load(); + break; + } + + file_picker_close(); + set_main_win_states(1); +} + +/* returns the hilight color index of the directory */ +int +find_dir_spot(char *search_dir, char *sub_dir) +{ + char search_path[PATH_SIZE]; + strcpy(search_path, search_dir); + strcat(search_path, sub_dir); + strcat(search_path, "/"); + + DIR *d; + struct dirent *dir; + struct stat file_stats; + char file_path[PATH_SIZE + NAME_SIZE]; + + d = opendir(search_path); + if (!d) { + printf("ERROR - couldn't read directory"); + return 0; + } + + while ((dir = readdir(d))) { + strcpy(file_path, search_path); + strcat(file_path, dir->d_name); + stat(file_path, &file_stats); + if ((file_stats.st_mode & S_IFMT) != S_IFDIR) { + if (strcmp(dir->d_name, ".htpt") == 0) { + return 1; + } + } + } + return 2; +} + +char* +get_data_item(unsigned int col, unsigned int row) +{ + return data + ITEM_SIZE * (col + row * (DATA_COLS_N + 3)); +} + +void +hotpot_close() +{ + hotpot_unload(); + select_data = -1; + strcpy(select_note, ""); + strcpy(open_dir, ""); + + set_main_win_states(1); +} + +void +hotpot_load() +{ + set_activity_status(STATUS_BUSY, 1); + + char hotpot_file[PATH_SIZE + NAME_SIZE]; + strcpy(hotpot_file, open_dir); + strcat(hotpot_file, "/.htpt"); + + FILE *f = fopen(hotpot_file, "r"); + if (!f) { + printf("ERROR - couldn't read hotpot file\n"); + set_activity_status(STATUS_ERROR, 1); + return; + } + + hotpot_unload(); + + /* arrays preparation for on the go resizing */ + data_buffer_n = DATA_BUFFER_INC; + data = malloc(data_buffer_n * ITEM_SIZE * (DATA_COLS_N + 3)); + data_notes = malloc(data_buffer_n * TEXT_SIZE); + data_tags = malloc(data_buffer_n * sizeof(int)); + data_spot = malloc(data_buffer_n * sizeof(int)); + data_rename = malloc(data_buffer_n * ITEM_SIZE); /* used for syncing later */ + + data_entry_t entry_read; + char line_read[READ_SIZE]; + char *last_newline; + int in_section = 0, col = 0, w = 0; + + /* get lastest used uid on the first line */ + /* if it is not present it will be regenerated after the rest of the file has + * been parsed */ + if (fgets(line_read, READ_SIZE, f) && line_read[0] == '-') { + last_uid = strtol(line_read + 1, NULL, 16); + } else { + last_uid = 0; /* to be regenerated */ + } + + /* parse rest of .htpt file */ + while (fgets(line_read, READ_SIZE, f)) { + switch (line_read[0]) { + + /* open section, reset entry_read */ + case '[': + if (in_section) + break; + in_section = 1; + col = 0; + w = 0; + entry_read.notes_w = 0; + entry_read.notes[0] = '\0'; + entry_read.uid[0] = '\0'; + entry_read.filename[0] = '\0'; + for (int i = 0; i < DATA_COLS_N; i++) + entry_read.misc[i][0] = '\0'; + break; + + /* close section, push entry_read to data */ + case ']': + if (!in_section) + break; + in_section = 0; + last_newline = strrchr(entry_read.notes, '\n'); + if (last_newline) /* remove last unecessary newline */ + *last_newline = '\0'; + push_entry(TAG_STRAY, entry_read); + break; + + /* get uid */ + case '-': + if (!in_section) + break; + clean_string_copy(entry_read.uid, line_read + 1, ITEM_SIZE, 1); + break; + + /* get filename */ + case '+': + if (!in_section) + break; + clean_string_copy(entry_read.filename, line_read + 1, ITEM_SIZE, 1); + break; + + /* get other data */ + case '.': + if (!in_section || col >= DATA_COLS_N) + break; + clean_string_copy(entry_read.misc[col], line_read + 1, ITEM_SIZE, 1); + col++; + break; + + /* get notes on multiple lines until section close or reaching TEXT_SIZE + * character limit */ + case '*': + if (!in_section) + break; + w = strlen(line_read + 1); + if (entry_read.notes_w + w >= TEXT_SIZE) { + w = TEXT_SIZE - entry_read.notes_w; + } else { + strncat(entry_read.notes, line_read + 1, w); + } + entry_read.notes_w += w; + break; + + default: + break; + } + } + fclose(f); + + /* regenerate uid if needed */ + if (!last_uid) { + for (int i = 0; i < data_n; i++) + if (last_uid < strtol(get_data_item(UID_COL, i), NULL, 16)) + last_uid = strtol(get_data_item(UID_COL, i), NULL, 16); + } + + /* parse all files in hotpot directory and match them to parsed data */ + DIR *d = opendir(open_dir); + struct dirent *dir; + struct stat file_stats; + char file_path[PATH_SIZE + NAME_SIZE]; + int found = 0; + int new_files = 0; + int n = data_n; + int is_dir; + + data_entry_t dummy_entry; + dummy_entry.uid[0] = '\0'; + dummy_entry.notes[0] = '\0'; + dummy_entry.notes_w = 0; + for (int i = 0; i < DATA_COLS_N; i++) + dummy_entry.misc[i][0] = '\0'; + + /* scan found files */ + while ((dir = readdir(d))) { + found = 0; + strcpy(file_path, open_dir); + strcat(file_path, dir->d_name); + stat(file_path, &file_stats); + is_dir = (file_stats.st_mode & S_IFMT) == S_IFDIR ? 1 : 0; + //printf("%i\t%s\n", is_dir, dir->d_name); TODO fix directory detection + if (is_dir || dir->d_name[0] == '.') { + continue; + } + for (int i = 0; i < n; i++) { + if (strcmp(dir->d_name, get_data_item(NAME_COL, i)) == 0) { + data_tags[i] = TAG_NONE; + found = 1; + break; + } + } + /* if file not found, create new entry */ + if (!found) { + clean_string_copy(dummy_entry.filename, dir->d_name, ITEM_SIZE, 0); + push_entry(TAG_NEW, dummy_entry); + new_files = 1; + } + } + + /* set tag values in data */ + for (int i = 0; i < data_n; i++) { + set_entry_tag_col(i, data_tags[i]); + } + + btk_cell_table_deselect(table_cell); + strcpy(select_note, ""); + set_activity_status((new_files ? STATUS_EDITED : STATUS_LATEST), 1); + + + return; +} + +void +hotpot_reload() +{ + hotpot_unload(); + select_data = -1; + strcpy(select_note, ""); + + hotpot_load(); + + set_main_win_states(1); +} + +void +hotpot_sync() +{ + set_activity_status(STATUS_BUSY, 1); + + char hotpot_file[PATH_SIZE + NAME_SIZE]; + strcpy(hotpot_file, open_dir); + strcat(hotpot_file, "/.htpt"); + + FILE *f = fopen(hotpot_file, "w"); + if (!f) { + printf("ERROR - couldn't write hotpot file\n"); + set_activity_status(STATUS_ERROR, 1); + return; + } + + /* output last uid */ + fprintf(f, "-%X\n\n", last_uid); + + /* output rows of data */ + char *note_token; + char note[TEXT_SIZE]; + for (int i = 0; i < data_n; i++) { + + if ((data_tags[i] & TAG_DEL)) + continue; + + fprintf(f, "[\n"); + + fprintf(f, "-%s\n", get_data_item(UID_COL, i)); + fprintf(f, "+%s\n", get_data_item(NAME_COL, i)); + for (int j = 0; j < DATA_COLS_N; j++) + fprintf(f, ".%s\n", get_data_item(j + 3, i)); + + // TODO + /* + strcpy(note, data_notes + i * TEXT_SIZE); + char *lim = strchr(note, '\n'); + while (lim) { + *lim = '\0'; + printf("%lu >> \'%s\'\n", strlen(note), note); + fprintf(f, "*%s\n", note); + strcpy(note, ++lim); + lim = strchr(note, '\n'); + } + */ + + strcpy(note, data_notes + i * TEXT_SIZE); + note_token = strtok(note, "\n"); + while (note_token) { + fprintf(f, "*%s\n", note_token); + note_token = strtok(NULL, "\n"); + } + + fprintf(f, "]\n\n"); + } + fclose(f); + + /* rename edited files if those are not stray entries */ + char cmd_mv[11 + 2 * (PATH_SIZE + NAME_SIZE)]; + for (int i = 0; i < data_n; i++) { + if (!(data_tags[i] & TAG_STRAY) && strcmp(data_rename + i * ITEM_SIZE, "")) { + sprintf(cmd_mv, + "mv \'%s/%s\' \'%s/%s\'", + open_dir, + data_rename + i * ITEM_SIZE, + open_dir, + get_data_item(NAME_COL, i)); + system(cmd_mv); + } + } + + /* delete rows with DEL tag by shifting down */ + int c = 0; + char mid[TEXT_SIZE]; /* just to get rid of pointer overlap warning */ + char cmd_rm[5 + PATH_SIZE + NAME_SIZE]; + for (int i = 0; i < data_n; i++) { + if (c) { + data_tags[i - c] = data_tags[i]; + strcpy(mid, data_notes + i * TEXT_SIZE); + strcpy(data_notes + (i - c) * TEXT_SIZE, mid); + for (int j = 0; j < DATA_COLS_N + 3; j++) + strcpy(get_data_item(j, i - c), get_data_item(j, i)); + } + if ((data_tags[i] & TAG_DEL)) { + if (!(data_tags[i] & TAG_STRAY)) { + sprintf(cmd_rm, + "rm \'%s/%s\'", + open_dir, + get_data_item(NAME_COL, i)); + system(cmd_rm); + } + c++; + } + } + data_n -= c; + + /* remove EDIT and NEW tags */ + for (int i = 0; i < data_n; i++) { + data_tags[i] &= ~(TAG_EDITED | TAG_NEW); + } + + /* update tag values in data */ + for (int i = 0; i < data_n; i++) { + set_entry_tag_col(i, data_tags[i]); + } + + set_activity_status(STATUS_LATEST, 0); + btk_window_redraw(main_win); + + return; +} + +void +hotpot_unload() +{ + if (data_n) { + free(data); + free(data_tags); + free(data_spot); + free(data_notes); + free(data_rename); + data_n = 0; + } +} + +void +move_dir(int dir_sel) +{ + if (dir_sel < 0 || !dir_spot[dir_sel]) + return; + + /* if first item selected ("..") go one dir up, unless at / */ + if (!dir_sel) { + if (strlen(dir_current) <= 1) + return; + dir_current[strlen(dir_current) - 1] = '\0'; + *(strrchr(dir_current, '/')) = '\0'; + } else { + char dir_sel_name[PATH_SIZE]; + strcpy(dir_sel_name, dir_list + dir_sel * NAME_SIZE); + strcat(dir_current, dir_sel_name); + } + + strcat(dir_current, "/"); + parse_dir(dir_current, nav_mode); + btk_window_redraw_cell(file_picker, dir_list_cell_id); + btk_window_redraw_cell(file_picker, 2); + + if (dir_sel) { + btk_cell_list_deselect(dir_list_cell); + set_file_picker_states(-1); + } +} + +void +open_ext_dir() +{ + char *args[PATH_SIZE]={"./EXEC", open_dir, NULL}; + int pid = fork(); + + if (pid == 0) { + execvp("xdg-open", args); + } else if (pid < 0) { + printf("ERROR - couldn't open hotpot directory in file explorer\n"); + } +} + +void +open_ext_entry(int sel) +{ + if (sel < 0) + return; + + char file[PATH_SIZE + ITEM_SIZE]; + strcpy(file, open_dir); + strcat(file, "/"); + strcat(file, get_data_item(NAME_COL, sel)); + + char *args[PATH_SIZE]={"./EXEC", file, NULL}; + int pid = fork(); + + if (pid == 0) { + execvp("xdg-open", args); + } else if (pid < 0) { + printf("ERROR - couldn't open item externaly\n"); + } +} + +int +parse_dir(char *dir_path, int mode) +{ + DIR *d = opendir(dir_path); + if (!d) { + printf("ERROR - couldn't read directory"); + return 1; + } + + struct dirent *dir; + struct stat file_stats; + char file_path[PATH_SIZE + NAME_SIZE]; + unsigned int dir_buffer_n = DIR_BUFFER_INC; + int c = 0; + int is_dir; + + if (dir_list_n) { + free(dir_list); + free(dir_spot); + } + + dir_list_n = 0; + dir_list = malloc(dir_buffer_n * NAME_SIZE); + dir_spot = malloc(dir_buffer_n * sizeof(int)); + + while ((dir = readdir(d))) { + sprintf(file_path, "%s%s", dir_path, dir->d_name); + stat(file_path, &file_stats); + is_dir = (file_stats.st_mode & S_IFMT) == S_IFDIR ? 1 : 0; + if (((mode & DIR_FILES) || is_dir) && c && + ((mode & DIR_HIDDEN) || dir->d_name[0] != '.' || c < 2)) { + strcpy(dir_list + dir_list_n * NAME_SIZE, dir->d_name); + if (is_dir) /* adding '/' to dir items to easily detect them when sorting */ + strcat(dir_list + dir_list_n * NAME_SIZE, "/"); + dir_spot[dir_list_n] = 0; + dir_list_n++; + } + c++; + if (dir_list_n >= dir_buffer_n - 1) { + dir_buffer_n += DIR_BUFFER_INC; + dir_spot = realloc(dir_spot, dir_buffer_n * sizeof(int)); + dir_list = realloc(dir_list, dir_buffer_n * NAME_SIZE); + } + } + + /* sort dir list and set hilights */ + char *a; + qsort(dir_list, dir_list_n, NAME_SIZE, cmp_str_for_qsort); + for (int i = 0; i < dir_list_n; i++) { + a = strchr(dir_list + i * NAME_SIZE, '/'); + if (a) { + *a = '\0'; + dir_spot[i] = find_dir_spot(dir_path, dir_list + i * NAME_SIZE); + } + } + + return 0; +} + +void +push_entry(int tag, data_entry_t entry) +{ + if (!data) { + printf("ERROR - tried to push entry into NULL data array\n"); + return; + } + + if (data_n >= data_buffer_n - 1) { + data_buffer_n += DATA_BUFFER_INC; + data = realloc(data, data_buffer_n * ITEM_SIZE * (DATA_COLS_N + 3)); + data_notes = realloc(data_notes, data_buffer_n * TEXT_SIZE); + data_tags = realloc(data_tags, data_buffer_n * sizeof(int)); + data_spot = realloc(data_spot, data_buffer_n * sizeof(int)); + data_rename = realloc(data_rename, data_buffer_n * ITEM_SIZE); + } + + if (!strcmp(entry.uid, "")) { + char new_uid[64]; + sprintf(new_uid, "%X", ++last_uid); + strcpy(get_data_item(UID_COL, data_n), new_uid); + } else { + strcpy(get_data_item(UID_COL, data_n), entry.uid); + } + + data_tags[data_n] = tag; + strcpy(data_rename + data_n * ITEM_SIZE, ""); + strcpy(get_data_item(NAME_COL, data_n), entry.filename); + for (int i = 0; i < DATA_COLS_N; i++) + strcpy(get_data_item(i + 3, data_n), entry.misc[i]); + strcpy(data_notes + data_n * TEXT_SIZE, entry.notes); + + data_n++; +} + +void +quit() +{ + hotpot_unload(); + if (dir_list_n) { + free(dir_list); + free(dir_spot); + } + + btk_window_destroy(entry_form); + btk_window_destroy(file_picker); + btk_window_destroy(main_win); + btk_window_destroy(settings); + btk_close(session); + + exit(0); +} + +void +settings_close() +{ + btk_window_enable(main_win); + btk_unmap(session, settings); +} + +void +settings_open() +{ + btk_window_disable(main_win); + btk_map(session, settings); +} + +void +set_activity_status(int status, int redraw) +{ + switch (status) { + case STATUS_NONE: + strcpy(status_activity, "n/a"); + activity_cell->state |= BTK_CELL_STATE_DISABLED; + activity_cell->state &= ~BTK_CELL_STATE_BELL; + activity_cell->state &= ~BTK_CELL_STATE_ON; + break; + case STATUS_ERROR: + strcpy(status_activity, "error"); + activity_cell->state &= ~BTK_CELL_STATE_DISABLED; + activity_cell->state |= BTK_CELL_STATE_BELL; + activity_cell->state &= ~BTK_CELL_STATE_ON; + break; + case STATUS_LATEST: + strcpy(status_activity, "synced"); + activity_cell->state &= ~BTK_CELL_STATE_DISABLED; + activity_cell->state &= ~BTK_CELL_STATE_BELL; + activity_cell->state &= ~BTK_CELL_STATE_ON; + break; + case STATUS_EDITED: + strcpy(status_activity, "unsynced"); + activity_cell->state &= ~BTK_CELL_STATE_DISABLED; + activity_cell->state &= ~BTK_CELL_STATE_BELL; + activity_cell->state |= BTK_CELL_STATE_ON; + break; + case STATUS_BUSY: + strcpy(status_activity, "busy"); + activity_cell->state &= ~BTK_CELL_STATE_DISABLED; + activity_cell->state |= BTK_CELL_STATE_BELL; + activity_cell->state &= ~BTK_CELL_STATE_ON; + break; + } + + if (redraw) + btk_window_redraw_cell(main_win, activity_cell_id); +} + +void +set_edit_tag() +{ + if (select_data < 0 || select_data >= data_n) { + printf("ERROR - can't tag out of bounds selection\n"); + return; + } + + data_tags[select_data] |= TAG_DEL; + btk_window_redraw_cell(main_win, table_cell_id); +} + +void +set_entry_tag_col(unsigned int id, int tag) +{ + if (tag & TAG_DEL) { + strcpy(get_data_item(TAG_COL, id), "DEL"); + data_spot[id] = 1; + } else if (tag & TAG_VISIBILITY) { + strcpy(get_data_item(TAG_COL, id), "FOCUS"); + data_spot[id] = 4; + } else if (tag & TAG_EDITED){ + strcpy(get_data_item(TAG_COL, id), "EDIT"); + data_spot[id] = 1; + } else if (tag & TAG_STRAY){ + strcpy(get_data_item(TAG_COL, id), "STRAY"); + data_spot[id] = 3; + } else if (tag & TAG_NEW){ + strcpy(get_data_item(TAG_COL, id), "NEW"); + data_spot[id] = 2; + } else { + strcpy(get_data_item(TAG_COL, id), ""); + data_spot[id] = 0; + } +} + +void +set_file_picker_states(int sel) +{ + nav_select = sel; + + if (sel < 1) { /* separate to avoid segfault */ + file_select_cell->state |= BTK_CELL_STATE_DISABLED; + btk_window_redraw_cell(file_picker, file_select_cell_id); + return; + } + + switch (file_picker_mode) { + case 0:; + char forbidden_path[PATH_SIZE]; + strcpy(forbidden_path, open_dir); + strcat(forbidden_path, "/"); + if(!dir_spot[sel] && strcmp(dir_current, forbidden_path) != 0 && !file_exists()) { + file_select_cell->state &= ~(BTK_CELL_STATE_DISABLED); + } else { + file_select_cell->state |= BTK_CELL_STATE_DISABLED; + } + break; + case 1: + if (dir_spot[sel] == 1) { + file_select_cell->state &= ~(BTK_CELL_STATE_DISABLED); + } else { + file_select_cell->state |= BTK_CELL_STATE_DISABLED; + } + break; + case 2: + if (dir_spot[sel] == 1) { + file_select_cell->state |= BTK_CELL_STATE_DISABLED; + } else { + file_select_cell->state &= ~(BTK_CELL_STATE_DISABLED); + } + break; + } + + btk_window_redraw_cell(file_picker, file_select_cell_id); +} + +void +set_main_win_states(int redraw) +{ + if (!(strcmp(open_dir, ""))) { + main_win->cells[4].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[5].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[6].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[7].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[10].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[16].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[17].state |= BTK_CELL_STATE_DISABLED; + strcpy(status_open_file, "no hotpot opened"); + strcpy(status_files_n, "n/a entries"); + set_activity_status(STATUS_NONE, redraw); + } else { + main_win->cells[4].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[5].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[6].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[7].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[10].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[16].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[17].state &= ~BTK_CELL_STATE_DISABLED; + strcpy(status_open_file, open_dir); + sprintf(status_files_n, "%i entries", data_n); + } + + if (data_n < 1 || select_data < 0) { + main_win->cells[11].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[12].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[19].state |= BTK_CELL_STATE_DISABLED; + } else { + main_win->cells[11].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[12].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[19].state &= ~BTK_CELL_STATE_DISABLED; + } + + if (redraw) + btk_window_redraw(main_win); +} + +void +str_to_lower_case(char *str) +{ + for (int i = 0; str[i] != '\0'; i++) + if(str[i] >= 'A' && str[i] <= 'Z') + str[i] += 32; +} + +void +toggle_col(int val, btk_arg_t arg) +{ + data_cols_ena[arg.i] = val; + btk_cell_table_update_geometry(table_cell); + btk_window_redraw_cell(main_win, table_cell_id); +} + +void +toggle_del_tag() +{ + if (select_data < 0 || select_data >= data_n) { + printf("ERROR - can't tag out of bounds selection\n"); + return; + } + + data_tags[select_data] ^= TAG_DEL; + set_entry_tag_col(select_data, data_tags[select_data]); + set_activity_status(STATUS_EDITED, 1); + btk_window_redraw_cell(main_win, table_cell_id); +} + +void +toggle_hidden(int i, btk_arg_t arg) +{ + if (i) { + nav_mode |= DIR_HIDDEN; + } else { + nav_mode &= ~DIR_HIDDEN; + } + parse_dir(dir_current, nav_mode); + btk_window_redraw_cell(file_picker, dir_list_cell_id); +} + +void +toggle_notes(int val, btk_arg_t arg) +{ + if (val) { + table_cell->ca.h = 9; + notes_view_cell->state &= ~BTK_CELL_STATE_HIDDEN; + } else { + table_cell->ca.h = 15; + notes_view_cell->state |= BTK_CELL_STATE_HIDDEN; + } + btk_window_update_cell_area(main_win, table_cell); + btk_window_redraw(main_win); +} + +void +toggle_visibility_tag() +{ + if (select_data < 0 || select_data >= data_n) { + printf("ERROR - can't tag out of bounds selection\n"); + return; + } + + data_tags[select_data] ^= TAG_VISIBILITY; + set_entry_tag_col(select_data, data_tags[select_data]); + btk_window_redraw_cell(main_win, table_cell_id); +} + +void +update_select_data(int sel) +{ + select_data = sel; + if (sel > data_n) + select_data = -1; + + if (select_data < 0) { + strcpy(select_note, ""); + main_win->cells[11].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[12].state |= BTK_CELL_STATE_DISABLED; + main_win->cells[19].state |= BTK_CELL_STATE_DISABLED; + } else { + strcpy(select_note,data_notes + sel * TEXT_SIZE); + main_win->cells[11].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[12].state &= ~BTK_CELL_STATE_DISABLED; + main_win->cells[19].state &= ~BTK_CELL_STATE_DISABLED; + } + + btk_cell_editor_update_text(notes_view_cell); + btk_window_redraw_cell(main_win, notes_view_cell_id); + btk_window_redraw_cell(main_win, 11); + btk_window_redraw_cell(main_win, 12); + btk_window_redraw_cell(main_win, 19); +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..9074e06 --- /dev/null +++ b/makefile @@ -0,0 +1,25 @@ +# hotpot - simple literrature manager + +# bucket version +VERSION = 1.0.0 + +# btk +BTK = btk +BTKFILES = ${BTK}/btk.c ${BTK}/btk-window.c ${BTK}/btk-cell.c ${BTK}/btk-text.c ${BTK}/btk-log.c + +# external libraries +LIBS = -lxcb -lcairo + +# flags +CPPFLAGS = -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\" +CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os + +# compiler and linker +CC = cc + + +hotpot: + ${CC} -o hotpot main.c ${BTKFILES} ${CPPFLAGS} ${CFLAGS} ${LIBS} + +hello: + ${CC} -o hello hello.c ${BTKFILES} ${CPPFLAGS} ${CFLAGS} ${LIBS} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..0704af0 Binary files /dev/null and b/screenshot.png differ