From cdf2e086e041ce237bc41d690018807ee2943bf5 Mon Sep 17 00:00:00 2001 From: Tomas Rivera <137088692+Totorile1@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:39:44 +0200 Subject: [PATCH] Added fzf search for journals --- README.md | 4 +- default.nix | 3 ++ src/fzf.sh | 12 ++++++ src/main.c | 6 ++- src/notes.c | 33 +++++++++------- src/ui.c | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.h | 5 +++ src/utils.h | 1 - 8 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 src/fzf.sh diff --git a/README.md b/README.md index 9ad2239..da85175 100644 --- a/README.md +++ b/README.md @@ -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. ### How to install -1. Have installed `ncurses`, `cjson`, `make`, `Vivify`, `rsync` and `pkg-config`. +1. Have installed `ncurses`, `cjson`, `make`, `Vivify`, `rsync`, `pkg-config`, `sed`, `ripgrep` and `fzf`. 2. Have a supported editor: `vim`, `neovim` and `nano` (for the moment) 3. Clone the repository and enter it. ```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] Port vivify.vim to nixpkgs - [ ] 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] Fix crash when the window is resized - [x] Actually open vivify when opening nvim diff --git a/default.nix b/default.nix index ac09997..17c2425 100644 --- a/default.nix +++ b/default.nix @@ -13,6 +13,9 @@ pkgs.stdenv.mkDerivation { buildInputs = [ pkgs.ncurses pkgs.cjson + pkgs.fzf + pkgs.gnused + pkgs.ripgrep ]; buildPhase = '' diff --git a/src/fzf.sh b/src/fzf.sh new file mode 100644 index 0000000..9ba86ef --- /dev/null +++ b/src/fzf.sh @@ -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 +}' diff --git a/src/main.c b/src/main.c index 19c1e80..d615ccb 100644 --- a/src/main.c +++ b/src/main.c @@ -334,10 +334,9 @@ note_selection: // this loop is the note selector int filesCount = 0; char **filesArray = getNotesFromVault(notesDirectoryString, vaultSelected, journalRegex, &filesCount, shouldDebug); - qsort(filesArray, filesCount, sizeof(const char *), compareString); // sorts the notes alphabetically + int journalCount = 0; char **journalArray = getJournalsFromVault(notesDirectoryString, vaultSelected, journalRegex, &journalCount, shouldDebug); - qsort(journalArray, journalCount, sizeof(const char *), compareString); // appends the journal at the end of filesArray filesArray = realloc(filesArray, (filesCount + journalCount)*sizeof(char*)); @@ -345,6 +344,9 @@ note_selection: filesArray[i + filesCount] = journalArray[i]; } 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:"); if (shouldDebug) { for (int i = 0; i < filesCount; i++) { diff --git a/src/notes.c b/src/notes.c index f49580c..990b0b1 100644 --- a/src/notes.c +++ b/src/notes.c @@ -1,6 +1,7 @@ #include "notes.h" +#include "ui.h" +#include -// (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) { debug("Searching %s for journals", vault); // 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) { // 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/ 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 @@ -62,29 +62,30 @@ char** getNotesFromVault(char *pathToVault, char *vault, char *journalRegex, int // for readdir() debug("┌------------------------------\nDetected Files and dirs from the vault:"); while ((vaultEntry = readdir(vaultDirectory)) != NULL) { - - altDebug("%s ", vaultEntry->d_name); + char *entryName = vaultEntry->d_name; + int entryLenght = strlen(entryName); + altDebug("%s ", entryName); // for the regex code https://stackoverflow.com/a/1085120 regex_t regex; int regexReturn; // compiles the regex regexReturn = regcomp(®ex, 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 - 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. - regexReturn = regexec(®ex, vaultEntry->d_name, 0, NULL, 0); + regexReturn = regexec(®ex, entryName, 0, NULL, 0); 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[notesCount] = strdup(vaultEntry->d_name); // copy the dir name into notesArray + notesArray[notesCount] = strdup(entryName); // copy the dir name into notesArray notesCount++; 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 {altDebug("was ignored\n");} } altDebug("└ ------------------------------\n"); - // (TODO LATER) Alphabetically sort them + // free's some used memory closedir(vaultDirectory); *count = notesCount; // passes the number of files @@ -120,7 +121,6 @@ char **getVaultsFromDirectory(char *dirString, int *count, int shouldDebug) { } } altDebug("└------------------------------\n"); - // (TODO LATER) Alphabetically sort them // free's some used memory 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 entryArray = realloc(entryArray, sizeof(char*));*/ // simpler to just malloc 2 and later realloc - const int extraOptions = 2; + const int extraOptions = 3; 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"; 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 // for readdir() debug("┌------------------------------\n Detected files and dirs from %s:", path); // iterates over all the entries from the dir while ((dividedJournalEntry = readdir(dividedJournalDirectory)) != NULL) { - // (TODO LATER) find a way to sort them 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) 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 strncpy(path, temp, PATH_MAX); 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 // snprintf does not like to have the same variable as input and output so we use a buffer char temp[PATH_MAX]; diff --git a/src/ui.c b/src/ui.c index 5e0b1ff..b3a2a18 100644 --- a/src/ui.c +++ b/src/ui.c @@ -145,6 +145,113 @@ char *createNewNote(char dirToVault[PATH_MAX], char *vaultFromDir, int bypass, c 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 “don’t 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) { int highlight = 0; //curently highlighted option int key; diff --git a/src/ui.h b/src/ui.h index f61e169..4ffccb6 100644 --- a/src/ui.h +++ b/src/ui.h @@ -12,6 +12,9 @@ #include #include #include "utils.h" +#define CMD_BUFFER 4096 // for fzfSelect +#define RESULT_BUFFER 1024 // for fzfSelect + /*Uses ncurses to get an input from the user. Creates a new note with this input. 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. returns nothing. */ 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. 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 ..."). diff --git a/src/utils.h b/src/utils.h index 9fc070a..a2d05cc 100644 --- a/src/utils.h +++ b/src/utils.h @@ -33,7 +33,6 @@ extern const char *supportedEditor[]; // array of supported editors extern const int numEditors; // number of supported editors -//(TODO LATER) organise this mess //compares two strings alphabetically. //this function is used for qsort