From 5d3e406d181c61a10a5baf77b7d6ef4958dc3b57 Mon Sep 17 00:00:00 2001 From: Tomas Rivera <137088692+Totorile1@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:50:37 +0200 Subject: [PATCH] Added support for backup using rsync --- README.md | 84 +++++++++++++++++++++++++++-------- config.json | 9 +++- makefile | 8 +++- src/main.c | 48 +++++++++++++++++++- src/utils.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.h | 7 +++ 6 files changed, 256 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 71deb50..b451ffa 100644 --- a/README.md +++ b/README.md @@ -6,44 +6,95 @@ Using my Neovim setup, I explored plugins like **[neorg](https://github.com/nvim The goal was a terminal-based interface for accessing vaults and notes using standard Markdown, with minimal extra features. Although it currently works only with Neovim, it is designed as a standalone wrapper that could be adapted for other editors with minimal changes. ### How to install -1. Have installed `ncurses`, `cjson` and `pkg-config`. +1. Have installed `ncurses`, `cjson`, `make`, `Vivify`, `rsync` and `pkg-config`. 2. Have a supported editor: `vim`, `neovim` and `nano` (for the moment) 3. Clone the repository. ```shell git clone https://github.com/Totorile1/NoteWrapper.git ``` -4. Compile the project. If you are on NixOS and have direnv installed, run `direnv allow`. It will create the nix-shell which will get all the necessary libraries. +4. Compile the project. If you are on NixOS and have direnv installed, run `direnv allow`. It will create the nix-shell which will get all the necessary build-time libraries. ``` cd NoteWrapper make ``` -5. configure `./config.json` +5. [configure](#configuration) `./config.json` 6. run the binary `notewrapper` ### Usage -If you want to render with Vivify with the flags `-r`, `--render` or with the option in the config file, you must have installed the plugins for your specific editor ([for Vim and Neovim](https://github.com/jannis-baum/vivify.vim)) - - +``` +Usage: notewrapper [options] +Options: + -c, --config Specify the config file. + -d, --directory Specify the vaults' directory. + -h, --help Display this message. + -e, --editor Specify the editor to open. + -j, --jump Jumps to the end of the file on opening. + -J, --no-jump Do not jump to the end of the file + -n, --note Specify the note (or journal). + -r, --render Renders the note with Vivify. + -R, --no-render Do not render. + -v, --vault Specify the vault. + --version Display the program version and the GPL3 notice. + -V, --verbose Show debug information. +``` ### Editor support `NoteWrapper` relies on certain editor features, so not all of its functionality is supported by every editor. - Here are the features that depend on editor support: - **Bufferless rendering**: As soon as you write something in the note (even without saving), it is rendered. - **Cursor following**: The rendered view follows your cursor. For example, if you move to the bottom of a note, the rendered view will also scroll to the bottom. - **Jump to end on open**: Automatically jumps to the end of the file when opening it. -The first two features depend on [Vivify's plugin for editors](https://github.com/jannis-baum/Vivify?tab=readme-ov-file#existing-integration). +The first two features depend on [Vivify's plugin for editors](https://github.com/jannis-baum/Vivify?tab=readme-ov-file#existing-integration) and are only usefull if you want to render the markdown externally on your browser. If your editor misses these features, you can just create a plugin or an extension which uses [Vivify's simple API](https://github.com/jannis-baum/Vivify?tab=readme-ov-file#editor-support). -| Editor | Bufferless | Cursor | Jump to end | -|---------|------------|--------|-------------| -| Neovim | ✅ | ✅ | ✅ | -| Vim | ✅ | ✅ | ✅ | -| Nano | ❌ | ❌ | ✅ | + +| Editor | Bufferless | Cursor | Jump to end | Necessary plugin | +|---------|------------|--------|-------------|------------------| +| Neovim | ✅ | ✅ | ✅ | [vivify-vim](https://github.com/jannis-baum/vivify.vim) | +| Vim | ✅ | ✅ | ✅ | [vivify-vim](https://github.com/jannis-baum/vivify.vim) | +| Nano | ❌ | ❌ | ✅ | | ### Configuration -Change `$/.config/notewrapper/config.json`. If it does not exist. On building, it should copy a default config. +Change `~/.config/notewrapper/config.json`. If it does not exist. On building, it should copy a default config. + +```json +{ + "directory": "~/Documents/Notes/", + "render": true, + "jumpToEndOfFileOnLaunch": true, + "editor": "neovim", + "journalRegex": ".*journal.*", + "dateEntry": "# %a %d %m %Y", + "newLineOnOpening": true, + "backup": { + "enable": false, + "directory": "/path/to/backup", + "interval": "weekly" + } +} +``` +`directory` is the directory where the program will search for vaults. + +`render` is a boolean that tells the program if it needs to render the markdown file using `Vivify` + +`jumpToEndOfFileOnLaunch` is a boolean that tells the programs if it needs to set the cursor at the end of the file on launch. + +`editor` is the editor the program will call. (It must be a supported editor. + +`journalRegex` is a regex code. If the note name matches with this code, the program will treat it as a journal. + +`dateEntry` is the style of the file title or paragraph title (depending on the type of journal). See [strftime](https://pubs.opengroup.org/onlinepubs/7908799/xsh/strftime.html?) for more info. + +`newLineOnOpening` is a boolean that tells the program to append a newline when opening a note. + +`backup.enable` enable the backups. The program relies on `rsync` for backuping. + +`backup.directory` is the directory where the backup will go. + +`backup.interval` can either be `daily`, `weekly`, `monthly` or an integer. It specifies the interval between two backups. + + ### What needs to be done (a lot) - [x] Add a way to create vaults @@ -57,15 +108,14 @@ Change `$/.config/notewrapper/config.json`. If it does not exist. On building, i - [x] Actually open vivify when opening nvim - [x] Write the journaling code (separate files or one big journal files) - [x] Adapt createNewNote with journals -- [ ] Automatic backups +- [x] Automatic backups - [x] Add flags for customization - [ ] Write a good README.md - [x] Option to not render and to only open nvim - [ ] Figure out this vivify issue https://github.com/jannis-baum/Vivify/issues/291 -- [ ] Maybe port this to other editors +- [ ] Port this to other editors - [x] Port to vim - [ ] Fixe all the small stuff marked //(TODO LATER) in the code -- [ ] Add a way to delete notes - [x] Comply with GPL-3 notice (add info about no waranty, etc.) - [ ] A converter for both type of journals diff --git a/config.json b/config.json index 2ead78a..0d573df 100644 --- a/config.json +++ b/config.json @@ -1,9 +1,14 @@ { - "directory": "$/Documents/Notes/", + "directory": "~/Documents/Notes/", "render": true, "jumpToEndOfFileOnLaunch": true, "editor": "neovim", "journalRegex": ".*journal.*", "dateEntry": "# %a %d %m %Y", - "newLineOnOpening": true + "newLineOnOpening": true, + "backup": { + "enable": false, + "directory": "/path/to/backup", + "interval": "weekly" + } } diff --git a/makefile b/makefile index 050bcea..a5c489f 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,7 @@ # Configuration CONFIG_DIR := $(HOME)/.config/notewrapper CONFIG_FILE := $(CONFIG_DIR)/config.json +CACHE_DIR := $(HOME)/.cache/notewrapper # Compiler and flags CC := gcc @@ -18,10 +19,10 @@ SRC := $(SRC_DIR)/main.c $(SRC_DIR)/ui.c $(SRC_DIR)/utils.c $(SRC_DIR)/notes.c TARGET := notewrapper # Phony targets -.PHONY: all clean run install_config +.PHONY: all clean run install_config create_cache_dir # Default target -all: install_config $(TARGET) +all: install_config create_cache_dir $(TARGET) # Build target $(TARGET): $(SRC) @@ -46,3 +47,6 @@ install_config: else \ echo "$(CONFIG_FILE) already exists, skipping installing default config file"; \ fi +# creates a dir in ~/.cache/ which is used to store data, such as time of last backup +create_cache_dir: + @mkdir -p $(CACHE_DIR) diff --git a/src/main.c b/src/main.c index 2a04bef..c048ddb 100644 --- a/src/main.c +++ b/src/main.c @@ -77,7 +77,7 @@ int main(int argc, char *argv[]) { char *notesDirectoryString = malloc(PATH_MAX); if (dirJson && cJSON_IsString(dirJson) && dirJson->valuestring != NULL) { char *rawPath = dirJson->valuestring; - if (rawPath[0] == '$') { // expands $ in the path + if (rawPath[0] == '~') { // expands ~ in the path snprintf(notesDirectoryString, PATH_MAX, "%s/%s", homedir, rawPath+1); } else { notesDirectoryString = rawPath; @@ -131,6 +131,46 @@ int main(int argc, char *argv[]) { newLineOnOpening = cJSON_IsTrue(newLineOnOpeningJSON) ? 1 : 0; } else {debug("config.json did not contained a correct \"newLineOnOpening\" value. The default is true.");} + + int doesBackup = 0; + int interval = 0; // this is an int. But some times it will be inputed a string. We must translate it. + char *pathToBackup = malloc(PATH_MAX); + cJSON *backupJSON = cJSON_GetObjectItem(json, "backup"); + if (backupJSON && cJSON_IsObject(backupJSON)) { + cJSON *doesBackupJSON = cJSON_GetObjectItem(backupJSON, "enable"); + if (doesBackupJSON && cJSON_IsBool(doesBackupJSON)) { + doesBackup = cJSON_IsTrue(doesBackupJSON) ? 1 : 0; + debug("doesBackup is set to %d", doesBackup); + cJSON *pathToBackupJSON = cJSON_GetObjectItem(backupJSON, "directory"); + if (pathToBackupJSON && cJSON_IsString(pathToBackupJSON)) { + if (pathToBackupJSON->valuestring[0] == '~') { // expands ~ + char *temp = pathToBackupJSON->valuestring; + temp++; + snprintf(pathToBackup, PATH_MAX, "%s/%s", homedir, temp); + debug("~ was expanded to %s\nThe backup path is %s", homedir, pathToBackup); + } else { + pathToBackup = strndup(pathToBackupJSON->valuestring, PATH_MAX); + debug("Directory for backup is set to %s in config.json", pathToBackup); // (TODO LATER) Maybe when in debug message when we say config.json get the actual config name. + } + } else {error(1, "user", "config.json did not contained a directory inside the backup section or the value is from an unexpected type");} + cJSON *intervalJSON = cJSON_GetObjectItem(backupJSON, "interval"); + if (intervalJSON && cJSON_IsString(intervalJSON)) { + if (strcmp(intervalJSON->valuestring, "daily") == 0) { + interval = DAILY; + debug("interval in config.json is set to \"daily\" which is %d", DAILY); + } else if (strcmp(intervalJSON->valuestring, "weekly") == 0) { + interval = WEEKLY; + debug("interval in config.json is set to \"weekly\" which is %d", WEEKLY); + } else if (strcmp(intervalJSON->valuestring, "monthly") == 0) { + interval = MONTHLY; + debug("interval in config.json is set to \"monthly\" which is %d", MONTHLY); + } else {error(1, "user", "Unexpected string %s for entry \"interval\" in config.json. You must put an int (number of seconds) or \"daily\" or \"weekly\" or \"monthly\".", intervalJSON->valuestring);} + } else if (intervalJSON && cJSON_IsNumber(intervalJSON)) { + interval = intervalJSON->valueint; // (TODO LATER) "writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead". we should pass all of these JSON->value... to the designed functions + debug("interval in config.json is set to %d", interval); + } else {error(1, "user", "config.json did not contained an interval value inside the backup section or the value is from an unexpected type");} + } else{error(1, "user", "config.json did not contained a enable value inside the backup section or the value is from an unexpected type");} + } else {debug("config.json did not contained a correct \"backup\" section. The default is {\"enable\": false}");} //cleans up cJSON_Delete(json); free(data); @@ -158,7 +198,7 @@ int main(int argc, char *argv[]) { printf(" -e, --editor Specify the editor to open.\n"); printf(" -j, --jump Jumps to the end of the file on opening.\n"); printf(" -J, --no-jump Do not jump to the end of the file\n"); - printf(" -n, --note Specify the note (or journal)."); + printf(" -n, --note Specify the note (or journal).\n"); printf(" -r, --render Renders the note with Vivify.\n"); printf(" -R, --no-render Do not render.\n"); printf(" -v, --vault Specify the vault.\n"); @@ -202,6 +242,10 @@ int main(int argc, char *argv[]) { error(!doesEditorExist(editorToOpen, shouldDebug), "user", "%s is either not in your path or not installed.", editorToOpen); debug("Finished parsing the attribute flags"); + if (doesBackup) { // (TODO LATER) If this takes too much time we should add a warning and when implementing multiple directories for vault we should verifiy this works. + handleBackups(notesDirectoryString, pathToBackup, homedir, interval, shouldDebug); + } + int shouldExit = 0; while(!shouldExit) { // this loop is the vault selector diff --git a/src/utils.c b/src/utils.c index 855f0f6..8363c05 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,4 +1,5 @@ #include "utils.h" +#include const char *supportedEditor[] = {"neovim", "vim", "nano"}; const int numEditors = 3; @@ -49,6 +50,128 @@ void _error(const int shouldDebug, const int condition, const char *type, const } } +void copyDir(const char *source, const char *destination, const int shouldDebug) { + debug("Backuping... source: %s and destination: %s", source, destination); + pid_t pid = fork(); + + error(pid < 0, "program", "fork() faild. pid = %d", pid); + + if (pid == 0) { + // Child process: execute rsync + char *args[] = { // (TODO LATER) + "rsync", + "-Lqah", // archive, follow links, human-readable // (TODO LATER) add a way to get verbose (-v) without it going on top of ncurses + "--progress", // show progress + "--update", // only update if newer + (char *)source, + (char *)destination, + NULL + }; + + execvp("rsync", args); + // If execvp returns, there was an error + error(1, "program", "execvp() failed"); + } else { + debug("Backup started asynchronously with PID %d\n", pid); + } +} +void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const char *homeDir, const int interval, const int shouldDebug) { + int shouldBackup = 0; + time_t now = time(NULL); + debug("Time since epoch is %ld", (long)now); + char cacheFilePATH[PATH_MAX]; + snprintf(cacheFilePATH, PATH_MAX, "%s/.cache/notewrapper/backupTime.txt", homeDir); + + struct stat st; + if (stat(cacheFilePATH, &st) != 0) { // file does not exist + shouldBackup = 1; + FILE *cacheFile = fopen(cacheFilePATH, "w"); + if (!cacheFile) { + error(1, "program", "Failed to open cache file for writing: %s", cacheFilePATH); + } + fprintf(cacheFile, "%ld", (long)now); + fclose(cacheFile); + } else { + FILE *cacheFile = fopen(cacheFilePATH, "r"); + if (!cacheFile) { + error(1, "program", "Failed to open cache file for reading: %s", cacheFilePATH); + } + + char line[64]; + if (fgets(line, sizeof(line), cacheFile) == NULL) { + error(1, "program", "Failed to read backup cache file (at %s)", cacheFilePATH); + } + fclose(cacheFile); + + char *endptr; + long lastBackupTime = strtol(line, &endptr, 10); + if (endptr == line || (*endptr != '\n' && *endptr != '\0')) { + error(1, "program", "Invalid timestamp in cache file: %s", line); + } + + if (difftime(now, lastBackupTime) > interval) { + shouldBackup = 1; + cacheFile = fopen(cacheFilePATH, "w"); + if (!cacheFile) { + error(1, "program", "Failed to open cache file for writing"); + } + fprintf(cacheFile, "%ld\n", (long)now); + fclose(cacheFile); + } + } + + if (shouldBackup) { + copyDir(pathOfVaults, pathOfBackup, shouldDebug); + } +} +/*void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const char *homeDir, const int interval, const int shouldDebug) { + int shouldBackup = 0; + time_t now = time(NULL); + debug("Time since epoch is %ld", (long)now); + char cacheFilePATH[PATH_MAX]; + // we store in this file the epoch time of the last backup to see if we need to backup + snprintf(cacheFilePATH, PATH_MAX, "%s/.cache/notewrapper/backupTime.txt", homeDir); + FILE *cacheFile = fopen(cacheFilePATH, "r"); + if (errno == ENOENT) { // if the files does not exists. We backup and set the backup time to now + shouldBackup = 1; + fclose(cacheFile); + cacheFile = fopen(cacheFilePATH, "w"); + fprintf(cacheFile, "%ld", (long)now); + fclose(cacheFile); + } else { + char line[64]; + if (fgets(line, sizeof(line), cacheFile) == NULL) { + // Error reading the file + error(1, "program", "Failed to read backup cache file (at %s)", cacheFilePATH); + fclose(cacheFile); + return; + } + fclose(cacheFile); + + // Convert the read line to long + char *endptr; + long lastBackupTime = strtol(line, &endptr, 10); + if (endptr == line || (*endptr != '\n' && *endptr != '\0')) { + error(1, "program", "Invalid timestamp in cache file: %s", line); + return; + } + + if (difftime(now, lastBackupTime) > interval) { + shouldBackup = 1; + // Update the cache file with current time + cacheFile = fopen(cacheFilePATH, "w"); + if (cacheFile == NULL) { + error(1, "program", "Failed to open cache file for writing"); + return; + } + fprintf(cacheFile, "%ld\n", (long)now); + fclose(cacheFile); + } + } + if (shouldBackup) { + copyDir(pathOfVaults, pathOfBackup, shouldDebug); + } +}*/ int doesEditorExist (char *editorToCheck, int shouldDebug) { // Some exectuables have not exaclty the same name as the editor. char *editor; diff --git a/src/utils.h b/src/utils.h index 13f24f5..4de35f0 100644 --- a/src/utils.h +++ b/src/utils.h @@ -18,6 +18,10 @@ #include #include #define BUFFER_SIZE 256 //standard buffer size. +// the three supported values are "daily" "weekly" and "monthly" (for months we use the avarage lenght of a month in a non leap year). All values were calculated on my loyal TI-30 ECO RS +#define DAILY 86400 +#define WEEKLY 604800 +#define MONTHLY 2628000 // calculated from the average lenght of a non leap year #define debug(message, ...) \ _debug(shouldDebug, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__) #define altDebug(message, ...) \ @@ -67,4 +71,7 @@ int openEditor(char *path, char *editor, int render, int shouldJumpToEndOfFile, // returns a string (buffered at 256 chars). // you should not forgot to free this string as it is in the heap. char *getFormatedTime(char *format, int shouldDebug); +/* Caclulates if we need to do another backup (by reading ~/.cache/NoteWrapper/backupTime.txt. +If need launches in the background rsync to do the backuping.*/ +void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const char *homeDir, const int interval, const int shouldDebug); #endif