Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
pkg-config \
libcjson-dev \
libncurses-dev \
libgit2-dev \
git \
neovim
- name: Prepare environnement
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Before building NoteWrapper, you must install the following dependencies:
* `sed`
* `ripgrep`
* `fzf`
* `libgit2`

You must also have a [supported editor (and their associated plugin if needed)](#editor-support) installed:

Expand Down Expand Up @@ -157,11 +158,13 @@ NoteWrapper relies on certain editor features, so not all functionality is suppo

### Features requiring editor support:

* **Bufferless rendering**: updates the rendered view while typing (without saving)
* **Cursor following**: rendered view follows the cursor position
* **Jump to end on open**: automatically moves the cursor to the end of the file
* **Bufferless rendering**: updates the rendered view while typing (without saving). You won't need to save the file to see it updated in your browser.
* **Cursor following**: rendered view follows the cursor position. When you scroll in your editor, it will automatically scroll your browser view.
* **Jump to end on open**: automatically moves the cursor to the end of the file when oppening.

The first two features depend on [Vivify's editor integration](https://github.com/jannis-baum/Vivify?tab=readme-ov-file#existing-integration) and are mainly useful if you want external Markdown rendering in your browser.
The first two features depend on [Vivify's editor integration](https://github.com/jannis-baum/Vivify?tab=readme-ov-file#existing-integration)[^1] and are mainly useful if you want external Markdown rendering in your browser.

[^1]: Eventhough the features are Vivify-related, please report bugs and ask questions in this repo.

If your editor does not support these features, you can implement a plugin using [Vivify's API](https://github.com/jannis-baum/Vivify?tab=readme-ov-file#editor-support).

Expand Down Expand Up @@ -200,7 +203,12 @@ Edit `~/.config/notewrapper/config.json`. If it does not exist, it will be creat
"/other/paths/": "path/to/backup2"
},
"interval": "weekly",
"rsyncArgs": ["-Lqah", "--update"]
"rsyncArgs": ["-Lqah", "--update"],
"git": {
"enable": false,
"name": "Example Example",
"email": "mail@example.com"
}
}
}
```
Expand All @@ -218,6 +226,8 @@ Edit `~/.config/notewrapper/config.json`. If it does not exist, it will be creat
* `backup.directory`: backup's destination for each directory
* `backup.interval`: backup frequency (`daily`, `weekly`, `monthly`, or integer)
* `backup.rsyncArgs`: arguments passed to `rsync`
* `git.enable`: enable git version control for backups
* `git.name` and `git.email` are for the commits signature

Note:
* Directories must end with `/`.
Expand All @@ -228,6 +238,8 @@ Note:
* `journalRegex` must match if the file name ends with `.md` and if it doesn't.
* It is recommended to keep `-q` or `--quiet` flag in `rsyncArgs` to avoid interference with `ncurses`.
* If `rsync` fails, you will see it inside `ncurses`.
* If you want the git backup but without rsync, you can leave `backup.directory` empty or map each directory to itself. When enabled, git will apply to all your vaults.
* Each vault gets its own git repository that will be created the first time you select the vault with the option enabled.

---

Expand Down
2 changes: 2 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ripgrep
fzf
gnused
libgit2
];

buildPhase = ''
Expand All @@ -51,6 +52,7 @@
valgrind
clang-tools
gnumake
libgit2
];
};

Expand Down
4 changes: 2 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ VERSION := $(shell git describe --tags --always --dirty)

CFLAGS := -Wall -Wextra -Werror -O2 \
-DVERSION=\"$(VERSION)\" \
$(shell pkg-config --cflags libcjson ncurses)
$(shell pkg-config --cflags libcjson ncurses libgit2)

LDFLAGS := $(shell pkg-config --libs libcjson ncurses)
LDFLAGS := $(shell pkg-config --libs libcjson ncurses libgit2)

SRC := src/main.c src/ui.c src/utils.c src/notes.c
TARGET := notewrapper
Expand Down
69 changes: 61 additions & 8 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ int main(int argc, char *argv[]) {
debug("Parsing the JSON config");

cJSON *json = cJSON_Parse(data);

if (!json) {
const char *error_ptr = cJSON_GetErrorPtr();

if (error_ptr) {
error(1, "program", "JSON parse error near: %.30s", error_ptr);
}

free(data);
}
error(!json, "program", "JSON parse error");
Expand Down Expand Up @@ -210,6 +217,9 @@ int main(int argc, char *argv[]) {
char **backupDirectoriesArray = malloc(numDirectories * sizeof(char *));
char **rsyncArgs = NULL;
int rsyncArgsNumber = 0;
int gitEnabled = 0;
char *gitSignatureName = NULL;
char *gitSignatureEmail = NULL;
cJSON *backupJSON = cJSON_GetObjectItem(json, "backup");
if (backupJSON && cJSON_IsObject(backupJSON)) {
cJSON *doesBackupJSON = cJSON_GetObjectItem(backupJSON, "enable");
Expand Down Expand Up @@ -285,11 +295,36 @@ int main(int argc, char *argv[]) {
"value is from an unexpected type",
configPath);
}

// Configure Git backup
cJSON *gitJSON = cJSON_GetObjectItem(backupJSON, "git");

if (gitJSON && cJSON_IsObject(gitJSON)) {
cJSON *gitEnableJSON = cJSON_GetObjectItem(gitJSON, "enable");
cJSON *gitNameJSON = cJSON_GetObjectItem(gitJSON, "name");
cJSON *gitEmailJSON = cJSON_GetObjectItem(gitJSON, "email");

error(!gitEnableJSON || !cJSON_IsBool(gitEnableJSON), "user", "Entry \"git.enable\" in %s must be a bool.", configPath);
error(!gitNameJSON || !cJSON_IsString(gitNameJSON), "user", "Entry \"git.name\" in %s must be a string.", configPath);
error(!gitEmailJSON || !cJSON_IsString(gitEmailJSON), "user", "Entry \"git.email\" in %s must be a string.", configPath);

gitEnabled = cJSON_IsTrue(gitEnableJSON) ? 1 : 0;

if (gitEnabled) {
gitSignatureName = strdup(cJSON_GetStringValue(gitNameJSON));
gitSignatureEmail = strdup(cJSON_GetStringValue(gitEmailJSON));

error(gitSignatureName == NULL || gitSignatureEmail == NULL, "program", "malloc failed");

debug("git in %s is enabled with name \"%s\" and email \"%s\"", configPath, gitSignatureName, gitSignatureEmail);
} else {
debug("git in %s is disabled", configPath);
}
} else {
error(1, "user", "Entry \"git\" in %s must be an object containing \"enable\", \"name\", and \"email\".", configPath);
}
} else {
error(1, "user",
"%s did not contained a enable value inside the backup section or the value is "
"from an unexpected type",
configPath);
error(1, "user", "%s did not contained a enable value inside the backup section or the value is from an unexpected type", configPath);
}
// handle rsyncs array of arguments.
cJSON *rsyncArgsJSON = cJSON_GetObjectItem(backupJSON, "rsyncArgs");
Expand All @@ -314,9 +349,7 @@ int main(int argc, char *argv[]) {
configPath);
}
} else {
debug("In %s, \"backup\" wasn't set or we encountered a abnormal type. Defaulting to "
"{\"enable\": false}.",
configPath);
debug("In %s, \"backup\" wasn't set or we encountered a abnormal type. Defaulting to {\"enable\": false}.", configPath);
}
backup_config_end:

Expand Down Expand Up @@ -541,6 +574,12 @@ int main(int argc, char *argv[]) {

initscr(); // initialize ncurses

// initialized libgit2
if (gitEnabled) {
int libgit2InitializationTimes = git_libgit2_init();
error(libgit2InitializationTimes != 1, "program", "libgit2 has been initialized %d times (instead of one)", libgit2InitializationTimes);
}

int shouldExit = 0;
while (!shouldExit) {
// this loop is the vault selector
Expand Down Expand Up @@ -621,6 +660,11 @@ int main(int argc, char *argv[]) {
int shouldChangeVault = 0;
// we must find the directory from which the vault comes again.
char *notesDirectoryString = getDirectoryFromVault(vaultSelected, vaultsArray, vaultsCount, vaultsCountForEachDirectory, directoriesArray, numDirectories, shouldDebug);

if (gitEnabled) { // we check if we need to git init the vault each time we enter it
ensureGitDirectory(notesDirectoryString, vaultSelected, gitSignatureName, gitSignatureEmail, shouldDebug);
}

while (!shouldExit && !shouldChangeVault) {
// this loop is the note selector
int filesCount = 0;
Expand Down Expand Up @@ -722,7 +766,16 @@ int main(int argc, char *argv[]) {
openEditor(fullPath, editorToOpen, shouldRender, shouldJumpToEnd, shouldDebug);
free(fullPath);
}
free(returnNoteSelection);
openEditor(fullPath, editorToOpen, shouldRender, shouldJumpToEnd, shouldDebug);
if (gitEnabled) { // After closing a file, we need to update the git repo (if git is enable
char *date = getDateAndTime();
char *commitMsg = malloc(29);
snprintf(commitMsg, 29, "Update - %s", date);
gitBackupUpdate(notesDirectoryString, vaultSelected, gitSignatureName, gitSignatureEmail, commitMsg, shouldDebug);
free(date);
free(commitMsg);
}
free(fullPath);
} else if (strcmp(noteSelected, "Create new note") == 0) {
note_creation:
noteSelected = createNewNote(notesDirectoryString, vaultSelected, bypassSelectionNote, bypassSelectionNoteValue, journalRegex, shouldDebug);
Expand Down
157 changes: 155 additions & 2 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,144 @@ static void ensureDir(const char *path, const int shouldDebug) {
}
}

void ensureGitDirectory(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const int shouldDebug) {
git_repository *repo = NULL;
char *fullPath = malloc(PATH_MAX);
snprintf(fullPath, PATH_MAX, "%s/%s", path, vault);

int git_repo_error_code = git_repository_open(&repo, fullPath);
if (git_repo_error_code) { // error (most probably that the git repo wasn't initialized)
int git_init_error_code = git_repository_init(&repo, fullPath, 0); // the 0 indicates that the directory isn't bare. See https://libgit2.org/docs/reference/main/repository/git_repository_init.html
error(git_init_error_code, "program", "git initialization didn't worked. Error code : %d\nSee https://libgit2.org/docs/reference/main/repository/git_repository_init.html for documentation\n\n%s",
git_init_error_code, git_error_last()->message);
debug("git repo %s was initialized.", fullPath);

// we need to setup the .gitignore
char *gitignorePath = malloc(PATH_MAX);
snprintf(gitignorePath, PATH_MAX, "%s/.gitignore", fullPath);
FILE *gitignore = fopen(gitignorePath, "a");
error(gitignore == NULL, "program", "failed to open/create %s\n%s", gitignorePath, git_error_last()->message);
fputs("*\n", gitignore);
fputs("!*/\n", gitignore);
fputs("!*.md\n", gitignore);
fclose(gitignore);
debug("Succesfully wrote .gitignore to %s", gitignorePath);
git_repository_free(repo); // we free to be able to open it in gitBackupUpdate()
char *date = getDateAndTime();
char *commitMsg = malloc(37);
snprintf(commitMsg, 37, "Initial commit - %s", date);
gitBackupUpdate(path, vault, signatureName, signatureEmail, commitMsg, shouldDebug);
free(date);
free(commitMsg);
} else { // the git repo already exists
debug("%s is already a git repo. Skipping initialization...", fullPath);
git_repository_free(repo);
}

free(fullPath);
}

void gitBackupUpdate(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const char *commitMsg, const int shouldDebug) {
git_repository *repo = NULL;
git_index *index = NULL;
git_tree *tree = NULL;
git_tree *head_tree = NULL;
git_commit *head_commit = NULL;
git_signature *signature = NULL;
git_diff *diff = NULL;

git_oid tree_id;
git_oid commit_id;

char *fullPath = malloc(PATH_MAX);

error(fullPath == NULL, "program", "Failed to allocate path");

snprintf(fullPath, PATH_MAX, "%s/%s", path, vault);

/* Open repository */
int return_code = git_repository_open(&repo, fullPath);

error(return_code, "program", "Failed to open Git repository\n%s", git_error_last()->message);

/* Get the index */
return_code = git_repository_index(&index, repo);
error(return_code, "program", "Failed to get Git index\n%s", git_error_last()->message);

/* git add . */
return_code = git_index_add_all(index, NULL, 0, NULL, NULL);
error(return_code, "program", "Failed to add files\n%s", git_error_last()->message);

return_code = git_index_write(index);
error(return_code, "program", "Failed to write index\n%s", git_error_last()->message);

/*
* Check whether there are changes to commit.
*
* For a new repository there is no HEAD yet, so the index
* itself tells us whether there is anything to commit.
*/
int has_changes = 0;

if (git_repository_head_unborn(repo)) {
has_changes = git_index_entrycount(index) > 0;
} else {
git_oid head_oid;

return_code = git_reference_name_to_id(&head_oid, repo, "HEAD");
error(return_code, "program", "Failed to get HEAD\n%s", git_error_last()->message);

return_code = git_commit_lookup(&head_commit, repo, &head_oid);
error(return_code, "program", "Failed to lookup HEAD commit\n%s", git_error_last()->message);

return_code = git_commit_tree(&head_tree, head_commit);
error(return_code, "program", "Failed to get HEAD tree\n%s", git_error_last()->message);

return_code = git_diff_tree_to_index(&diff, repo, head_tree, index, NULL);

error(return_code, "program", "Failed to create diff\n%s", git_error_last()->message);

has_changes = git_diff_num_deltas(diff) > 0;
}

/* Nothing changed, so don't create a commit */
if (!has_changes) {
debug("No changes to commit in %s", fullPath);
goto cleanup;
}

/* index → tree */
return_code = git_index_write_tree(&tree_id, index);
error(return_code, "program", "Failed to write tree\n%s", git_error_last()->message);

return_code = git_tree_lookup(&tree, repo, &tree_id);
error(return_code, "program", "Failed to lookup tree\n%s", git_error_last()->message);

/* Author/committer */
return_code = git_signature_now(&signature, signatureName, signatureEmail);
error(return_code, "program", "Failed to create signature\n%s", git_error_last()->message);

/* Create commit */
if (git_repository_head_unborn(repo)) {
return_code = git_commit_create_v(&commit_id, repo, "HEAD", signature, signature, NULL, commitMsg, tree, 0);
} else {
return_code = git_commit_create_v(&commit_id, repo, "HEAD", signature, signature, NULL, commitMsg, tree, 1, head_commit);
}
error(return_code, "program", "Failed to create commit\n%s", git_error_last()->message);

debug("Created commit %s in %s", commitMsg, fullPath);

cleanup:
git_diff_free(diff);
git_signature_free(signature);
git_tree_free(tree);
git_tree_free(head_tree);
git_commit_free(head_commit);
git_index_free(index);
git_repository_free(repo);
free(fullPath);
}

void initAppFilesAndDirs(const char *home, const int shouldDebug) {
// sometimes .cache and .config doesn't exist. Such as with github actions machines
char config_dir[PATH_MAX];
Expand Down Expand Up @@ -157,15 +295,30 @@ void initAppFilesAndDirs(const char *home, const int shouldDebug) {
" \"enable\": false,\n"
" \"directory\": {\n"
" \"~/Documents/\": \"path/to/backup/\"\n"
" },\n"
" },\n"
" \"interval\": \"weekly\",\n"
" \"rsyncArgs\": [\"-Lqah\", \"--update\"]\n"
" \"rsyncArgs\": [\"-Lqah\", \"--update\"],\n"
" \"git\": {\n"
" \"enable\": false,\n"
" \"email\": \"mail@example.com\",\n"
" \"name\": \"Example Example\"\n"
" }\n"
" }\n"
"}\n");

fclose(w);
}

char *getDateAndTime() {
char *return_value = malloc(20);
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);

strftime(return_value, sizeof(return_value), "%Y-%m-%%d %H:%M:%%S", &tm_now);
return return_value;
}

void handleBackups(char **sourceDirectoryArray, const int sourceNumber, char **destinationDirectoryArray, const char *homeDir, const int interval, const char **rsyncArguments, const int rsyncArgumentsNumber,
const int forceBackup, int *backupMessage, const int shouldDebug) {
int shouldBackup = 0;
Expand Down
Loading
Loading