mirror of
https://github.com/tomasriveral/NoteWrapper.git
synced 2026-08-12 10:20:46 +02:00
Added fzf search for journals
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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(®ex, journalRegex, 0);
|
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
|
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(®ex, vaultEntry->d_name, 0, NULL, 0);
|
regexReturn = regexec(®ex, 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];
|
||||||
|
|||||||
@@ -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 “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) {
|
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;
|
||||||
|
|||||||
@@ -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 ...").
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user