Compare commits

..
62 Commits
Author SHA1 Message Date
tomasr 1506d43ffb Merge pull request #75 from tomasriveral/flake-update/2026-10-05 2026-10-05 17:47:16 +02:00
github-actions[bot] e1c63a1694 chore: weekly flake update 2026-10-05 10:17:06 +00:00
tomasr e01e580162 Merge pull request #74 from tomasriveral/flake-update/2026-09-28 2026-09-28 18:32:01 +02:00
github-actions[bot] b2d76adb88 chore: weekly flake update 2026-09-28 09:37:56 +00:00
tomasr 7403bed29c Merge pull request #73 from tomasriveral/flake-update/2026-09-21 2026-09-21 12:41:37 +02:00
github-actions[bot] 13bacbb6d6 chore: weekly flake update 2026-09-21 08:49:09 +00:00
tomasr 886eff21d3 Merge pull request #72 from tomasriveral/flake-update/2026-09-14 2026-09-14 12:38:03 +02:00
github-actions[bot] bcf30a4576 chore: weekly flake update 2026-09-14 08:47:43 +00:00
tomasr 72bf03bc9f Merge pull request #71 from tomasriveral/flake-update/2026-09-07 2026-09-07 12:00:23 +02:00
github-actions[bot] 7ca0bf1561 chore: weekly flake update 2026-09-07 08:10:41 +00:00
tomasr 94d659db2c Merge pull request #70 from tomasriveral/flake-update/2026-08-31 2026-08-31 16:40:06 +02:00
github-actions[bot] e01be6ecea chore: weekly flake update 2026-08-31 09:36:47 +00:00
tomasr 713fa27893 Merge pull request #69 from tomasriveral/flake-update/2026-08-24
[Automated] Weekly flake update 2026-08-24
2026-08-24 06:51:22 +02:00
github-actions[bot] dd7345c2fa chore: weekly flake update 2026-08-24 04:00:09 +00:00
tomasr dd58a86fce Merge (#68) from tomasriveral/fix-git-backup
feat: warning for slow git backups + docs: improved demo
2026-08-18 18:40:24 +02:00
tomasr 19610c1c51 fix: clang format 2026-08-18 18:37:15 +02:00
tomasr 39a3cb6bba docs: redone demo gif
It was first done on a WebDav mounted drive. That meant that read and
write operations were really slow.
2026-08-17 17:54:39 +02:00
tomasr 0d5da72b13 feat: show warning if git operation too slow 2026-08-17 17:43:00 +02:00
tomasr e78a9ca669 fix: git commit message being truncated 2026-08-17 17:16:06 +02:00
tomasr c83a504648 Merge pull request #67 from tomasriveral/flake-update/2026-08-17 2026-08-17 08:18:04 +02:00
github-actions[bot] 2e301b0b52 chore: weekly flake update 2026-08-17 03:56:10 +00:00
tomasr c7a9d45a65 Merge (#66) from tomasriveral/vhs-demo-gif
docs: add demo and miscellanious improvement in README.md
2026-08-16 15:11:37 +02:00
tomasr 62da4cfb2e docs: add demo and miscellanious improvement in README.md 2026-08-16 15:09:28 +02:00
tomasr d56e7d9e4f Merge (#65) from tomasriveral/fix-double-free
fix: double free and double openEditor
2026-08-16 08:38:07 +02:00
tomasr 449a9eaca6 fix: double free and double openEditor
I hadn't pulled latest change to main before my last branch-checkout.
There was some old code mixed with new one. Specially, the "note return"
button introduced in last version displaced the openEditor and some
free() inside a loop. That caused duplication of this part of the code.
I should be carefull next time to work on up-to-date branches.
2026-08-16 08:19:53 +02:00
tomasr edc979dccb Merge (#63) from tomasriveral/git-backup
feat: git version control for backup
2026-08-14 17:48:15 +02:00
tomasr 5f37e48030 CI: add libgit2 dependency 2026-08-14 17:46:49 +02:00
tomasr df0301fdcd fix: clang format 2026-08-14 17:44:59 +02:00
tomasr 518d4fd09c Merge branch 'main' into git-backup 2026-08-14 17:43:17 +02:00
tomasr a83da3e1aa docs: clarify editor specific features
Added better explaination and footnote to avoid people oppening
issues/disscussion threads in Vivify instead of NoteWrapper. See
https://github.com/jannis-baum/Vivify/discussions/299
2026-08-14 17:36:48 +02:00
tomasr eb0d6b6e5d docs: document git backup 2026-08-14 17:33:20 +02:00
tomasr 82d016fdd7 feat: git version control for backup 2026-08-14 17:28:15 +02:00
tomasr 2e03edac2b flake: add libgit2 dependency 2026-08-14 15:14:53 +02:00
tomasr 689c3074c0 Merge pull request #60 from tomasriveral/flake-update/2026-08-10 2026-08-10 09:13:26 +02:00
github-actions[bot] e54933e5fa chore: weekly flake update 2026-08-10 04:45:43 +00:00
tomasr 3f9471531d Merge pull request #58 from tomasriveral/flake-update/2026-08-03 2026-08-03 11:11:57 +02:00
github-actions[bot] ab1928551a chore: weekly flake update 2026-08-03 06:31:05 +00:00
tomasr e75c8e5815 Merge pull request #57 from tomasriveral/flake-update/2026-07-27 2026-07-27 13:06:13 +02:00
github-actions[bot] 693e85d974 chore: weekly flake update 2026-07-27 06:37:15 +00:00
tomasr b9f71f580a Merge pull request #55 from tomasriveral/flake-update/2026-07-13 2026-07-13 19:24:40 -05:00
github-actions[bot] c6aac80951 chore: weekly flake update 2026-07-13 06:26:42 +00:00
tomasr b395fe1072 Merge pull request #52 from tomasriveral/flake-update/2026-06-22 2026-06-23 01:43:20 +00:00
github-actions[bot] 27cc3de27d chore: weekly flake update 2026-06-22 09:18:09 +00:00
tomasr 18538f3250 Merge pull request #51 from tomasriveral/flake-update/2026-06-15 2026-06-15 09:34:33 +00:00
github-actions[bot] 6c7cc413da chore: weekly flake update 2026-06-15 09:31:17 +00:00
tomasr e670ef935f Merge pull request #50 from tomasriveral/flake-update/2026-06-08
[Automated] Weekly flake update 2026-06-08
2026-06-08 08:53:32 +00:00
github-actions[bot] 29d86955f4 chore: weekly flake update 2026-06-08 08:19:19 +00:00
tomasr 0f9ec32bfc Merge (#49) from tomasriveral/release/v2.3.0
[Automatic] Release PR v2.3.0
2026-06-02 14:41:02 +02:00
github-actions[bot] 5ad8f2f99e chore(release): v2.3.0 2026-06-02 12:40:33 +00:00
tomasr c6e590e57c Merge (#48) from tomasriveral/vault-indicator
feat: vault indicator
2026-06-02 14:39:23 +02:00
tomasr d47bdb82eb feat: vault indicator 2026-06-02 14:37:39 +02:00
tomasr 50748eeac0 Merge (#47) from tomasriveral/ncurse-quick-selection
feat: ncurses quick select
2026-06-02 14:32:43 +02:00
tomasr 19a8f7e07f feat: ncurses quick select 2026-06-02 14:30:16 +02:00
tomasr 1c17c62835 Merge (#46) from tomasriveral/journal-return-to-vault
feat: button return to note selection from journal entry selection
2026-06-02 14:05:49 +02:00
tomasr e666ca32de feat: button return to note selection from journal entry selection 2026-06-02 14:03:05 +02:00
tomasr 2a8392ac2e Merge (#45) from tomasriveral/vault-fzf-search
feat: fzf search inside vaults
2026-06-02 13:51:45 +02:00
tomasr 292688296e format: fix 2026-06-02 13:46:03 +02:00
tomasr 4c18c2dda4 feat: fzf search inside vaults 2026-06-02 13:42:46 +02:00
tomasr 1bfb7f82a5 fix: fzfSearch when journal entry 2026-06-02 13:42:37 +02:00
tomasr a32f97d229 Merge (#44) from tomasriveral/release/v2.2.1
[Automatic] Release PR v2.2.1
2026-06-01 11:46:28 +02:00
github-actions[bot] 6555b7247e chore(release): v2.2.1 2026-06-01 09:42:58 +00:00
tomasr 2b2a3455d2 Merge (#43) from tomasriveral/fix-flake-src
fix: flake src
2026-06-01 11:41:29 +02:00
14 changed files with 626 additions and 95 deletions

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ jobs:
pkg-config \
libcjson-dev \
libncurses-dev \
libgit2-dev \
git \
neovim
- name: Prepare environnement
+3 -1
View File
@@ -10,7 +10,7 @@ This project aims to remain minimal, terminal-focused, and easy to extend. Contr
Please use the standardized debugging and error-handling functions defined in [/src/utils.c](./src/utils.c):
* `debug(...)`
* `debug("message", ...)`
Used for formatted debugging output (printf-style formatting).
* `altDebug(...)`
@@ -21,6 +21,8 @@ Please use the standardized debugging and error-handling functions defined in [/
* `"user"`: errors caused by user input or configuration
* `"program"`: internal or unexpected program errors
* `warn(condition, "message", ...)`
Used for formatted warning (printf-style formatting).
---
+29 -14
View File
@@ -4,6 +4,8 @@ Note: This project fullfills all my usecases for now, so my main source for feat
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tomasriveral/notewrapper)
![demo.gif](demo.gif)
## Why?
I started journaling and note-taking with **[Obsidian](https://obsidian.md/)** but wanted to use only free software. I also tried **[Logseq](https://logseq.com/)** and **[Joplin](https://github.com/laurent22/joplin)**, but I preferred a terminal-based workflow.
@@ -41,6 +43,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:
@@ -157,11 +160,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).
@@ -200,7 +205,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"
}
}
}
```
@@ -218,16 +228,21 @@ 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 `/`.
* All directories used must exist.
* Elements of `directory` and `backup.directory` must match exactly.
* Directories set in `directory` but not in `backup.directory` won't be backed up.
* Directories set in `backup.directory` but not in `directory` will be ignored.
* `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`.
> [!NOTE]
> * The git backup implementation relies a lot on the IO speed of your drive. A low speed one (or a web mounted one) could create blanks between closing a document and the TUI opening.
> * Directories must end with `/`.
> * All directories used must exist.
> * Elements of `directory` and `backup.directory` must match exactly.
> * Directories set in `directory` but not in `backup.directory` won't be backed up.
> * Directories set in `backup.directory` but not in `directory` will be ignored.
> * `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.
---
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

+190
View File
@@ -0,0 +1,190 @@
Require echo
Set Shell "zsh"
Set TypingSpeed 35ms
Set FontSize 18
Set Width 1800
Set Height 900
Output demo.gif
# ─────────────────────────────────────────────────────────────────────
# Introduction
# ─────────────────────────────────────────────────────────────────────
Type "echo 'Welcome to NoteWrapper!'"
Sleep 800ms
Enter
Sleep 1500ms
Type "notewrapper --help"
Sleep 500ms
Enter
Sleep 4000ms
# ─────────────────────────────────────────────────────────────────────
# Create / open a unified journal
# ─────────────────────────────────────────────────────────────────────
Type "notewrapper"
Sleep 700ms
Enter
Sleep 1500ms
Down
Sleep 500ms
Down
Sleep 500ms
Down
Sleep 700ms
Enter
Sleep 1200ms
Down
Sleep 600ms
Enter
Sleep 1500ms
# Enter the editor
Type "i"
Sleep 800ms
Type "NoteWrapper uses standard Markdown files, so you can open your notes in any other application that supports Markdown."
Sleep 700ms
Enter
Sleep 800ms
Type "This is a unified journal: all your daily entries live in the same file. You can also configure NoteWrapper to use monthly or other time-based entries."
Sleep 700ms
Enter
Sleep 800ms
Type "And, although you can't see it in this demo, this Markdown file is rendered live in my browser thanks to Vivify!"
Sleep 1500ms
Sleep 3000ms
# Leave the editor
Escape
Sleep 500ms
Type ":q!"
Sleep 800ms
Enter
Sleep 1500ms
# ─────────────────────────────────────────────────────────────────────
# Create / open a divided journal
# ─────────────────────────────────────────────────────────────────────
Down
Sleep 500ms
Down
Sleep 500ms
Down
Sleep 700ms
Enter
Sleep 2000ms
Enter
Sleep 1000ms
Type "i"
Sleep 800ms
Type "And this is a divided journal."
Sleep 500ms
Type " It uses one file per entry."
Sleep 700ms
Enter
Sleep 1000ms
Type "Find the dates and journal divisions a little too cumbersome?"
Sleep 700ms
Enter
Sleep 1500ms
Type "Don't worry: NoteWrapper also works with basic notes."
Sleep 800ms
Type " It distinguishes them using a regular expression, which defaults to `.*journal.*`."
Sleep 800ms
Enter
Sleep 700ms
Type " The unified or divided journal mode is chosen when you first create the journal."
Sleep 1000ms
Enter
Sleep 1500ms
# ─────────────────────────────────────────────────────────────────────
# Search
# ─────────────────────────────────────────────────────────────────────
Type "Let me show you one last cool feature."
Sleep 1000ms
Type " Have you ever forgotten when or where you wrote something?"
Sleep 1000ms
Enter
Sleep 800ms
Type "Well ... NoteWrapper was made for you!"
Sleep 1500ms
Escape
Sleep 500ms
Type ":q!"
Sleep 800ms
Enter
Sleep 1500ms
# Navigate to search
Down
Sleep 500ms
Down
Sleep 500ms
Down
Sleep 800ms
Enter
Sleep 1500ms
Down
Sleep 500ms
Down
Sleep 800ms
Enter
Sleep 2000ms
# Search all notes with fzf
Type "c"
Sleep 300ms
Type "o"
Sleep 300ms
Type "o"
Sleep 300ms
Type "l"
Sleep 1500ms
# Show the search result / preview
Up
Sleep 2000ms
Enter
Sleep 1000ms
# ─────────────────────────────────────────────────────────────────────
# Final note
# ─────────────────────────────────────────────────────────────────────
Type "i"
Sleep 700ms
Type "Never lose a note."
Sleep 1500ms
Enter
Sleep 1000ms
Type "P. S. Don't mind the broken fzf preview. Normally, you'll see the context around your search result, but sometimes `sed` breaks when fzf is opened for the first time."
Sleep 5000ms
Generated
+3 -3
View File
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"lastModified": 1791048980,
"narHash": "sha256-KgItSKML8Xvte0B7/uGnBDsYzSnnKHcOaiUbgWBXLXw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"rev": "a7868a727837f3c09cee2ce0ca671c76b1589fed",
"type": "github"
},
"original": {
+4 -1
View File
@@ -13,7 +13,7 @@
in {
packages.default = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "notewrapper";
version = "v2.2";
version = "v2.3.0";
src = ./.;
@@ -30,6 +30,7 @@
ripgrep
fzf
gnused
libgit2
];
buildPhase = ''
@@ -51,6 +52,8 @@
valgrind
clang-tools
gnumake
libgit2
vhs
];
};
+2 -2
View File
@@ -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
+87 -17
View File
@@ -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");
@@ -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");
@@ -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 {
error(1, "user",
"%s did not contained a enable value inside the backup section or the value is "
"from an unexpected type",
configPath);
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);
}
// handle rsyncs array of arguments.
cJSON *rsyncArgsJSON = cJSON_GetObjectItem(backupJSON, "rsyncArgs");
@@ -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:
@@ -541,6 +574,12 @@ backup_config_end:
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
@@ -598,7 +637,8 @@ backup_config_end:
*backupMessage = 0; // we reset it to show the message only once
vaultSelected = ncursesSelect(vaultsArray, "Select vault to open (Use arrows or WASD, Enter to select):", vaultsCount, extraOptions, " ", "Or select an option below", "", shouldDebug);
vaultSelected = ncursesSelect(vaultsArray, "Select vault to open (Use arrows or WASD, Enter to select or use 1-9 to quick select or use 1-9 to quick select):", vaultsCount, extraOptions, " ",
"Or select an option below", "", shouldDebug);
// now that we won't use vaultsArray in this iteration of the loop, we should free it and
// all its elements. (As this is memory in the heap and not the stack and thus is our
@@ -620,6 +660,11 @@ backup_config_end:
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;
@@ -644,12 +689,13 @@ backup_config_end:
altDebug("└------------------------------\n");
}
// adds options
int extraNotesOptions = 4;
int extraNotesOptions = 5;
filesArray = realloc(filesArray, (filesCount + extraNotesOptions) * sizeof(char *)); // resize filesArray to fit the extra options
filesArray[filesCount] = "Create new note";
filesArray[filesCount + 1] = "Back to vault selection";
filesArray[filesCount + 2] = "Delete vault";
filesArray[filesCount + 3] = "Quit (Ctrl+C)";
filesArray[filesCount + 1] = "Search inside the vault";
filesArray[filesCount + 2] = "Back to vault selection";
filesArray[filesCount + 3] = "Delete vault";
filesArray[filesCount + 4] = "Quit (Ctrl+C)";
char *noteSelected;
// if we set to bypass the note selector
if (bypassSelectionNote) {
@@ -665,8 +711,9 @@ backup_config_end:
goto note_creation;
}
}
noteSelected =
ncursesSelect(filesArray, "Select note or journal to open (Use arrows or WASD, Enter to select):", filesCount, extraNotesOptions, " ", "Or select an option below", "", shouldDebug);
char vaultMessage[PATH_MAX];
snprintf(vaultMessage, PATH_MAX, "%s: Select note or journal to open (Use arrows or WASD, Enter to select or use 1-9 to quick select):", vaultSelected);
noteSelected = ncursesSelect(filesArray, vaultMessage, filesCount, extraNotesOptions, " ", "Or select an option below", "", shouldDebug);
// now that we won't use filesArray in this iteration of the loop, we should free it
// and all its elements. (As this is memory in the heap and not the stack and thus
// is our responsability to manage)
@@ -680,7 +727,7 @@ backup_config_end:
free(filesArray);
debug("Selected note: %s", noteSelected);
if (strcmp(noteSelected, "Create new note") != 0 && strcmp(noteSelected, "Back to vault selection") != 0 && strcmp(noteSelected, "Delete vault") != 0 &&
strcmp(noteSelected, "Quit (Ctrl+C)") != 0) {
strcmp(noteSelected, "Quit (Ctrl+C)") != 0 && strcmp(noteSelected, "Search inside the vault") != 0) {
open_note:
bypassSelectionNote = 0; // we must reset bypassSelectionNote to avoid getting into an infinite
// loop of bypassing the note selection
@@ -694,12 +741,18 @@ backup_config_end:
int *journalWasUpdated = malloc(sizeof(int));
*journalWasUpdated = 0;
// used to go back to note selection
int *returnNoteSelection = malloc(sizeof(int));
*returnNoteSelection = 0;
if (!regexReturn) { // if the regex matches -> it's a journal
debug("%s is a journal. Updating it...", noteSelected);
fullPath = updateJournal(fullPath, noteSelected, timeFormat, journalWasUpdated,
fullPath = updateJournal(fullPath, noteSelected, timeFormat, journalWasUpdated, returnNoteSelection,
shouldDebug); // we return the path. As if it is a divided journal we
// must point to the correct entry
}
if (!*returnNoteSelection) { // if we don't need to return to the note selection
if (newLineOnOpening) {
if (*journalWasUpdated) {
appendToFile(fullPath, " \n",
@@ -712,11 +765,28 @@ backup_config_end:
}
openEditor(fullPath, editorToOpen, shouldRender, shouldJumpToEnd, shouldDebug);
free(fullPath);
}
free(returnNoteSelection);
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);
}
} else if (strcmp(noteSelected, "Create new note") == 0) {
note_creation:
noteSelected = createNewNote(notesDirectoryString, vaultSelected, bypassSelectionNote, bypassSelectionNoteValue, journalRegex, shouldDebug);
// we can just go back to open_note
goto open_note;
} else if (strcmp(noteSelected, "Search inside the vault") == 0) {
char *vaultPath = malloc(PATH_MAX);
snprintf(vaultPath, PATH_MAX, "%s/%s/", notesDirectoryString, vaultSelected);
char *result = fzfSelect(vaultPath, "Input text to be searched", shouldDebug);
bypassSelectionNote = 1;
bypassSelectionNoteValue = result;
free(vaultPath);
} else if (strcmp(noteSelected, "Back to vault selection") == 0) {
shouldChangeVault = 1;
} else if (strcmp(noteSelected, "Delete vault") == 0) {
@@ -725,7 +795,7 @@ backup_config_end:
const char *yesNo[] = {"No, go back to note selection.", "Yes."};
char *answer = ncursesSelect((char **)yesNo,
"Are you sure you want to delete the entire vault? This can "
"not be undone (Use arrows or WASD, Enter to select):",
"not be undone (Use arrows or WASD, Enter to select or use 1-9 to quick select):",
1, 1, " ", "", "", shouldDebug);
debug("You answered: %s for deleting the vault %s", answer, vaultSelected);
if (strcmp(answer, "Yes.") == 0) {
+7 -3
View File
@@ -188,7 +188,7 @@ char **getVaultsFromDirectories(char **directoryStringArray, int directoryNumber
return vaultsArray;
}
char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWasUpdated, int shouldDebug) {
char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWasUpdated, int *returnNoteSelection, int shouldDebug) {
path[PATH_MAX] = '\0'; // it assures it is a string (Most cases this does nothing). But rewriting at least
// one bytes make the compile happy. He doesn't want to return an unchanged input.
debug("Handling the journal %s", path);
@@ -221,13 +221,14 @@ char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWas
that the Create new entry in journal is on top entryArray = realloc(entryArray,
sizeof(char*));*/
// simpler to just malloc 2 and later realloc
const int extraOptions = 3;
const int extraOptions = 4;
char **entryArray = malloc(extraOptions * sizeof(char *));
char *createEntryMessage = malloc(PATH_MAX);
snprintf(createEntryMessage, PATH_MAX, "Create new entry for the journal %s", journal);
entryArray[0] = createEntryMessage;
entryArray[1] = "Open random entry";
entryArray[2] = "Search inside entries";
entryArray[3] = "Return to the note selection";
int entryCount = extraOptions; // we need to count how many dirs there is to always readjust
// how many memory we alloc
// Refer https://pubs.opengroup.org/onlinepubs/7990989775/xsh/readdir.html
@@ -259,7 +260,8 @@ char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWas
// element
// we must now select to create new entry or to enter in old one
char *selectedOption = ncursesSelect(entryArray, "Create new entry or acces old entry (Use arrows or WASD, Enter to select):", extraOptions, entryCount - extraOptions, " ", " ", "",
char *selectedOption =
ncursesSelect(entryArray, "Create new entry or acces old entry (Use arrows or WASD, Enter to select or use 1-9 to quick select):", extraOptions, entryCount - extraOptions, " ", " ", "",
shouldDebug); // the "Create new entry for the journal %s" will be the only options. All
// other will be extraOptions. This is made so that "Create [...] %s" will
// always be on top
@@ -301,6 +303,8 @@ char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWas
snprintf(selectedOption, PATH_MAX, "%s/%s", path, temp);
path = strdup(selectedOption);
free(selectedOption);
} else if (strcmp(selectedOption, "Return to the note selection") == 0) {
*returnNoteSelection = 1;
} else { // we just recreate the path to the selected entry
// snprintf does not like to have the same variable as input and output so we use a
// buffer
+2 -1
View File
@@ -25,6 +25,7 @@ char **getVaultsFromDirectories(char **directoryStringArray, int directoryNumber
// handles both type of journal (divided and unified).
// creates new entry with date.
// for divided select if we want to acces to a new entry or a old one.
// returnNoteSelection tells us if the user wants to go back to the note selection.
// returns the path to the file that needs to be opened.
char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWasUpdated, int shouldDebug);
char *updateJournal(char *path, char *journal, char *timeFormat, int *journalWasUpdated, int *returnNoteSelection, int shouldDebug);
#endif
+80 -52
View File
@@ -6,7 +6,8 @@ void createNewVault(char **directoriesArray, int directoryCount, char **vaultsAr
int emptyWarning = 0; // set to 1 later if inpted empty name
input_screen:
// choose in which directory the vault will be created
char *dirToVault = ncursesSelect(directoriesArray, "Select a directory, in which the vault will remain (Use arrows or WASD, Enter to select)", directoryCount, 0, "", "", " ", shouldDebug);
char *dirToVault =
ncursesSelect(directoriesArray, "Select a directory, in which the vault will remain (Use arrows or WASD, Enter to select or use 1-9 to quick select)", directoryCount, 0, "", "", " ", shouldDebug);
char *vaultName = malloc(PATH_MAX);
if (!bypass) { // if won't bypass (if -v or --vault weren't set)
echo();
@@ -179,13 +180,7 @@ char *fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
error(fd == -1, "program", "mkstemp failed");
close(fd);
/*
* STEP 1:
* Build index once into temp file
* format: file:line:content
*/
snprintf(command, sizeof(command), "rg --line-number --no-heading --color=never . \"%s\" > %s", pathToFiles, indexFile);
debug("INDEX CMD: %s", command);
if (system(command) != 0) {
@@ -193,45 +188,6 @@ char *fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
error(1, "program", "rg indexing failed");
}
/*
* STEP 2:
* Use fzf on the index file (NO rg anymore)
*/
/*
EXPLANATION:
cat %s
→ feeds prebuilt index file (format: file:line:content) into fzf
fzf --delimiter ':'
→ splits each line into fields:
{1} = file path
{2} = line number
{3} = content
--prompt
→ UI prompt text shown in fzf
--preview
→ runs a shell command for selected item:
file={1} → current file
line={2} → line number of match
nl -ba "$file"
→ prints file with line numbers
sed -n "$((line-5)),$((line+5))p"
→ extracts a window of 5 lines above and below match
sed "... -> ..."
→ highlights the matched line (middle of window) with red arrow
--preview-window=right:60%:hidden
→ preview appears on right, 60% width, initially hidden
--bind 'change:show-preview'
→ preview only appears after first selection change (not at startup)
*/
snprintf(command, sizeof(command),
"cat %s | fzf --delimiter ':' --prompt='%s' "
"--preview 'file={1}; line={2}; nl -ba \"$file\" | sed -n \"$((line-5)),$((line+5))p\" | "
@@ -254,16 +210,25 @@ char *fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
buffer[strcspn(buffer, "\n")] = '\0';
result = strdup(buffer);
}
debug("Buffer: %s", buffer);
debug("result: %s", result);
// cut at first ':'
result = strtok(result, ":");
// cut at / (because we need the full paths before and we only need to return the journal entry)
char *token = strtok(result, "/");
result = NULL;
while (token != NULL) {
result = token;
token = strtok(NULL, "/"); // Passing NULL means “don’t start a new string, resume
// we need to cut out the whole pathToFiles
int pathToFilesLen = strlen(pathToFiles);
int cutOut = 0;
for (int i = 0; i < pathToFilesLen; i++) {
if (pathToFiles[i] == result[i]) {
cutOut = i;
}
}
result += cutOut + 1; // the + 1 cuts the first /
// in case we select a journal, we won't open the entry just yet. We only need to return the journal name (everything before [now the first] /
result = strtok(result, "/");
debug("Cleaned note: %s", result);
// clean up
pclose(fzfPipe);
@@ -345,6 +310,69 @@ char *ncursesSelect(char **options, char *optionsText, int optionsNumber, int ex
highlight = 0;
}
break;
case '1':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 1.");
highlight = 0;
goto end_loop;
}
break;
case '2':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 2.");
highlight = 1;
goto end_loop;
}
break;
case '3':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 3.");
highlight = 2;
goto end_loop;
}
break;
case '4':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 4.");
highlight = 3;
goto end_loop;
}
break;
case '5':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 5.");
highlight = 4;
goto end_loop;
}
break;
case '6':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 6.");
highlight = 5;
goto end_loop;
}
break;
case '7':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 7.");
highlight = 6;
goto end_loop;
}
break;
case '8':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 8.");
highlight = 7;
goto end_loop;
}
break;
case '9':
if (optionsNumber + extraOptionsNumber >= 1) {
debug("Quickselected option 9.");
highlight = 8;
goto end_loop;
}
break;
case 10: //\n
goto end_loop;
}
+205 -1
View File
@@ -71,6 +71,26 @@ void _error(const int shouldDebug, const int condition, const char *type, const
exit(1);
}
}
void _warn(const int condition, const char *file, const int line, const char *function, const char *message, ...) { // use for warnings that should always be shown
if (condition) {
fflush(stdout);
fflush(stderr);
va_list args;
va_start(args, message);
int h, m, s;
getCurrentTime(&h, &m, &s);
fprintf(stdout, "\e[0;33m[WARNING -- %d:%d:%d] From file %s line %d function %s:\e[0m\n", h, m, s, file, line, function);
vfprintf(stdout, message, args);
fprintf(stdout, "\e[0m\n");
va_end(args);
}
}
static void copyDir(const char *source, const char *destination, const char **rsyncArgs, const int rsyncArgsNumber, const int shouldDebug) {
debug("Backuping... source: %s and destination: %s", source, destination);
@@ -113,6 +133,175 @@ static void ensureDir(const char *path, const int shouldDebug) {
}
}
static void slowGitWarning(struct timespec start, int *wasShown) {
if (!*wasShown) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
double elapsed = (double)(now.tv_sec - start.tv_sec) + (double)(now.tv_nsec - start.tv_nsec) / 1e9;
warn(elapsed > 0.3, "Git operations are taking too much time. You drive might be slow. Please do not stop the program.");
if (elapsed > 0.3) {
*wasShown = 1;
}
}
}
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) {
// on slow drive or WebDav mounted drive this git operations are pretty slow. We'll just show a warning to avoid the user closing the program.
struct timespec start;
clock_gettime(CLOCK_MONOTONIC, &start);
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;
int slowGitWarningAlreadyShown = 0;
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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
/* 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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
/* 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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
return_code = git_index_write(index);
error(return_code, "program", "Failed to write index\n%s", git_error_last()->message);
slowGitWarning(start, &slowGitWarningAlreadyShown);
/*
* 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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
return_code = git_commit_tree(&head_tree, head_commit);
error(return_code, "program", "Failed to get HEAD tree\n%s", git_error_last()->message);
slowGitWarning(start, &slowGitWarningAlreadyShown);
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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
return_code = git_tree_lookup(&tree, repo, &tree_id);
error(return_code, "program", "Failed to lookup tree\n%s", git_error_last()->message);
slowGitWarning(start, &slowGitWarningAlreadyShown);
/* Author/committer */
return_code = git_signature_now(&signature, signatureName, signatureEmail);
error(return_code, "program", "Failed to create signature\n%s", git_error_last()->message);
slowGitWarning(start, &slowGitWarningAlreadyShown);
/* 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);
slowGitWarning(start, &slowGitWarningAlreadyShown);
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];
@@ -159,13 +348,28 @@ void initAppFilesAndDirs(const char *home, const int shouldDebug) {
" \"~/Documents/\": \"path/to/backup/\"\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, 20, "%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;
+13
View File
@@ -6,6 +6,7 @@
#include <dirent.h>
#include <errno.h>
#include <ftw.h>
#include <git2.h>
#include <limits.h>
#include <ncurses.h>
#include <pwd.h>
@@ -17,6 +18,7 @@
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#ifndef VERSION
#define VERSION "dev"
#endif
@@ -29,6 +31,7 @@
#define debug(message, ...) _debug(shouldDebug, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__)
#define altDebug(message, ...) _altDebug(shouldDebug, message, ##__VA_ARGS__)
#define error(condition, type, message, ...) _error(shouldDebug, condition, type, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__)
#define warn(condition, message, ...) _warn(condition, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__)
// you must edit this two values if you want to add suport for an editor
extern const char *supportedEditor[]; // array of supported editors
extern const int numEditors; // number of supported editors
@@ -51,6 +54,8 @@ void _debug(const int d, const char *file, const int line, const char *function,
void _altDebug(const int d, const char *message, ...);
// formated error.
void _error(const int shouldDebug, const int condition, const char *type, const char *file, const int line, const char *function, const char *message, ...);
// formated warn
void _warn(const int condition, const char *file, const int line, const char *function, const char *message, ...);
// Returns 1 if the string is in the array.
// Returns 0 if the string is not in the array.
// If you want to only check the first n elements of the array, pass n as len.
@@ -85,4 +90,12 @@ will be added in the function.
backupMessage will be set to 1 if a backup was launched.*/
void handleBackups(char **sourceDirectoryArray, const int sourceDirectoryNumber, char **destinationDirectoryArray, const char *homeDir, const int interval, const char **rsyncArgs, const int rsyncArgsNumber,
const int forceBackup, int *backupMessage, const int shouldDebug);
/*Equivalent of git init.
If the directory wasn't git initialized. We do it.*/
void ensureGitDirectory(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const int shouldDebug);
/*Configures git, stages everything, commits everything*/
void gitBackupUpdate(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const char *commitMsg, const int shouldDebug);
// yyyy-mm-dd hh:mm:ss
// Don't forget to free the pointer.
char *getDateAndTime();
#endif