Added fzf search for journals

This commit is contained in:
Tomas Rivera
2026-04-16 18:39:44 +02:00
parent 17fb5edd48
commit cdf2e086e0
8 changed files with 152 additions and 19 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ 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. 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 ### How to install
1. Have installed `ncurses`, `cjson`, `make`, `Vivify`, `rsync` and `pkg-config`. <TODO LATER we should put hyprlinks to each program.> 1. Have installed `ncurses`, `cjson`, `make`, `Vivify`, `rsync`, `pkg-config`, `sed`, `ripgrep` and `fzf`. <TODO LATER we should put hyprlinks to each program.>
2. Have a supported editor: `vim`, `neovim` and `nano` (for the moment) 2. Have a supported editor: `vim`, `neovim` and `nano` (for the moment)
3. Clone the repository and enter it. 3. Clone the repository and enter it.
```shell ```shell
@@ -102,7 +102,7 @@ Change `~/.config/notewrapper/config.json`. If it does not exist, the program sh
- [x] Search for config.json in other directories such as ~/.config/notewrapper/ and not only this directory - [x] Search for config.json in other directories such as ~/.config/notewrapper/ and not only this directory
- [x] Port vivify.vim to nixpkgs - [x] Port vivify.vim to nixpkgs
- [ ] Add a way to have vaults in different directories - [ ] Add a way to have vaults in different directories
- [ ] some kind of FZF search for notes - [x] some kind of FZF search for notes
- [x] A button to randomly select a note or an entry in a journal - [x] A button to randomly select a note or an entry in a journal
- [x] Fix crash when the window is resized - [x] Fix crash when the window is resized
- [x] Actually open vivify when opening nvim - [x] Actually open vivify when opening nvim
+3
View File
@@ -13,6 +13,9 @@ pkgs.stdenv.mkDerivation {
buildInputs = [ buildInputs = [
pkgs.ncurses pkgs.ncurses
pkgs.cjson pkgs.cjson
pkgs.fzf
pkgs.gnused
pkgs.ripgrep
]; ];
buildPhase = '' buildPhase = ''
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
file="$1"
line="$2"
start=$((line-5))
end=$((line+5))
nl -ba "$file" | awk -v s="$start" -v e="$end" -v l="$line" '
NR>=s && NR<=e {
if (NR==l) print ">>> " $0;
else print " " $0
}'
+4 -2
View File
@@ -334,10 +334,9 @@ note_selection:
// this loop is the note selector // this loop is the note selector
int filesCount = 0; int filesCount = 0;
char **filesArray = getNotesFromVault(notesDirectoryString, vaultSelected, journalRegex, &filesCount, shouldDebug); char **filesArray = getNotesFromVault(notesDirectoryString, vaultSelected, journalRegex, &filesCount, shouldDebug);
qsort(filesArray, filesCount, sizeof(const char *), compareString); // sorts the notes alphabetically
int journalCount = 0; int journalCount = 0;
char **journalArray = getJournalsFromVault(notesDirectoryString, vaultSelected, journalRegex, &journalCount, shouldDebug); char **journalArray = getJournalsFromVault(notesDirectoryString, vaultSelected, journalRegex, &journalCount, shouldDebug);
qsort(journalArray, journalCount, sizeof(const char *), compareString);
// appends the journal at the end of filesArray // appends the journal at the end of filesArray
filesArray = realloc(filesArray, (filesCount + journalCount)*sizeof(char*)); filesArray = realloc(filesArray, (filesCount + journalCount)*sizeof(char*));
@@ -345,6 +344,9 @@ note_selection:
filesArray[i + filesCount] = journalArray[i]; filesArray[i + filesCount] = journalArray[i];
} }
filesCount = filesCount + journalCount; filesCount = filesCount + journalCount;
// we sort them // (TODO LATER) THis doesn't seem to sort them
qsort(filesArray, filesCount, sizeof(const char *), compareString);
debug("Available notes and journals:"); debug("Available notes and journals:");
if (shouldDebug) { if (shouldDebug) {
for (int i = 0; i < filesCount; i++) { for (int i = 0; i < filesCount; i++) {
+19 -14
View File
@@ -1,6 +1,7 @@
#include "notes.h" #include "notes.h"
#include "ui.h"
#include <stdio.h>
// (TODO LATER) Check if it isn't a know media file like .jpg, etc. more generally only text, code or markdown file should be accepted. maybe se if it is in utf8?
char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex, int *count, int shouldDebug) { char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex, int *count, int shouldDebug) {
debug("Searching %s for journals", vault); debug("Searching %s for journals", vault);
// originally from https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/ // originally from https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/
@@ -48,7 +49,6 @@ char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex,
char** getNotesFromVault(char *pathToVault, char *vault, char *journalRegex, int *count, int shouldDebug) { char** getNotesFromVault(char *pathToVault, char *vault, char *journalRegex, int *count, int shouldDebug) {
// this function is inputed a path to a vault (which was selected before) and outpus all the suitable notes (so not the hidden ones) // this function is inputed a path to a vault (which was selected before) and outpus all the suitable notes (so not the hidden ones)
// (TODO LATER) Check how it handles non .md files
// originally from https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/ // originally from https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/
debug("Searching %s for notes", vault); debug("Searching %s for notes", vault);
struct dirent *vaultEntry; // (TODO LATER) change name of these variables. notesDirectory is dumb as it is the directory of vaults struct dirent *vaultEntry; // (TODO LATER) change name of these variables. notesDirectory is dumb as it is the directory of vaults
@@ -62,29 +62,30 @@ char** getNotesFromVault(char *pathToVault, char *vault, char *journalRegex, int
// for readdir() // for readdir()
debug("┌------------------------------\nDetected Files and dirs from the vault:"); debug("┌------------------------------\nDetected Files and dirs from the vault:");
while ((vaultEntry = readdir(vaultDirectory)) != NULL) { while ((vaultEntry = readdir(vaultDirectory)) != NULL) {
char *entryName = vaultEntry->d_name;
altDebug("%s ", vaultEntry->d_name); int entryLenght = strlen(entryName);
altDebug("%s ", entryName);
// for the regex code https://stackoverflow.com/a/1085120 // for the regex code https://stackoverflow.com/a/1085120
regex_t regex; regex_t regex;
int regexReturn; int regexReturn;
// compiles the regex // compiles the regex
regexReturn = regcomp(&regex, journalRegex, 0); regexReturn = regcomp(&regex, journalRegex, 0);
if (vaultEntry->d_name[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files) if (entryName[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files)
char fullPathEntry[PATH_MAX]; // creates a string of size of the maximum path lenght char fullPathEntry[PATH_MAX]; // creates a string of size of the maximum path lenght
snprintf(fullPathEntry, sizeof(fullPathEntry), "%s/%s/%s", pathToVault, vault, vaultEntry->d_name); // sets the full absolute path to fullPathEntry snprintf(fullPathEntry, sizeof(fullPathEntry), "%s/%s/%s", pathToVault, vault, entryName); // sets the full absolute path to fullPathEntry
// check if it matches the regex. If it does not match regexReturn != 0. // check if it matches the regex. If it does not match regexReturn != 0.
regexReturn = regexec(&regex, vaultEntry->d_name, 0, NULL, 0); regexReturn = regexec(&regex, entryName, 0, NULL, 0);
struct stat metadataPathEntry; struct stat metadataPathEntry;
if (stat(fullPathEntry, &metadataPathEntry) == 0 && regexReturn && S_ISREG(metadataPathEntry.st_mode)) { // if this entry is a file if (stat(fullPathEntry, &metadataPathEntry) == 0 && entryName[entryLenght - 3] == '.' && entryName[entryLenght - 2] == 'm' && entryName[entryLenght - 1] == 'd' && regexReturn && S_ISREG(metadataPathEntry.st_mode)) { // if this entry is a file ending in .md and that does no match the regex
notesArray = realloc(notesArray, (notesCount + 1)*sizeof(char*)); // resize notesArray so that notesArray = realloc(notesArray, (notesCount + 1)*sizeof(char*)); // resize notesArray so that
notesArray[notesCount] = strdup(vaultEntry->d_name); // copy the dir name into notesArray notesArray[notesCount] = strdup(entryName); // copy the dir name into notesArray
notesCount++; notesCount++;
altDebug("did not match with the regex. It is a note.\n"); altDebug("did not match with the regex. It is a note.\n");
} else if (!regexReturn) {altDebug("matched with the regex. It is a journal.\n");} } else if (!regexReturn) {altDebug("matched with the regex. It is a journal.\n");}
} else {altDebug("was ignored\n");} } else {altDebug("was ignored\n");}
} }
altDebug("└ ------------------------------\n"); altDebug("└ ------------------------------\n");
// (TODO LATER) Alphabetically sort them
// free's some used memory // free's some used memory
closedir(vaultDirectory); closedir(vaultDirectory);
*count = notesCount; // passes the number of files *count = notesCount; // passes the number of files
@@ -120,7 +121,6 @@ char **getVaultsFromDirectory(char *dirString, int *count, int shouldDebug) {
} }
} }
altDebug("└------------------------------\n"); altDebug("└------------------------------\n");
// (TODO LATER) Alphabetically sort them
// free's some used memory // free's some used memory
closedir(vaultsDirectory); closedir(vaultsDirectory);
@@ -160,19 +160,19 @@ char *updateJournal(char *path, char *journal, char *timeFormat, int shouldDebug
// This time it will be different. We must add "invert" the extraOptions to the options so that the Create new entry in journal is on top // This time it will be different. We must add "invert" the extraOptions to the options so that the Create new entry in journal is on top
entryArray = realloc(entryArray, sizeof(char*));*/ entryArray = realloc(entryArray, sizeof(char*));*/
// simpler to just malloc 2 and later realloc // simpler to just malloc 2 and later realloc
const int extraOptions = 2; const int extraOptions = 3;
char **entryArray = malloc(extraOptions*sizeof(char*)); char **entryArray = malloc(extraOptions*sizeof(char*));
char *createEntryMessage = malloc(PATH_MAX); char *createEntryMessage = malloc(PATH_MAX);
snprintf(createEntryMessage, PATH_MAX, "Create new entry for the journal %s", journal); snprintf(createEntryMessage, PATH_MAX, "Create new entry for the journal %s", journal);
entryArray[0] = createEntryMessage; entryArray[0] = createEntryMessage;
entryArray[1] = "Open random entry"; entryArray[1] = "Open random entry";
entryArray[2] = "Search inside entries";
int entryCount = extraOptions; // we need to count how many dirs there is to always readjust how many memory we alloc 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 // Refer https://pubs.opengroup.org/onlinepubs/7990989775/xsh/readdir.html
// for readdir() // for readdir()
debug("┌------------------------------\n Detected files and dirs from %s:", path); debug("┌------------------------------\n Detected files and dirs from %s:", path);
// iterates over all the entries from the dir // iterates over all the entries from the dir
while ((dividedJournalEntry = readdir(dividedJournalDirectory)) != NULL) { while ((dividedJournalEntry = readdir(dividedJournalDirectory)) != NULL) {
// (TODO LATER) find a way to sort them
altDebug("%s\n", dividedJournalEntry->d_name); altDebug("%s\n", dividedJournalEntry->d_name);
if (dividedJournalEntry->d_name[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files) if (dividedJournalEntry->d_name[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files)
char fullPathEntry[PATH_MAX]; // creates a string of size of the maximum path lenght char fullPathEntry[PATH_MAX]; // creates a string of size of the maximum path lenght
@@ -220,7 +220,12 @@ char *updateJournal(char *path, char *journal, char *timeFormat, int shouldDebug
snprintf(temp, PATH_MAX, "%s/%s", path, entryArray[randomEntry]); // reconstruct the path snprintf(temp, PATH_MAX, "%s/%s", path, entryArray[randomEntry]); // reconstruct the path
strncpy(path, temp, PATH_MAX); strncpy(path, temp, PATH_MAX);
debug("Selected random entry %s. It's path is %s.", entryArray[randomEntry], path); debug("Selected random entry %s. It's path is %s.", entryArray[randomEntry], path);
// } else if (strcmp(selectedOption, entryArray[2]) == 0) { // if we choosed to search
char *temp = fzfSelect(path, "Input text to be searched", shouldDebug); // uses ripgrep and fzf to search inside the files
char *selectedOption = malloc(PATH_MAX); // we can't just use path as both input and output of snprintf
snprintf(selectedOption, PATH_MAX, "%s/%s", path, temp);
path = strdup(selectedOption);
free(selectedOption);
} else { // we just recreate the path to the selected entry } 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 // snprintf does not like to have the same variable as input and output so we use a buffer
char temp[PATH_MAX]; char temp[PATH_MAX];
+107
View File
@@ -145,6 +145,113 @@ char *createNewNote(char dirToVault[PATH_MAX], char *vaultFromDir, int bypass, c
return fileName; return fileName;
} }
char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
// we are gonna write each line of each files (with the filename and the line number to an index)
char command[CMD_BUFFER];
char indexFile[] = "/tmp/notewrapper_index_XXXXXX";
int fd = mkstemp(indexFile);
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) {
unlink(indexFile);
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\" | sed \"$((line-$(($((line-5))))+1))s/^/\\x1b[31m-> \\x1b[0m/\"' "
"--preview-window=right:60%%:hidden "
"--bind 'change:show-preview'",
indexFile,
selectText ? selectText : "> "
);
debug("FZF CMD: %s", command);
FILE *fzfPipe = popen(command, "r");
if (!fzfPipe) {
unlink(indexFile);
error(1, "program", "popen failed");
}
char buffer[RESULT_BUFFER];
char *result = NULL;
if (fgets(buffer, sizeof(buffer), fzfPipe)) {
buffer[strcspn(buffer, "\n")] = '\0';
result = strdup(buffer);
}
// 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 “dont start a new string, resume
}
// clean up
pclose(fzfPipe);
unlink(indexFile);
return result;
}
char* ncursesSelect(char **options, char *optionsText, int optionsNumber, int extraOptionsNumber, char *bottomText, char *middleText, char *topText, int shouldDebug) { char* ncursesSelect(char **options, char *optionsText, int optionsNumber, int extraOptionsNumber, char *bottomText, char *middleText, char *topText, int shouldDebug) {
int highlight = 0; //curently highlighted option int highlight = 0; //curently highlighted option
int key; int key;
+5
View File
@@ -12,6 +12,9 @@
#include <limits.h> #include <limits.h>
#include <regex.h> #include <regex.h>
#include "utils.h" #include "utils.h"
#define CMD_BUFFER 4096 // for fzfSelect
#define RESULT_BUFFER 1024 // for fzfSelect
/*Uses ncurses to get an input from the user. /*Uses ncurses to get an input from the user.
Creates a new note with this input. Creates a new note with this input.
also can create a journal if the name matches with journalRegex. also can create a journal if the name matches with journalRegex.
@@ -24,6 +27,8 @@ Creates a new vault with this input.
If vault already exists, prints a warning. If vault already exists, prints a warning.
returns nothing. */ returns nothing. */
void createNewVault(char *dirToVault, int bypass, char *bypassvalue, int debug); void createNewVault(char *dirToVault, int bypass, char *bypassvalue, int debug);
// uses fzf and ripgrep to search inside the files to choose between all the options.
char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug);
/*this function is used multiple times let the user select one options from many with ncurses in a TUI. /*this function is used multiple times let the user select one options from many with ncurses in a TUI.
options is the array of strings with all the options. options is the array of strings with all the options.
optionsText is the text that will be printed at the top (For example: "Please select ..."). optionsText is the text that will be printed at the top (For example: "Please select ...").
-1
View File
@@ -33,7 +33,6 @@
extern const char *supportedEditor[]; // array of supported editors extern const char *supportedEditor[]; // array of supported editors
extern const int numEditors; // number of supported editors extern const int numEditors; // number of supported editors
//(TODO LATER) organise this mess
//compares two strings alphabetically. //compares two strings alphabetically.
//this function is used for qsort //this function is used for qsort