Compare commits

...
Author SHA1 Message Date
github-actions[bot] 682d794443 chore: weekly flake update 2026-07-20 06:23:38 +00:00
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 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
tomasrandGitHub 2b2a3455d2 Merge (#43) from tomasriveral/fix-flake-src
fix: flake src
2026-06-01 11:41:29 +02:00
tomasr d8a11bbc16 fix: flake src 2026-06-01 11:37:47 +02:00
tomasrandGitHub 1d95983d5d Merge (#42) from tomasriveral/flake-update/2026-06-01
[Automated] Weekly flake update 2026-06-01
2026-06-01 11:10:45 +02:00
github-actions[bot] 30449ca473 chore: weekly flake update 2026-06-01 08:42:14 +00:00
tomasrandGitHub 7af79c76b1 Merge pull request #41 from tomasriveral/release/v2.2
[Automatic] Release PR v2.2
2026-05-27 17:42:59 +02:00
github-actions[bot] 27b8b310cb chore(release): v2.2 2026-05-27 15:39:57 +00:00
tomasrandGitHub 4d228fb911 Merge (#40) from tomasriveral/fix-release-workflow-electric-bogaloo
CI: fix release workflow uncorrect branch
2026-05-27 17:38:57 +02:00
tomasr 34a2546990 CI: fix release workflow uncorrect branch 2026-05-27 17:35:42 +02:00
tomasrandGitHub 7231741f2d Merge (#39) from tomasriveral/feat-backup-now
feat: add backup now button
2026-05-27 17:11:43 +02:00
tomasr e86d7e7e03 feat: add backup now button 2026-05-27 17:09:14 +02:00
tomasrandGitHub 280c329339 Merge (#38) from tomasriveral/release/vv2.1.1
[Automatic] Release PR v2.1.1
2026-05-26 11:56:22 +02:00
tomasrandGitHub f7c84136a5 Merge (#37) from tomasriveral/fix-release-workflow
fix: release workflow correct branch
2026-05-26 11:53:33 +02:00
tomasr f66371137a fix: release workflow correct branch 2026-05-26 11:51:08 +02:00
github-actions[bot] 77b5148489 chore(release): v2.1.1 2026-05-26 09:45:50 +00:00
tomasrandGitHub d537357802 Merge (#36) from tomasriveral/fix-33-flake-version
fix(#33): CI update flake version flag
2026-05-26 11:44:55 +02:00
tomasr 503dddd9be fix(#33): CI update flake version flag 2026-05-26 11:43:14 +02:00
tomasrandGitHub fd77eaa4de Merge (#35) from tomasriveral/revert-34-fix-33-flake-version
Revert "fix(#33): flake notewrapper version flag"
2026-05-26 11:37:04 +02:00
tomasrandGitHub d6fa89785f Revert "fix(#33): flake notewrapper version flag" 2026-05-26 09:36:39 +00:00
tomasrandGitHub 30616b1b6f Merge (#34) from tomasriveral/fix-33-flake-version
fix(#33): flake notewrapper version flag
2026-05-26 11:22:05 +02:00
tomasr 004b536e05 fix(#33): flake notewrapper version flag 2026-05-26 11:19:03 +02:00
tomasrandGitHub c9ee6ca032 Merge (#32) from tomasriveral/readme-flake
docs: add flake instruction in readme
2026-05-25 17:43:25 +02:00
tomasr c8b5538e1c docs: add flake instruction in readme 2026-05-25 17:42:42 +02:00
tomasrandGitHub 2a09d4ee46 Merge pull request (#31)
flake: fix fetching
2026-05-25 17:29:59 +02:00
tomasr 52169ccbe4 CI: drop release workflow 2026-05-25 17:26:59 +02:00
tomasr 31366aeb48 flake: fix fetching 2026-05-25 17:26:21 +02:00
tomasrandGitHub e370f98c5e Merge (#30) from tomasriveral/workflows-fix
CI: release workflow fix bash
2026-05-25 17:02:22 +02:00
tomasr 5c4bd6ef16 CI: release workflow fix bash 2026-05-25 17:01:51 +02:00
tomasrandGitHub 6e6ddebb5e Merge (#29) from tomasriveral/workflows
CI: add weeklyUpdate workflow
2026-05-25 16:56:13 +02:00
tomasr fcb853e36a CI: add weeklyUpdate workflow
Assissted-by: ChatGPT 5.5
2026-05-25 16:52:15 +02:00
tomasrandGitHub 91d6db6c74 Merge (#28) from tomasriveral/flake.nix
flake: init
2026-05-25 16:46:57 +02:00
tomasr 833f8c1bd2 CI: add release workflow 2026-05-25 16:44:14 +02:00
tomasr 5328fdc91c flake: init 2026-05-25 16:20:01 +02:00
tomasrandGitHub f85a32f97f Merge pull request #27 from tomasriveral/tomasriveral-patch-1
docs: fix typo in readme
2026-05-23 18:26:26 +02:00
tomasrandGitHub bc306ca630 docs: fix typo in readme 2026-05-23 16:26:07 +00:00
tomasrandGitHub 89c686652e Merge pull request #26 from tomasriveral/readme-message
docs: add message in readme
2026-05-23 18:25:25 +02:00
tomasr d1c19696ff docs: add message in readme 2026-05-23 18:23:28 +02:00
tomasrandGitHub 8ee0096540 Merge (#25) from tomasriveral/fix-contributing
docs: fix forgot to update function name
2026-05-02 08:50:37 +02:00
tomasr 3fa0dbe0dd docs: fix forgot to update function name 2026-05-02 08:49:48 +02:00
tomasrandGitHub 2d6787a91b Merge (#24) from tomasriveral/fixJournalCreation fix (#21)
journal: fix (#21)
2026-05-02 08:21:51 +02:00
tomasr 2f688df3ec journal: fix crash when creating unified journal without ".md" 2026-05-02 08:18:46 +02:00
tomasrandGitHub 1f20eb6996 Merge (#23) from tomasriveral/backupDebug fix (#22)
fix (#22): backup
2026-05-02 07:58:03 +02:00
tomasr ebd1ea70e1 backup: fixed debugging 2026-05-02 07:54:43 +02:00
tomasrandGitHub ffcc16c14f Merge (#20) from tomasriveral/format
format: fix line width
2026-04-27 20:09:10 +02:00
tomasr 9aa8b075e9 format: formated whole project 2026-04-27 20:06:47 +02:00
tomasr 9a865897ed format: change line width from 100 to 225 2026-04-27 20:05:36 +02:00
tomasrandGitHub 5cdf27f7ed Merge (#19) from tomasriveral/clangd-test CI: add clang-format check
add documentation about this test
add .clang-format file
add build flag -Werror
add clang-tools to shell.nix
format whole project
2026-04-27 19:33:26 +02:00
tomasrandGitHub e98985fb5b Merge (#18) from tomasriveral/readme-configuration
fix: default configuration 
replace ~/Documents/Notes to ~/Documents which should exist in most systems
2026-04-27 19:29:36 +02:00
tomasr dff2fb25aa docs: add in CONTRIBUTING.md info about CI 2026-04-27 19:27:26 +02:00
tomasr 56498b2732 Env: add clang-tools to ./shell.nix 2026-04-27 19:26:42 +02:00
tomasr 73f892aff0 Build: add -Werror to makefile 2026-04-27 19:26:03 +02:00
tomasr 533afd0a4b CI: add clang-format check 2026-04-27 19:20:03 +02:00
tomasr 986618a05b format: fix whole project 2026-04-27 19:15:50 +02:00
tomasr 7bd39a08c6 format: add .clangd-format 2026-04-27 19:15:17 +02:00
tomasr 37a0763e14 config: change default directory 2026-04-27 19:05:54 +02:00
tomasr 0bf3d4624e docs: add info about default config 2026-04-27 19:02:25 +02:00
tomasrandGitHub 36189b4a9d Merge (#17) from tomasriveral/github-testing
Fix: all the remaining issue with CI
2026-04-27 18:57:52 +02:00
tomasr feadf62632 CI: fix no ~/Documents/Notes 2026-04-27 18:55:03 +02:00
tomasr a839b0da5b CI: fix terminal: unknown 2026-04-27 18:52:01 +02:00
tomasr 027038e49e Fix: removed headers clangd added 2026-04-27 18:47:45 +02:00
tomasr 0b2c499bd1 CI: remove on push testing 2026-04-27 18:34:36 +02:00
tomasr 8720a17286 CI: fix immediate crash 2026-04-27 18:33:08 +02:00
tomasrandGitHub c46ff838c1 Merge (#16). Correction in github actions path
CI: change ./workflow to ./workflows
2026-04-27 17:58:15 +02:00
tomasr 66ca150190 CI: change ./workflow to ./workflows 2026-04-27 17:56:54 +02:00
tomasrandGitHub acd64a845b Merge (#14) from tomasriveral/github-testing. Test: add github actions
Added make test run the program for five seconds and sees if it crashes
Added github action to build and test
2026-04-27 17:45:58 +02:00
tomasr 2a5b08b737 Test: add github actions 2026-04-27 17:43:45 +02:00
tomasrandGitHub a142f92c79 Merge (#13) from tomasriveral/readme-ameliorations
docs: miscellanious improvements in README.md
2026-04-27 17:13:36 +02:00
tomasr 4422a8d6fd docs: miscellanious improvements in README.md 2026-04-27 17:12:34 +02:00
tomasrandGitHub fc9480e12e Merge (#12) from tomasriveral/jed. Jed: add initial support
Jed: add initial support
2026-04-27 17:05:28 +02:00
tomasr d1561ddc2e editors: correct debugging info 2026-04-27 17:04:15 +02:00
tomasr a33a94637f Jed: add initial support 2026-04-27 17:01:57 +02:00
tomasr 4d7c44c016 Jed: add initial documentation 2026-04-27 16:58:34 +02:00
tomasrandGitHub 433aca0a58 Merge (#11) from tomasriveral/multiple-vault-directories backup: fixed unconsistent expansion of \~
backup: fixed unconsistent expansion of \~
introduced in last PR
2026-04-27 16:49:41 +02:00
tomasr f8bc4004ed backup: fixed unconsistent expansion of \~ 2026-04-27 16:43:59 +02:00
tomasr bf32877f96 docs: add warnings about some config option 2026-04-27 16:17:07 +02:00
tomasrandGitHub 99e0f9c130 Merge (#10) from tomasriveral/multiple-vault-directories
things i forgot to fix or that i broke last time.
2026-04-26 22:10:08 +02:00
tomasr 92caf3ffd7 docs: fix default config information about backup's directory 2026-04-26 22:08:51 +02:00
tomasr a3ece38fc1 Fix: bug cjson parsing logic triggered an error 2026-04-26 22:06:50 +02:00
tomasrandGitHub aa4fa234ff Merge (#9) from tomasriveral/multiple-vault-directories add multiple vault directory
add the code for multiple vault directory.
each directory can be backed up to a different dir.
added documentation about it.
added documentation about changing default config
fixed multiple bugs i introduced myself.
2026-04-26 21:41:46 +02:00
tomasr d5b5d07e64 fix: multiple small bugs from previous commit 2026-04-26 21:37:20 +02:00
tomasr 3fb2a32b83 Fix: prevent cases where // in paths could broke stat() 2026-04-26 21:14:54 +02:00
tomasr 5c87ac0aa2 fix: improved memory safety in getVaultsFromDirectories() 2026-04-26 21:14:17 +02:00
tomasr 3233b330db fix: bug wrong realloc size 2026-04-26 21:04:02 +02:00
tomasr b7ca3eca14 fix: bug available vaults written to wrong array 2026-04-26 21:02:54 +02:00
tomasr 68991673c0 fix: expands \~ to the home directory inside "directory" in the config file 2026-04-26 21:00:20 +02:00
tomasr 0d2ec12263 config: add correct "directory" field to default configuration 2026-04-26 20:50:02 +02:00
tomasr 107519507f feat: allow the usage of multiple vault directories 2026-04-26 20:45:42 +02:00
tomasr e2343dd401 docs: reworked example in commit convention 2026-04-26 13:11:30 +02:00
tomasr 08fffcb3d1 docs: add instruction changing default configuration 2026-04-26 13:10:43 +02:00
tomasr 8cae6cf8ff docs: add multiple vault's directories 2026-04-26 13:02:37 +02:00
tomasrandGitHub 495e8015bc Merge (#8) from tomasriveral/default-to-$editor Add default to $EDITOR
editor: add default to $editor
2026-04-26 10:43:57 +02:00
tomasr d208ccaa20 editors: add default to \$EDITOR 2026-04-26 10:39:14 +02:00
tomasr ce4b5cd719 docs: add default to \$EDITOR 2026-04-26 10:39:00 +02:00
tomasrandGitHub f6625e0ef1 Merge (#7) from tomasriveral/versionFlagIsBroken Fix: version flag
Fix: version flag is now set with compiler flag
2026-04-23 20:08:32 +02:00
tomasr ae168f92ce Fix: version flag is now set with compiler flag 2026-04-23 20:06:03 +02:00
tomasrandGitHub 0eac451751 Merge (#6) from tomasriveral/vi. Adds vi support
Vi: added initial support
2026-04-22 19:18:16 +02:00
tomasr 9d7e2efe17 Vi: added initial support 2026-04-22 19:15:02 +02:00
tomasr 0b92270e15 Vi: added initial documentation 2026-04-22 19:06:48 +02:00
tomasr 7db5bfb44a Docs: updated list of planned ports 2026-04-22 19:04:54 +02:00
tomasrandGitHub eceaf723f1 Merge (#5) from tomasriveral/micro. Adds micro support
Micro: added initial support
2026-04-22 18:41:01 +02:00
tomasr cb6ac9bce6 Micro: added initial support 2026-04-22 18:37:31 +02:00
tomasr 847f0dae5d Micro: added initial documentation 2026-04-22 18:24:35 +02:00
tomasrandGitHub 411ed0939a Merge (#4) from tomasriveral/backup-broken. Fixes (#3)
Backup: Fix (#3) rsync arguments were not stored correcly
2026-04-22 12:03:39 +02:00
tomasr 83008d8da8 Backup: Fix (#3) rsync arguments were not stored correcly 2026-04-22 12:00:47 +02:00
tomasrandGitHub 44f14803e0 Merge (#2) from tomasriveral/kakoune. Adds Kakoune support.
Kakoune: added initial support
2026-04-21 19:26:49 +02:00
tomasr 3190da3cc5 Kakoune: added initial support 2026-04-21 19:24:27 +02:00
tomasr adbc56fb36 Kakoune: added initial documentation 2026-04-21 19:17:05 +02:00
tomasrandGitHub 9ad71fb4f5 Merge (#1) from tomasriveral/helix. Adds helix support.
Helix: added initial support
2026-04-20 18:44:29 +02:00
tomasr 5aee1f540c Helix: added initial support 2026-04-20 18:41:32 +02:00
tomasr f131316fa7 Docs: Reformatted editor support 2026-04-20 17:55:06 +02:00
tomasr 7ced9e735f Doc: Added planned editors 2026-04-20 12:06:11 +02:00
22 changed files with 2078 additions and 1214 deletions
+3
View File
@@ -0,0 +1,3 @@
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 210
+1 -1
View File
@@ -1 +1 @@
use nix use flake
+55
View File
@@ -0,0 +1,55 @@
name: Release PR
on:
workflow_dispatch:
inputs:
version:
required: true
permissions:
contents: write
pull-requests: write
jobs:
release_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- uses: DeterminateSystems/nix-installer-action@v12
- name: Create release branch
run: |
git checkout -B release/${{ inputs.version }}
- name: Set version in flake.nix
run: |
sed -i "s/version = \".*\";/version = \"${{ inputs.version }}\";/" flake.nix
- name: Update flake inputs
run: nix flake update
- name: Commit changes
run: |
git add flake.nix flake.lock
git commit -m "chore(release): ${{ inputs.version }}"
- name: Push branch
run: |
git push origin release/${{ inputs.version }}
- name: Create Pull Request
run: |
gh pr create \
--base main \
--head release/${{ inputs.version }} \
--title "[Automatic] Release PR ${{ inputs.version }}" \
--body "Automated release PR.\nAfter the test's succesfull run, create manually a release with a new tag matching ${{ inputs.version }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+41
View File
@@ -0,0 +1,41 @@
name: test
on:
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang
run: |
sudo apt-get update
sudo apt-get install -y clang-format
- name: Check format
run: |
clang-format --version
find src -name "*.c" -o -name "*.h" | xargs clang-format --dry-run --Werror
- name: Install dependencies
run: |
sudo apt-get install -y \
gcc \
make \
pkg-config \
libcjson-dev \
libncurses-dev \
git \
neovim
- name: Prepare environnement
run: mkdir -p ~/Documents/Notes
- name: Build
run: make
- name: Run tests
run: |
export TERM=xterm
make test
+58
View File
@@ -0,0 +1,58 @@
name: Weekly Flake Update
on:
schedule:
- cron: "0 3 * * 1" # every Monday at 03:00 UTC
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
flake_update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v12
- name: Set branch name (date)
id: vars
run: |
echo "branch=flake-update/$(date +%Y-%m-%d)" >> $GITHUB_OUTPUT
- name: Create update branch
run: |
git checkout -B ${{ steps.vars.outputs.branch }}
- name: Update flake inputs
run: |
nix flake update
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add flake.lock
git commit -m "chore: weekly flake update" || echo "No changes to commit"
- name: Push branch
run: |
git push origin ${{ steps.vars.outputs.branch }} --force
- name: Install GitHub CLI
run: |
sudo apt-get update
sudo apt-get install -y gh
- name: Create Pull Request
run: |
gh pr create \
--base main \
--head ${{ steps.vars.outputs.branch }} \
--title "[Automated] Weekly flake update $(date +%Y-%m-%d)" \
--body "Automated weekly update of flake inputs for $(date +%Y-%m-%d)."
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1
View File
@@ -8,3 +8,4 @@ result
massif.out.* massif.out.*
*.out.* *.out.*
vgcore.* vgcore.*
.direnv/
+20 -2
View File
@@ -37,7 +37,7 @@ Format:
Examples: Examples:
* `Editor (#1): add support for Microsoft Word` * `Editor (#1): add support for Microsoft Word`
* `README.md: fix typos` * `docs: fix typos in README.md`
* `Journal: improve entry parsing logic` * `Journal: improve entry parsing logic`
Keep commit messages short and descriptive. Keep commit messages short and descriptive.
@@ -58,7 +58,7 @@ To add support for a new editor, you must update both documentation and source c
* Add the editor name to `supportedEditor[]` * Add the editor name to `supportedEditor[]`
* Increment `numEditor` * Increment `numEditor`
* If the editor executable name differs from its logical name (for example `neovim``nvim`), update `doesEditorExist()` in `src/utils.c` * If the editor executable name differs from its logical name (for example `neovim``nvim`), update `isEditorValid()` in `src/utils.c`
* Modify `openEditor()` in `src/utils.c`: * Modify `openEditor()` in `src/utils.c`:
@@ -67,6 +67,15 @@ To add support for a new editor, you must update both documentation and source c
--- ---
## Changing default configuration
If you want to change the default configuration or add a new option:
1. Update [README's documentation](./README.md#configuration)
2. In `./src/utils.c`, update the function `initAppFilesAndDirs()` which creates a default configuration if `~/.config/notewrapper/config.json` or if neither `-c` nor `--config` is set.
3. In `./src/main.c` where the JSON is parsed, add the parsing logic. Do not forget the debugging information, errors when wrong type or when an important field is missing and add a default value if a less important field is missing.
---
## Before opening a pull request ## Before opening a pull request
Before submitting a pull request, ensure that: Before submitting a pull request, ensure that:
@@ -76,3 +85,12 @@ Before submitting a pull request, ensure that:
* `make`, or * `make`, or
* `nix-build` (on NixOS) * `nix-build` (on NixOS)
* There are **no warnings or errors** * There are **no warnings or errors**
---
## Continuous integration
All PR will need to pass these checks:
1. `clang-format`. Running `find src -name "*.c" -o -name "*.h" | xargs clang-format -i` will automatically apply those rules.
2. The program must build without warning or error.
3. The program must run for five seconds without crashing.
+66 -20
View File
@@ -1,5 +1,7 @@
# NoteWrapper # NoteWrapper
Note: This project fullfills all my usecases for now, so my main source for features ideas dried out. So if you have good ideas, feel free to open an issue ;)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tomasriveral/notewrapper) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tomasriveral/notewrapper)
## Why? ## Why?
@@ -21,8 +23,8 @@ The goal was to create a terminal-based interface for accessing vaults and notes
* Standard Markdown files (no custom format) * Standard Markdown files (no custom format)
* Journal support with flexible entry formats * Journal support with flexible entry formats
* Real-time Markdown rendering via Vivify * Real-time Markdown rendering via Vivify
* Backup support using `rsync` * Backup different directories into different destinations
* Supports multiple editors (vim, neovim, nano) * Supports multiple editors
--- ---
@@ -40,16 +42,41 @@ Before building NoteWrapper, you must install the following dependencies:
* `ripgrep` * `ripgrep`
* `fzf` * `fzf`
You must also have a [supported editor](#editor-support) installed: You must also have a [supported editor (and their associated plugin if needed)](#editor-support) installed:
* `vim` * `helix`
* `neovim` * `jed`
* `kakoune`
* `micro`
* `nano` * `nano`
* `neovim`
* `vi`
* `vim`
--- ---
## How to install ## How to install
### Nix flakes
Add this to your inputs in flake.nix:
```nix
notewrapper = {
url = "github:tomasriveral/notewrapper";
};
```
If your flake already inputs nixpkgs and/or flake-utils, you can follow it:
```nix
notewrapper = {
url = "github:tomasriveral/notewrapper;
inputs.nixpkgs.follow = "nixpkgs";
inputs.flake-utils.follow = "flake-utils";
};
```
### Building from source
1. Install all required dependencies listed above. 1. Install all required dependencies listed above.
2. Ensure you have a supported editor installed. 2. Ensure you have a supported editor installed.
3. Clone the repository: 3. Clone the repository:
@@ -73,7 +100,7 @@ nix-build
make make
``` ```
5. Configure `./config.json` (it is automatically created on first launch if it does not already exist). 5. [Configure](#configuration) `~/.config/notewrapper/config.json` (it is automatically created on first launch if it does not already exist).
6. Run the program: 6. Run the program:
```shell ```shell
@@ -82,7 +109,7 @@ make
--- ---
## Packaging ### Packaging
There is currently no official package available for NoteWrapper. There is currently no official package available for NoteWrapper.
@@ -98,7 +125,6 @@ As a NixOS user, I will likely package it for **nixpkgs** in the future if the p
Usage: notewrapper [options] Usage: notewrapper [options]
Options: Options:
-c, --config <path/to/config> Specify the config file. -c, --config <path/to/config> Specify the config file.
-d, --directory <path/to/directory> Specify the vaults' directory.
-h, --help Display this message. -h, --help Display this message.
-e, --editor Specify the editor to open. -e, --editor Specify the editor to open.
-j, --jump Jump to the end of the file on opening. -j, --jump Jump to the end of the file on opening.
@@ -139,11 +165,16 @@ The first two features depend on [Vivify's editor integration](https://github.co
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). 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).
| Editor | Bufferless | Cursor | Jump to end | Plugin required | | Editor | Bufferless | Cursor | Jump to end | Aditional requirements |
| ------ | ---------- | ------ | ----------- | ------------------------------------------------------- | | --------- | ----------- | ------- | ------------ | ---------------------------------------------------------|
| Neovim | | | ✅ | [vivify-vim](https://github.com/jannis-baum/vivify.vim) | | [Helix](https://helix-editor.com/) | | | ✅ | |
| Vim | | | ✅ | [vivify-vim](https://github.com/jannis-baum/vivify.vim) | | [Jed](https://www.jedsoft.org/jed/) | | | ✅ | |
| Nano | ❌ | ❌ | ✅ | — | | [Kakoune](http://kakoune.org/) | ❌ | ❌ | ✅ | — |
| [Micro](https://micro-editor.github.io/) | ❌ | ✅ | ✅ | [micro-vivify](https://codeberg.org/gibbert/micro-vivify) and [modifications to your `init.lua`](./docs/micro.md)|
| [Nano](https://www.nano-editor.org/) | ❌ | ❌ | ✅ | — |
| [Neovim](https://neovim.io/) | ✅ | ✅ | ✅ | [vivify-vim](https://github.com/jannis-baum/vivify.vim) |
| [Vi](https://en.wikipedia.org/wiki/Vi_(text_editor)) | ❌ | ❌ | ✅ | |
| [Vim](https://www.vim.org/) | ✅ | ✅ | ✅ | [vivify-vim](https://github.com/jannis-baum/vivify.vim) |
[How to add support for another editor](./CONTRIBUTING.md#adding-editor-support) [How to add support for another editor](./CONTRIBUTING.md#adding-editor-support)
@@ -155,7 +186,7 @@ Edit `~/.config/notewrapper/config.json`. If it does not exist, it will be creat
```json ```json
{ {
"directory": "~/Documents/Notes/", "directory": ["~/Documents/Notes/", "/other/paths/"],
"render": true, "render": true,
"jumpToEndOfFileOnLaunch": true, "jumpToEndOfFileOnLaunch": true,
"editor": "neovim", "editor": "neovim",
@@ -164,7 +195,10 @@ Edit `~/.config/notewrapper/config.json`. If it does not exist, it will be creat
"newLineOnOpening": true, "newLineOnOpening": true,
"backup": { "backup": {
"enable": false, "enable": false,
"directory": "/path/to/backup", "directory": {
"~/Documents/Notes": "path/to/backup1",
"/other/paths/": "path/to/backup2"
},
"interval": "weekly", "interval": "weekly",
"rsyncArgs": ["-Lqah", "--update"] "rsyncArgs": ["-Lqah", "--update"]
} }
@@ -173,18 +207,28 @@ Edit `~/.config/notewrapper/config.json`. If it does not exist, it will be creat
### Fields ### Fields
* `directory`: root directory containing all vaults * `directory`: Array of directories containing the vaults.
* `render`: enable/disable Vivify rendering * `render`: enable/disable Vivify rendering
* `jumpToEndOfFileOnLaunch`: move cursor to end of file on open * `jumpToEndOfFileOnLaunch`: move cursor to end of file on open
* `editor`: selected editor (must be supported) * `editor`: selected editor (must be supported). If not set, it defaults to `$EDITOR`.
* `journalRegex`: regex used to detect journal files * `journalRegex`: regex used to detect journal files
* `dateEntry`: format for journal entries (see `strftime`) * `dateEntry`: format for journal entries (see `strftime`)
* `newLineOnOpening`: add a newline when opening a note * `newLineOnOpening`: add a newline when opening a note
* `backup.enable`: enable automatic backups using `rsync` * `backup.enable`: enable automatic backups using `rsync`
* `backup.directory`: destination directory for backups * `backup.directory`: backup's destination for each directory
* `backup.interval`: backup frequency (`daily`, `weekly`, `monthly`, or integer) * `backup.interval`: backup frequency (`daily`, `weekly`, `monthly`, or integer)
* `backup.rsyncArgs`: arguments passed to `rsync` * `backup.rsyncArgs`: arguments passed to `rsync`
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`.
--- ---
## Vivify configuration ## Vivify configuration
@@ -199,5 +243,7 @@ It is recommended to use a browser different from your main one for rendering.
## Planned features ## Planned features
* [ ] A converter between journal types * [ ] A converter between journal types
* [ ] Support multiple vault directories * [x] Support multiple vault directories
* [ ] Port NoteWrapper to other editors * [ ] Port NoteWrapper to other editors (non-exhaustive list of planned ports: `emacs -nw`, `ad`, flow-control, `ee`, `amp`, `dte`, `cano`, `mle`, `zee`, `ptext`, `kibi`, `ox`, `ne`, `dit`, `zile`, `moe`, `joe`, `pico`, `vis`)
* [x] Default to $EDITOR
* [ ] Refactoring the source code
-14
View File
@@ -1,14 +0,0 @@
{
"directory": "~/Documents/Notes/",
"render": true,
"jumpToEndOfFileOnLaunch": true,
"editor": "neovim",
"journalRegex": ".*journal.*",
"dateEntry": "# %Y %m %d %a",
"newLineOnOpening": true,
"backup": {
"enable": false,
"directory": "/path/to/backup",
"interval": "weekly"
}
}
-35
View File
@@ -1,35 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
pname = "notewrapper";
version = "1.0";
src = ./.;
nativeBuildInputs = [
pkgs.pkg-config
];
buildInputs = [
pkgs.ncurses
pkgs.cjson
pkgs.fzf
pkgs.gnused
pkgs.ripgrep
];
buildPhase = ''
make
'';
installPhase = ''
mkdir -p $out/bin
cp notewrapper $out/bin/
'';
meta = with pkgs.lib; {
description = " Journaling and notetaking TUI wrapper on top of neovim, vim and nano.";
license = licenses.gpl3Only;
platforms = platforms.linux;
};
}
+28
View File
@@ -0,0 +1,28 @@
You must add the following code to your `init.lua`:
```lua
local buffer = import("micro/buffer")
local shell = import("micro/shell")
function vivifyOpen(buf)
local cursor = buf:GetActiveCursor()
local line = cursor.Loc.Y + 1
shell.ExecCommand("viv", string.format("%s:%d", buf.AbsPath, line))
end
function onBufferOpen(buf)
if os.getenv("MICRO_VIVIFY") ~= "1" then
return
end
if buf.Type.Kind == buffer.BTDefault then
vivifyOpen(buf)
end
end
```
This enables Vivify to run automatically when a file is opened, effectively simulating “startup” behavior (since Micro does not provide a true startup command hook for this use case).
Big thanks to @Andriamanitra for answering [my questions](https://github.com/micro-editor/micro/discussions/4078).
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1784356753,
"narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "61b7c44c4073f0b827768aff0049561b5110ea5a",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+62
View File
@@ -0,0 +1,62 @@
{
description = "NoteWrapper";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils}:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in {
packages.default = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "notewrapper";
version = "v2.3.0";
src = ./.;
nativeBuildInputs = with pkgs; [
pkg-config
gnumake
];
buildInputs = with pkgs; [
ncurses
cjson
vivify
rsync
ripgrep
fzf
gnused
];
buildPhase = ''
make VERSION=${finalAttrs.version}
'';
installPhase = ''
mkdir -p $out/bin
cp notewrapper $out/bin/
'';
});
devShells.default = pkgs.mkShell {
packages = with pkgs; [
cjson
ncurses
pkg-config
gdb
valgrind
clang-tools
gnumake
];
};
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/notewrapper";
};
});
}
+19 -2
View File
@@ -1,7 +1,10 @@
CC := gcc CC := gcc
CFLAGS := -Wall -Wextra -O2 \ VERSION := $(shell git describe --tags --always --dirty)
$(shell pkg-config --cflags libcjson ncurses)
CFLAGS := -Wall -Wextra -Werror -O2 \
-DVERSION=\"$(VERSION)\" \
$(shell pkg-config --cflags libcjson ncurses)
LDFLAGS := $(shell pkg-config --libs libcjson ncurses) LDFLAGS := $(shell pkg-config --libs libcjson ncurses)
@@ -20,3 +23,17 @@ run: $(TARGET)
clean: clean:
rm -f $(TARGET) rm -f $(TARGET)
test: $(TARGET)
@timeout 5s ./$(TARGET); \
status=$$?; \
if [ $$status -eq 124 ]; then \
echo "Program ran for 5 seconds without crashing ✅"; \
exit 0; \
elif [ $$status -ne 0 ]; then \
echo "Program crashed ❌"; \
exit 1; \
else \
echo "Program exited cleanly ✅"; \
exit 0; \
fi
-12
View File
@@ -1,12 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.cjson
pkgs.ncurses
pkgs.pkg-config
pkgs.gdb
pkgs.valgrind
pkgs.kdePackages.kcachegrind
];
}
+551 -391
View File
File diff suppressed because it is too large Load Diff
+264 -180
View File
@@ -1,16 +1,44 @@
#include "notes.h" #include "notes.h"
#include "ui.h" #include "utils.h"
char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex, int *count, int shouldDebug) { char *getDirectoryFromVault(char *targetVault, char **vaultsArray, int vaultTotalNumber, int *vaultNumberPerDirectory, char **directoryArray, int directoryNumber, int shouldDebug) {
debug("Searching the vault %s inside all the directories...", targetVault);
debug("Here are how many vaults there is per directory:");
for (int i = 0; i < directoryNumber; i++) {
altDebug("%d for %s\n", vaultNumberPerDirectory[i], directoryArray[i]);
}
debug("Here are all the vaults in the order they will be searched:");
for (int i = 0; i < vaultTotalNumber; i++) {
altDebug("%s\n", vaultsArray[i]);
}
int index = 0;
for (int i = 0; i < directoryNumber; i++) {
for (int j = 0; j < vaultNumberPerDirectory[i]; j++) {
if (strcmp(vaultsArray[index], targetVault) == 0) {
debug("%s found in %s. (index %d)", targetVault, directoryArray[i], index);
return directoryArray[i];
}
index++;
}
}
error(1, "program", "the vault %s was not found", targetVault);
return "this makes the compiler and clangd happy. Who doesn't want GCC and clangd to be happy? "
"Such person would be a terrible monster... One must imagine GCC and clangd happy.";
}
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/
struct dirent *vaultEntry; struct dirent *vaultEntry;
char tempPath[PATH_MAX]; char tempPath[PATH_MAX];
snprintf(tempPath, sizeof(tempPath), "%s/%s", pathToVault, vault); // sets the full absolute path to fullPathEntry snprintf(tempPath, sizeof(tempPath), "%s/%s", pathToVault,
vault); // sets the full absolute path to fullPathEntry
DIR *vaultDirectory = opendir(tempPath); DIR *vaultDirectory = opendir(tempPath);
error(vaultDirectory==NULL, "program", "Could not open directory %s", tempPath); error(vaultDirectory == NULL, "program", "Could not open directory %s", tempPath);
char **journalsArray = NULL; // will contain all the notes char **journalsArray = NULL; // will contain all the notes
int journalsCount = 0; // we need to count how many notes there is to always readjust how many memory we alloc int journalsCount = 0; // we need to count how many notes there is to always readjust how many memory we alloc
// https://stackoverflow.com/a/1085120 for regex code // https://stackoverflow.com/a/1085120 for regex code
regex_t regex; regex_t regex;
@@ -23,20 +51,21 @@ char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex,
// 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) { // we iterate over every entry from the dir. So files and dirs (. and .. included) while ((vaultEntry = readdir(vaultDirectory)) != NULL) { // we iterate over every entry from the dir. So files and dirs (. and .. included)
altDebug("%s ", vaultEntry->d_name); altDebug("%s ", vaultEntry->d_name);
if (vaultEntry->d_name[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files) if (vaultEntry->d_name[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files)
regexReturn = regexec(&regex, vaultEntry->d_name, 0, NULL, 0); regexReturn = regexec(&regex, vaultEntry->d_name, 0, NULL, 0);
if (!regexReturn) { // if the regex matches if (!regexReturn) { // if the regex matches
altDebug("matched with the regex. It is a journal.\n"); altDebug("matched with the regex. It is a journal.\n");
journalsArray = realloc(journalsArray, (journalsCount + 1)*sizeof(char*)); // resize notesArray so that journalsArray = realloc(journalsArray,
journalsArray[journalsCount] = strdup(vaultEntry->d_name); // copy the dir name into notesArray (journalsCount + 1) * sizeof(char *)); // resize notesArray so that
journalsCount++; journalsArray[journalsCount] = strdup(vaultEntry->d_name); // copy the dir name into notesArray
journalsCount++;
} else {
altDebug("did not matched with the regex. It is a note.\n");
}
} else { } else {
altDebug("did not matched with the regex. It is a note.\n"); altDebug(" was ignored\n");
} }
} else {
altDebug(" was ignored\n");
}
} }
altDebug("└ ------------------------------\n"); altDebug("└ ------------------------------\n");
// free's some used memory // free's some used memory
@@ -45,42 +74,51 @@ char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex,
return journalsArray; return journalsArray;
} }
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
// originally from https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/ // suitable notes (so not the hidden ones) 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; struct dirent *vaultEntry;
char tempPath[PATH_MAX]; char tempPath[PATH_MAX];
snprintf(tempPath, sizeof(tempPath), "%s/%s", pathToVault, vault); // sets the full absolute path to fullPathEntry snprintf(tempPath, sizeof(tempPath), "%s/%s", pathToVault,
vault); // sets the full absolute path to fullPathEntry
DIR *vaultDirectory = opendir(tempPath); DIR *vaultDirectory = opendir(tempPath);
error(vaultDirectory==NULL, "program", "Could not open directory %s", tempPath); error(vaultDirectory == NULL, "program", "Could not open directory %s", tempPath);
char **notesArray = NULL; // will contain all the notes char **notesArray = NULL; // will contain all the notes
int notesCount = 0; // we need to count how many notes there is to always readjust how many memory we alloc int notesCount = 0; // we need to count how many notes 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("┌------------------------------\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; char *entryName = vaultEntry->d_name;
int entryLenght = strlen(entryName); int entryLenght = strlen(entryName);
altDebug("%s ", 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 (entryName[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, entryName); // sets the full absolute path to fullPathEntry snprintf(fullPathEntry, sizeof(fullPathEntry), "%s/%s/%s", pathToVault, vault,
// check if it matches the regex. If it does not match regexReturn != 0. entryName); // sets the full absolute path to fullPathEntry
regexReturn = regexec(&regex, entryName, 0, NULL, 0); // check if it matches the regex. If it does not match regexReturn != 0.
struct stat metadataPathEntry; regexReturn = regexec(&regex, entryName, 0, NULL, 0);
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 struct stat metadataPathEntry;
notesArray = realloc(notesArray, (notesCount + 1)*sizeof(char*)); // resize notesArray so that if (stat(fullPathEntry, &metadataPathEntry) == 0 && entryName[entryLenght - 3] == '.' && entryName[entryLenght - 2] == 'm' && entryName[entryLenght - 1] == 'd' && regexReturn &&
notesArray[notesCount] = strdup(entryName); // copy the dir name into notesArray S_ISREG(metadataPathEntry.st_mode)) { // if this entry is a file ending in .md and
notesCount++; // that does no match the regex
altDebug("did not match with the regex. It is a note.\n"); notesArray = realloc(notesArray, (notesCount + 1) * sizeof(char *)); // resize notesArray so that
} else if (!regexReturn) {altDebug("matched with the regex. It is a journal.\n");} notesArray[notesCount] = strdup(entryName); // copy the dir name into notesArray
} else {altDebug("was ignored\n");} 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"); altDebug("└ ------------------------------\n");
@@ -90,148 +128,194 @@ char** getNotesFromVault(char *pathToVault, char *vault, char *journalRegex, int
return notesArray; return notesArray;
} }
char **getVaultsFromDirectory(char *dirString, int *count, int shouldDebug) { char **getVaultsFromDirectories(char **directoryStringArray, int directoryNumber, int *vaultsPerDirectoryNumber, int *count, int shouldDebug) {
// this function is inputed a path to a directory (which comes usually from the config file) and outpus all the suitable directories (so not the hidden ones) which will serve as separate vaults for notes // to avoid having to work with a tree-dimensional array. We will use a 2d and
debug("Opening %s ", dirString); // vaultsPerDirectoryNumber will indicate the width of the directories.
// originally from https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/ char **vaultsArray = NULL;
struct dirent *vaultsDirectoryEntry; int nthVault = 0; // this is only used internally to set the string into the right place in
DIR *vaultsDirectory = opendir(dirString); // directoryStringArray.
error(vaultsDirectory==NULL, "program", "Could not open directory %s", dirString); int previousStartIndex = 0;
char **dirsArray = NULL; // will contain all the dirs/vaults for (int i = 0; i < directoryNumber; i++) {
int dirsCount = 0; // we need to count how many dirs there is to always readjust how many memory we alloc debug("Opening %s", directoryStringArray[i]);
// Refer https://pubs.opengroup.org/onlinepubs/7990989775/xsh/readdir.html // originally from
// for readdir() // https://www.geeksforgeeks.org/c/c-program-list-files-sub-directories-directory/
debug("┌------------------------------\n Detected files and dirs from the directory:"); struct dirent *vaultsDirectoryEntry;
while ((vaultsDirectoryEntry = readdir(vaultsDirectory)) != NULL) { DIR *vaultsDirectory = opendir(directoryStringArray[i]);
altDebug("%s\n", vaultsDirectoryEntry->d_name); error(!vaultsDirectory, "program", "Could not open directory %s", directoryStringArray[i]);
if (vaultsDirectoryEntry->d_name[0] != '.') { // if the entry don't start with a dot (so hidden dirs and hidden files) vaultsPerDirectoryNumber[i] = 0;
char fullPathEntry[PATH_MAX]; // creates a string of size of the maximum path lenght debug("┌------------------------------\n Detected files and dirs %s:", directoryStringArray[i]);
snprintf(fullPathEntry, sizeof(fullPathEntry), "%s/%s", dirString, vaultsDirectoryEntry->d_name); // sets the full absolute path to fullPathEntry // Refer https://pubs.opengroup.org/onlinepubs/7990989775/xsh/readdir.html
// for readdir()
struct stat metadataPathEntry; while ((vaultsDirectoryEntry = readdir(vaultsDirectory)) != NULL) {
if (stat(fullPathEntry, &metadataPathEntry) == 0 && S_ISDIR(metadataPathEntry.st_mode)) { // if this entry is a directory char *entryName = vaultsDirectoryEntry->d_name; // gets the entry as a string value
dirsArray = realloc(dirsArray, (dirsCount + 1)*sizeof(char*)); // resize dirsArray so that altDebug("%s", entryName);
dirsArray[dirsCount] = strdup(vaultsDirectoryEntry->d_name); // copy the dir name into dirsArray if (entryName[0] != '.') { // if not hidden file/dir
dirsCount++; char tempFullEntryPath[PATH_MAX]; // we recreate the full path to check it's
// proprieties
snprintf(tempFullEntryPath, PATH_MAX, "%s%s", directoryStringArray[i], entryName);
altDebug(" (%s)", tempFullEntryPath);
// checking the metadata to see if it is a dir
struct stat metadataEntry;
if (stat(tempFullEntryPath, &metadataEntry) == 0 && S_ISDIR(metadataEntry.st_mode)) { // get's the metadata (stat should return 0 if
// fails) and sees if it is a dir.
altDebug(" is a vault");
vaultsArray = realloc(vaultsArray, sizeof(char *) * (nthVault + 1)); // resize vaultsArray
error(vaultsArray == NULL, "program", "realloc failed");
vaultsPerDirectoryNumber[i]++; // it will be used later to know which vaults
// goes into which directory
vaultsArray[nthVault] = strdup(entryName); // we use strdup and not strcpy, because memory used with
// opendir and readdir will be closed.
error(vaultsArray[nthVault] == NULL, "program", "strdup failed");
nthVault++; // it is used immediatly to set the vault into directoryStringArray
} else {
altDebug(" is not a vault (not a dir.)");
}
} else {
altDebug(" is not a vault (hidden file/dir)");
}
} }
} // we sort entries for the same vault. We can't sort them all. This would break the order.
qsort(vaultsArray + previousStartIndex, nthVault - previousStartIndex, sizeof(const char *),
compareString); // sorts the vaults alphabetically
closedir(vaultsDirectory);
previousStartIndex = nthVault;
} }
altDebug("└------------------------------\n"); // we calculate once the total number of vaults to avoid recalculation every time we use it
*count = 0;
// free's some used memory for (int i = 0; i < directoryNumber; i++) {
closedir(vaultsDirectory); *count += vaultsPerDirectoryNumber[i];
*count = dirsCount; }
return dirsArray; 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. path[PATH_MAX] = '\0'; // it assures it is a string (Most cases this does nothing). But rewriting at least
debug("Handling the journal %s", path); // one bytes make the compile happy. He doesn't want to return an unchanged input.
debug("Handling the journal %s", path);
char *date = getFormatedTime(timeFormat, shouldDebug); char *date = getFormatedTime(timeFormat, shouldDebug);
debug("Formated time for the journal's entry is %s", date); debug("Formated time for the journal's entry is %s", date);
char dateWithExtension[PATH_MAX]; char dateWithExtension[PATH_MAX];
snprintf(dateWithExtension, PATH_MAX, "%s.md", date); snprintf(dateWithExtension, PATH_MAX, "%s.md", date);
sanitize(dateWithExtension); sanitize(dateWithExtension);
debug("Sanitized date: %s\n(it might be used later for a file name if the journal is divided)", dateWithExtension); debug("Sanitized date: %s\n(it might be used later for a file name if the journal is divided)", dateWithExtension);
struct stat metadata; struct stat metadata;
error(stat(path, &metadata), "program", "stat() failed to get information about %s", path); stat(path, &metadata);
if (S_ISREG(metadata.st_mode)) { if (S_ISREG(metadata.st_mode)) {
debug("%s is a unified journal.", path); debug("%s is a unified journal.", path);
if (!isStringInFile(path, date, shouldDebug)) { // if there is no entry for current date if (!isStringInFile(path, date, shouldDebug)) { // if there is no entry for current date
appendToFile(path, date, shouldDebug); appendToFile(path, date, shouldDebug);
*journalWasUpdated = 1; *journalWasUpdated = 1;
} // if there is an entry do nothing } // if there is an entry do nothing
} else if (S_ISDIR(metadata.st_mode)) { } else if (S_ISDIR(metadata.st_mode)) {
debug("%s is a divided journal.", path); debug("%s is a divided journal.", path);
struct dirent *dividedJournalEntry; struct dirent *dividedJournalEntry;
// snprintf does not like to have the same variable as input and output so we use a buffer
char temp[PATH_MAX];
snprintf(temp, PATH_MAX, "%s/", path); // appends a /. For safety
strncpy(path, temp, PATH_MAX);
DIR *dividedJournalDirectory = opendir(path);
error(dividedJournalDirectory==NULL, "program", "Could not open directory %s", path);
/*char **entryArray = NULL; // will contain all the entries of the divided journal
// 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 = 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) {
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
snprintf(fullPathEntry, sizeof(fullPathEntry), "%s/%s", path, dividedJournalEntry->d_name); // sets the full absolute path to fullPathEntry
struct stat metadataPathEntry;
if (stat(fullPathEntry, &metadataPathEntry) == 0 && S_ISREG(metadataPathEntry.st_mode)) { // if this entry is a directory
entryArray = realloc(entryArray, (entryCount + 1)*sizeof(char*));
entryArray[entryCount] = strdup(dividedJournalEntry->d_name); // copy the dir name into entryArray
entryCount++;
}
}
}
altDebug("└------------------------------\n");
// free's some used memory
closedir(dividedJournalDirectory);
qsort(entryArray + extraOptions, entryCount - extraOptions, sizeof(const char *), reverseCompareString); // sorts the journals entries alphabetically. // the + extraOptions - extraOptions is to not sort "Create new entry for the journal" or Open random entry which is the first 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, " ", " ", "", 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
debug("Selected option from journal entry selection: %s", selectedOption);
if (strcmp(selectedOption, createEntryMessage) == 0) { // create new entry
char temp[PATH_MAX];
error(strlen(path)+1+strlen(dateWithExtension)+1>PATH_MAX, "Error file path too long. %s/%s must not exceed PATH_MAX", path, dateWithExtension);
snprintf(temp, PATH_MAX, "%s/%s", path, dateWithExtension);
strncpy(path, temp, PATH_MAX);
if (!isStringInArray(dateWithExtension, (const char **)entryArray, entryCount)) { // it only creates it if it doesn't already exist
// if it does exist it will just pass the full path
debug("Creating entry %s inside %s", date, path);
FILE *file;
file = fopen(path, "w");
error(file == NULL, "program", "%s couldn't be created", path);
debug("Writing %s\\n to %s", date, path);
fprintf(file, "%s\n", date);
fclose(file);
free(createEntryMessage);
*journalWasUpdated = 1;
} else {
debug("Today's entry (%s) already exist. We won't create a new one.", dateWithExtension);
}
} else if (strcmp(selectedOption, entryArray[1]) == 0) { // if we choosed to open a random entry
// setting up the seed for rand()
srand(time(NULL)); // not totaly random, just pseudorandom
int randomEntry = extraOptions + (rand() % (entryCount - extraOptions)); // we get a random number between extraOptions and entryCount (so all journal entries and not extraOptions)
char temp[PATH_MAX];
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 // 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];
snprintf(temp, PATH_MAX, "%s/%s", path, selectedOption); snprintf(temp, PATH_MAX, "%s/", path); // appends a /. For safety
strncpy(path, temp, PATH_MAX); strncpy(path, temp, PATH_MAX);
debug("Returning path to the selected entry: %s", path); DIR *dividedJournalDirectory = opendir(path);
} error(dividedJournalDirectory == NULL, "program", "Could not open directory %s", path);
} else { /*char **entryArray = NULL; // will contain all the entries of the divided journal
error(1, "program", "%s is not a file and not a directory.", path); // 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 = 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
// for readdir()
debug("┌------------------------------\n Detected files and dirs from %s:", path);
// iterates over all the entries from the dir
while ((dividedJournalEntry = readdir(dividedJournalDirectory)) != NULL) {
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
snprintf(fullPathEntry, sizeof(fullPathEntry), "%s/%s", path,
dividedJournalEntry->d_name); // sets the full absolute path to fullPathEntry
return path; struct stat metadataPathEntry;
if (stat(fullPathEntry, &metadataPathEntry) == 0 && S_ISREG(metadataPathEntry.st_mode)) { // if this entry is a directory
entryArray = realloc(entryArray, (entryCount + 1) * sizeof(char *));
entryArray[entryCount] = strdup(dividedJournalEntry->d_name); // copy the dir name into entryArray
entryCount++;
}
}
}
altDebug("└------------------------------\n");
// free's some used memory
closedir(dividedJournalDirectory);
qsort(entryArray + extraOptions, entryCount - extraOptions, sizeof(const char *),
reverseCompareString); // sorts the journals entries alphabetically. // the +
// extraOptions - extraOptions is to not sort "Create new entry
// for the journal" or Open random entry which is the first
// 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 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
debug("Selected option from journal entry selection: %s", selectedOption);
if (strcmp(selectedOption, createEntryMessage) == 0) { // create new entry
char temp[PATH_MAX];
error(strlen(path) + 1 + strlen(dateWithExtension) + 1 > PATH_MAX, "Error file path too long. %s/%s must not exceed PATH_MAX", path, dateWithExtension);
snprintf(temp, PATH_MAX, "%s/%s", path, dateWithExtension);
strncpy(path, temp, PATH_MAX);
if (!isStringInArray(dateWithExtension, (const char **)entryArray,
entryCount)) { // it only creates it if it doesn't already exist
// if it does exist it will just pass the full path
debug("Creating entry %s inside %s", date, path);
FILE *file;
file = fopen(path, "w");
error(file == NULL, "program", "%s couldn't be created", path);
debug("Writing %s\\n to %s", date, path);
fprintf(file, "%s\n", date);
fclose(file);
free(createEntryMessage);
*journalWasUpdated = 1;
} else {
debug("Today's entry (%s) already exist. We won't create a new one.", dateWithExtension);
}
} else if (strcmp(selectedOption, entryArray[1]) == 0) { // if we choosed to open a random entry
// setting up the seed for rand()
srand(time(NULL)); // not totaly random, just pseudorandom
int randomEntry = extraOptions + (rand() % (entryCount - extraOptions)); // we get a random number between extraOptions and entryCount (so
// all journal entries and not extraOptions)
char temp[PATH_MAX];
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 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
char temp[PATH_MAX];
snprintf(temp, PATH_MAX, "%s/%s", path, selectedOption);
strncpy(path, temp, PATH_MAX);
debug("Returning path to the selected entry: %s", path);
}
} else {
error(1, "program", "%s is not a file and not a directory.", path);
}
return path;
} }
+17 -8
View File
@@ -1,22 +1,31 @@
#ifndef NOTES_H #ifndef NOTES_H
#define NOTES_H #define NOTES_H
#include "utils.h"
#include "ui.h" #include "ui.h"
#include "utils.h"
// find which directory contains targetVault
char *getDirectoryFromVault(char *targetVault, char **vaultsArray, int vaultTotalNumber, int *vaultNumberPerDirectory, char **directoryArray, int directoryNumber, int shouldDebug);
// gets the journals from the vault. // gets the journals from the vault.
//to distinguish journals from note we use regex. // to distinguish journals from note we use regex.
//Note: we must handle them separatly as journals can either be a single file (which will treat specially) or a directory. // Note: we must handle them separatly as journals can either be a single file (which will treat
// specially) or a directory.
char **getJournalsFromVault(char *pathToVault, char *vault, char *journalRegex, int *count, int shouldDebug); char **getJournalsFromVault(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
// journalRegex is the regex code for the journals. If a note matches this code, it is treated as a journal and it is not outputed from this function. // suitable notes (so not the hidden ones). journalRegex is the regex code for the journals. If a
// note matches this code, it is treated as a journal and it is not outputed from this function.
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 directory (which comes usually from the config file) and outpus all the suitable directories (so not the hidden ones) which will serve as separate vaults for notes. // function gets as input an array of directoryNumber strings. Which are the directories the
char **getVaultsFromDirectory(char *dirString, int *count, int shouldDebug); // function will search for vaults. it returns an array of vaults. the vaults are organized in order
// per directory. vaultsPerDirectoryNumber and count should be initiallized before calling the
// function and the address be inputed. count is the total number of vaults.
// vaultsPerDirectoryNumber gives how many vaults each directory has.
char **getVaultsFromDirectories(char **directoryStringArray, int directoryNumber, int *vaultsPerDirectoryNumber, int *count, int shouldDebug);
// path is the path to the file. // path is the path to the file.
// journal is the name of the file. // journal is the name of the file.
// journalWasUpdated will be set to 1 if a new entry was created // journalWasUpdated will be set to 1 if a new entry was created
// handles both type of journal (divided and unified). // handles both type of journal (divided and unified).
// creates new entry with date. // creates new entry with date.
// for divided select if we want to acces to a new entry or a old one. // 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. // 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 #endif
+304 -261
View File
@@ -1,160 +1,178 @@
#include "ui.h" #include "ui.h"
#include "utils.h"
void createNewVault(char *dirToVault, int bypass, char *bypassvalue, int shouldDebug) { void createNewVault(char **directoriesArray, int directoryCount, char **vaultsArray, int vaultCount, int bypass, char *bypassvalue, int shouldDebug) {
int duplicateWarning = 0; // set to 1 later if the vault you tried to create already existed int duplicateWarning = 0; // set to 1 later if the vault you tried to create already existed
int emptyWarning = 0; // set to 1 later if inpted empty name int emptyWarning = 0; // set to 1 later if inpted empty name
input_screen: input_screen:
char *vaultName = malloc(PATH_MAX); // choose in which directory the vault will be created
if (!bypass) { // if won't bypass (if -v or --vault weren't set) char *dirToVault =
echo(); 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);
keypad(stdscr, FALSE); char *vaultName = malloc(PATH_MAX);
// color code from https://stackoverflow.com/a/73396575 if (!bypass) { // if won't bypass (if -v or --vault weren't set)
start_color(); echo();
clear(); keypad(stdscr, FALSE);
use_default_colors(); // color code from https://stackoverflow.com/a/73396575
init_pair(1, COLOR_WHITE, -1); // -1 is blank start_color();
init_pair(2, COLOR_RED, -1); clear();
attron(COLOR_PAIR(1)); use_default_colors();
printw("Enter the name of the new vault: "); init_pair(1, COLOR_WHITE, -1); // -1 is blank
mvprintw(3, 0, "Unsafe characters such as \\ and / will be replaced by _"); init_pair(2, COLOR_RED, -1);
attroff(COLOR_PAIR(1)); attron(COLOR_PAIR(1));
if (duplicateWarning) { printw("Enter the name of the new vault: ");
attron(COLOR_PAIR(2)); mvprintw(3, 0, "Unsafe characters such as \\ and / will be replaced by _");
mvprintw(4, 0, "A vault with this name already exists. Please input another name"); attroff(COLOR_PAIR(1));
attroff(COLOR_PAIR(2)); if (duplicateWarning) {
attron(COLOR_PAIR(2));
mvprintw(4, 0, "A vault with this name already exists. Please input another name");
attroff(COLOR_PAIR(2));
}
if (emptyWarning) {
attron(COLOR_PAIR(2));
mvprintw(5, 0, "The vault's name must not be empty. Please input another name");
attroff(COLOR_PAIR(2));
}
move(1, 1); // replace cursor
wgetnstr(stdscr, vaultName, PATH_MAX - 1);
refresh();
endwin();
reset_shell_mode();
fflush(stdout);
fflush(stderr);
} else {
strncpy(vaultName, bypassvalue, PATH_MAX - 2); // -2 (and later -1) because indexing
vaultName[PATH_MAX - 1] = '\0';
} }
if (emptyWarning) { // check if no empty string
attron(COLOR_PAIR(2)); if (strcmp(vaultName, "") == 0) {
mvprintw(5, 0, "The vault's name must not be empty. Please input another name"); emptyWarning = 1;
attroff(COLOR_PAIR(2)); goto input_screen;
} }
move(1, 1); // replace cursor debug("Inputed vaultName=%s", vaultName);
wgetnstr(stdscr, vaultName, PATH_MAX-1); sanitize(vaultName);
refresh(); debug("Sanitized vaultName=%s", vaultName);
endwin();
reset_shell_mode();
fflush(stdout);
fflush(stderr);
} else {
strncpy(vaultName, bypassvalue, PATH_MAX -2); // -2 (and later -1) because indexing
vaultName[PATH_MAX-1] = '\0';
}
// check if no empty string
if (strcmp(vaultName, "") == 0) {
emptyWarning = 1;
goto input_screen;
}
debug("Inputed vaultName=%s", vaultName);
sanitize(vaultName);
debug("Sanitized vaultName=%s", vaultName);
struct stat st = {0}; // https://stackoverflow.com/a/7430262 if (!isStringInArray(vaultName, (const char **)vaultsArray,
char vaultFullPath[PATH_MAX]; vaultCount)) { // if vault doesnt already exists. We avoid vaults with the
sprintf(vaultFullPath, "%s/%s/", dirToVault, vaultName); // same name even if they are from different directories.
if (stat(vaultFullPath, &st) == -1) { char vaultFullPath[PATH_MAX]; // recreating the full path
mkdir(vaultFullPath, 0744); sprintf(vaultFullPath, "%s/%s/", dirToVault, vaultName);
} else {
// if stat(...) != -1 it means the vault already exist. We will go back to the input screen with a new message. mkdir(vaultFullPath, 0744);
duplicateWarning = 1; } else {
goto input_screen; duplicateWarning = 1;
} goto input_screen;
free(vaultName); }
free(vaultName);
} }
char *createNewNote(char dirToVault[PATH_MAX], char *vaultFromDir, int bypass, char *bypassvalue, char *journalRegex, int shouldDebug) { char *createNewNote(char dirToVault[PATH_MAX], char *vaultFromDir, int bypass, char *bypassvalue, char *journalRegex, int shouldDebug) {
// input from user for the name // input from user for the name
char *fileName = malloc(BUFFER_SIZE); char *fileName = malloc(BUFFER_SIZE);
if (!bypass) { // if we don't bypass. (if -n or --note weren't set.) if (!bypass) { // if we don't bypass. (if -n or --note weren't set.)
echo(); echo();
keypad(stdscr, FALSE); keypad(stdscr, FALSE);
clear(); clear();
printw("Enter the name of the new note: "); printw("Enter the name of the new note: ");
mvprintw(3, 0, "Unsafe characters such as \\, and / will be replaced by _"); mvprintw(3, 0, "Unsafe characters such as \\, and / will be replaced by _");
mvprintw(4, 0, "If the name matches with the regex for a journal (%s), it will create a journal instead of a note.", journalRegex); mvprintw(4, 0,
move(1,1); "If the name matches with the regex for a journal (%s), it will create a journal "
wgetnstr(stdscr, fileName, BUFFER_SIZE-4); //limits the buffer to prevent overflow (-4 to account indexing and from ".md" in case we need to add it later) "instead of a note.",
refresh(); journalRegex);
endwin(); move(1, 1);
reset_shell_mode(); wgetnstr(stdscr, fileName,
fflush(stdout); BUFFER_SIZE - 4); // limits the buffer to prevent overflow (-4 to account indexing
fflush(stderr); // and from ".md" in case we need to add it later)
} else { // bypasses user input if we bypass is set to 1 refresh();
strncpy(fileName, bypassvalue, BUFFER_SIZE); endwin();
fileName[BUFFER_SIZE-1] = '\0'; reset_shell_mode();
} fflush(stdout);
error(strcmp(fileName, "") == 0, "user", "fileName is empty"); // replace this with a warning and add a warning if duplicate file and handle case where multiple warnings (if such case is possible) fflush(stderr);
// check/sanitize the input } else { // bypasses user input if we bypass is set to 1
debug("Inputed fileName=%s", fileName); strncpy(fileName, bypassvalue, BUFFER_SIZE);
sanitize(fileName); fileName[BUFFER_SIZE - 1] = '\0';
debug("Sanitized fileName=%s (We might append .md later", fileName);
// we trow an error if the file already exists (previous behaviour was overwriting the file)
char path[PATH_MAX];
snprintf(path, PATH_MAX, "%s/%s/%s", dirToVault, vaultFromDir, fileName);
struct stat st;
error(stat(path, &st) == 0, "user", "%s already exists", fileName);
// if it matches with the journalRegex we treat it as a journal instead of a note
regex_t regex;
int regexReturn = regcomp(&regex, journalRegex, 0);
error(regexReturn, "program", "Regex compilation failed.");
regexReturn = regexec(&regex, fileName, 0, NULL, 0);
if (!regexReturn) {
debug("%s matches with %s treating it as a journal", fileName, journalRegex);
char **options = malloc(32); // the number of bytes is exactly what in the two strings
options[0] = "Divided journal";
options[1] = "Unified journal";
char *optionSelected = ncursesSelect(options, "Select which type of journal you want to create (Use arrows or WASD, Enter to select):", 2, 0, "", "", " ", shouldDebug);
debug("%s was selected to be a %s", fileName, optionSelected);
if (strcmp(optionSelected, options[0]) == 0) { // if it is a divided journal
char *fileFullPath = malloc(PATH_MAX);
snprintf(fileFullPath, PATH_MAX, "%s/%s/%s/", dirToVault, vaultFromDir, fileName);
struct stat st = {0};
if (stat(fileFullPath, &st) == -1) {
error(!mkdir(fileFullPath, 0744), "program", "mkdir failed");
} else {
error(1, "program", "%s could not be created", fileFullPath);
}
free(fileFullPath);
} else { // if it is a unified journal
char *fileFullPath = malloc(PATH_MAX);
snprintf(fileFullPath, PATH_MAX, "%s/%s/%s", dirToVault, vaultFromDir, fileName);
int len = strlen(fileFullPath);
if (fileFullPath[len-3] != '.' || fileFullPath[len-2] != 'm' || fileFullPath[len-1] != 'd') { // there might be a cleaner way to do this
error(len > PATH_MAX - 3, "user", "%s is too big (greater than PATH_MAX-3) and we can't append .md", fileFullPath);
strncat(fileFullPath, ".md", PATH_MAX);
// we checked before if fileName didn't already exist.
// we must redo it as we add a .mode
struct stat st;
error(stat(fileFullPath, &st) == 0, "user", "%s already exists", fileFullPath);
}
FILE *filePointer;
filePointer = fopen(fileFullPath, "w"); // creates and opens the file
error(filePointer == NULL, "program", "The %s couldn't be created.", fileFullPath);
fprintf(filePointer, "### %s\n", fileName);
fclose(filePointer); // closes the file so that nvim could open it
free(fileFullPath);
} }
free(options); // they are useless now error(strcmp(fileName, "") == 0, "user",
} else { // normal process for a note "fileName is empty"); // replace this with a warning and add a warning if duplicate file and
debug("%s does not match with %s treating it as a note", fileName, journalRegex); // handle case where multiple warnings (if such case is possible)
// if there is no .md add an .md // check/sanitize the input
int len = strlen(fileName); debug("Inputed fileName=%s", fileName);
if (fileName[len-3] != '.' || fileName[len-2] != 'm' || fileName[len-1] != 'd') { // there might be a cleaner way to do this sanitize(fileName);
strcat(fileName, ".md"); // this should not cause an overflow issue as we get at most 252 chars (+'.'+'m'+'d'+'\0' makes it to 256) with wgetnstr debug("Sanitized fileName=%s (We might append .md later", fileName);
// we trow an error if the file already exists (previous behaviour was overwriting the file)
char path[PATH_MAX];
snprintf(path, PATH_MAX, "%s/%s/%s", dirToVault, vaultFromDir, fileName);
struct stat st;
error(stat(path, &st) == 0, "user", "%s already exists", fileName);
// if it matches with the journalRegex we treat it as a journal instead of a note
regex_t regex;
int regexReturn = regcomp(&regex, journalRegex, 0);
error(regexReturn, "program", "Regex compilation failed.");
regexReturn = regexec(&regex, fileName, 0, NULL, 0);
if (!regexReturn) {
debug("%s matches with %s treating it as a journal", fileName, journalRegex);
char **options = malloc(32); // the number of bytes is exactly what in the two strings
options[0] = "Divided journal";
options[1] = "Unified journal";
char *optionSelected = ncursesSelect(options,
"Select which type of journal you want to create "
"(Use arrows or WASD, Enter to select):",
2, 0, "", "", " ", shouldDebug);
debug("%s was selected to be a %s", fileName, optionSelected);
if (strcmp(optionSelected, options[0]) == 0) { // if it is a divided journal
char *fileFullPath = malloc(PATH_MAX);
snprintf(fileFullPath, PATH_MAX, "%s/%s/%s/", dirToVault, vaultFromDir, fileName);
struct stat st = {0};
if (stat(fileFullPath, &st) == -1) {
error(mkdir(fileFullPath, 0744), "program", "mkdir failed");
} else {
error(1, "program", "%s could not be created", fileFullPath);
}
free(fileFullPath);
} else { // if it is a unified journal
char *fileFullPath = malloc(PATH_MAX);
// we append .md directly to fileName and not only to fileFullPath to avoid passing the wrong fileName later.
int lenName = strlen(fileName);
if (fileName[lenName - 3] != '.' && fileName[lenName - 2] != 'm' && fileName[lenName - 2] != '1') {
strcat(fileName, ".md");
debug("We just appended .md to %s", fileName);
}
snprintf(fileFullPath, PATH_MAX, "%s/%s/%s", dirToVault, vaultFromDir, fileName);
// we verify if the file doesn't already exist as we might just have changed the fileName and (by extension) the file path.
struct stat metadata;
error(stat(fileFullPath, &metadata) == 0, "user", "%s already exists", fileFullPath);
FILE *filePointer;
filePointer = fopen(fileFullPath, "w"); // creates and opens the file
error(filePointer == NULL, "program", "The %s couldn't be created.", fileFullPath);
fprintf(filePointer, "### %s\n", fileName);
fclose(filePointer); // closes the file so that nvim could open it
free(fileFullPath);
}
free(options); // they are useless now
} else { // normal process for a note
debug("%s does not match with %s treating it as a note", fileName, journalRegex);
// if there is no .md add an .md
int len = strlen(fileName);
if (fileName[len - 3] != '.' || fileName[len - 2] != 'm' || fileName[len - 1] != 'd') { // there might be a cleaner way to do this
strcat(fileName, ".md"); // this should not cause an overflow issue as we get at most
// 252 chars (+'.'+'m'+'d'+'\0' makes it to 256) with wgetnstr
}
char *fileFullPath = malloc(PATH_MAX); // this dinamically allocated because we use it in
// the main function to call openEditor
sprintf(fileFullPath, "%s/%s/%s", dirToVault, vaultFromDir, fileName);
FILE *filePointer;
filePointer = fopen(fileFullPath, "w"); // creates and opens the file
error(filePointer == NULL, "program", "The %s couldn't be created.", fileFullPath);
fprintf(filePointer, "### %s\n", fileName);
fclose(filePointer); // closes the file so that nvim could open it
free(fileFullPath);
} }
char *fileFullPath = malloc(PATH_MAX); // this dinamically allocated because we use it in the main function to call openEditor return fileName;
sprintf(fileFullPath, "%s/%s/%s", dirToVault, vaultFromDir, fileName);
FILE *filePointer;
filePointer = fopen(fileFullPath, "w"); // creates and opens the file
error(filePointer == NULL, "program", "The %s couldn't be created.", fileFullPath);
fprintf(filePointer, "### %s\n", fileName);
fclose(filePointer); // closes the file so that nvim could open it
free(fileFullPath);
}
return fileName;
} }
char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) { 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) // we are gonna write each line of each files (with the filename and the line number to an
// index)
char command[CMD_BUFFER]; char command[CMD_BUFFER];
char indexFile[] = "/tmp/notewrapper_index_XXXXXX"; char indexFile[] = "/tmp/notewrapper_index_XXXXXX";
@@ -162,17 +180,7 @@ char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
error(fd == -1, "program", "mkstemp failed"); error(fd == -1, "program", "mkstemp failed");
close(fd); close(fd);
/* snprintf(command, sizeof(command), "rg --line-number --no-heading --color=never . \"%s\" > %s", pathToFiles, indexFile);
* 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); debug("INDEX CMD: %s", command);
if (system(command) != 0) { if (system(command) != 0) {
@@ -180,53 +188,13 @@ char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
error(1, "program", "rg indexing failed"); 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), snprintf(command, sizeof(command),
"cat %s | fzf --delimiter ':' --prompt='%s' " "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 '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 " "--preview-window=right:60%%:hidden "
"--bind 'change:show-preview'", "--bind 'change:show-preview'",
indexFile, indexFile, selectText ? selectText : "> ");
selectText ? selectText : "> "
);
debug("FZF CMD: %s", command); debug("FZF CMD: %s", command);
FILE *fzfPipe = popen(command, "r"); FILE *fzfPipe = popen(command, "r");
@@ -242,16 +210,25 @@ char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
buffer[strcspn(buffer, "\n")] = '\0'; buffer[strcspn(buffer, "\n")] = '\0';
result = strdup(buffer); result = strdup(buffer);
} }
debug("Buffer: %s", buffer);
debug("result: %s", result);
// cut at first ':' // cut at first ':'
result = strtok(result, ":"); result = strtok(result, ":");
// cut at / (because we need the full paths before and we only need to return the journal entry) // we need to cut out the whole pathToFiles
char *token = strtok(result, "/"); int pathToFilesLen = strlen(pathToFiles);
result = NULL; int cutOut = 0;
while (token != NULL) { for (int i = 0; i < pathToFilesLen; i++) {
result = token; if (pathToFiles[i] == result[i]) {
token = strtok(NULL, "/"); //Passing NULL means “dont start a new string, resume 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 // clean up
pclose(fzfPipe); pclose(fzfPipe);
@@ -260,81 +237,147 @@ char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug) {
return result; 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 highlight = 0; //curently highlighted option int shouldDebug) { // TODO we should see how it handles larges strings (like large directories)
int highlight = 0; // curently highlighted option
int key; int key;
cbreak(); // disable line buffering cbreak(); // disable line buffering
noecho(); // don't echo key presses noecho(); // don't echo key presses
keypad(stdscr, TRUE); // enable arrow keys keypad(stdscr, TRUE); // enable arrow keys
start_color(); start_color();
curs_set(0); // sets the cursor to invisible. (We will emulate the cursor with a hightlight that go up and down). curs_set(0); // sets the cursor to invisible. (We will emulate the cursor with a hightlight that
// go up and down).
use_default_colors(); use_default_colors();
init_pair(1, COLOR_WHITE, -1); // color for optionsNumber (so notes, vaults, etc.) init_pair(1, COLOR_WHITE, -1); // color for optionsNumber (so notes, vaults, etc.)
init_pair(2, COLOR_BLUE, -1); // color for extraOptionsNumber (so settings, delete notes, create vault, etc.) init_pair(2, COLOR_BLUE,
-1); // color for extraOptionsNumber (so settings, delete notes, create vault, etc.)
while (1) { while (1) {
clear(); clear();
attron(COLOR_PAIR(1)); attron(COLOR_PAIR(1));
mvprintw(0,0, "%s", optionsText); mvprintw(0, 0, "%s", optionsText);
int offset = 1; int offset = 1;
if (strcmp(bottomText, "") != 0) { if (strcmp(bottomText, "") != 0) {
mvprintw(offset, 1, "%s", bottomText); mvprintw(offset, 1, "%s", bottomText);
offset++; offset++;
}
// Print options with highlighting
for (int i = 0; i < optionsNumber; i++) {
if (i == highlight) {
attron(A_REVERSE);
} // highlight selected
mvprintw(i+offset, 2, "%s", options[i]);
if (i == highlight) {
attroff(A_REVERSE);
} }
} // Print options with highlighting
attroff(COLOR_PAIR(1)); for (int i = 0; i < optionsNumber; i++) {
if (strcmp(middleText, "") != 0) { if (i == highlight) {
mvprintw(offset+optionsNumber, 1, "%s", middleText); attron(A_REVERSE);
offset++; } // highlight selected
} mvprintw(i + offset, 2, "%s", options[i]);
// Printf extraOptions with highlighting if (i == highlight) {
attron(COLOR_PAIR(2)); attroff(A_REVERSE);
for (int k = optionsNumber; k < optionsNumber + extraOptionsNumber; k++) { }
if (k == highlight) {
attron(A_REVERSE);
} }
mvprintw(k+offset, 2, "%s", options[k]); attroff(COLOR_PAIR(1));
if (k == highlight) { if (strcmp(middleText, "") != 0) {
attroff(A_REVERSE); mvprintw(offset + optionsNumber, 1, "%s", middleText);
offset++;
} }
} // Printf extraOptions with highlighting
attroff(COLOR_PAIR(2)); attron(COLOR_PAIR(2));
attron(COLOR_PAIR(1)); for (int k = optionsNumber; k < optionsNumber + extraOptionsNumber; k++) {
mvprintw(offset+optionsNumber+extraOptionsNumber, 1, "%s", topText); if (k == highlight) {
attroff(COLOR_PAIR(1)); attron(A_REVERSE);
key = getch(); //get some int which value correspond to some key being pressed }
mvprintw(k + offset, 2, "%s", options[k]);
if (k == highlight) {
attroff(A_REVERSE);
}
}
attroff(COLOR_PAIR(2));
attron(COLOR_PAIR(1));
mvprintw(offset + optionsNumber + extraOptionsNumber, 1, "%s", topText);
attroff(COLOR_PAIR(1));
key = getch(); // get some int which value correspond to some key being pressed
switch(key) { switch (key) {
case KEY_UP: case KEY_UP:
case 'w': case 'w':
case 'W': case 'W':
highlight--; highlight--;
if (highlight < 0) { if (highlight < 0) {
highlight = optionsNumber + extraOptionsNumber - 1;// can't select the -1nth option highlight = optionsNumber + extraOptionsNumber - 1; // can't select the -1nth option
} }
break; break;
case 's': case 's':
case 'S': case 'S':
case KEY_DOWN: case KEY_DOWN:
highlight++; highlight++;
if (highlight >= optionsNumber + extraOptionsNumber) { if (highlight >= optionsNumber + extraOptionsNumber) {
highlight = 0; highlight = 0;
} }
break; 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 case 10: //\n
goto end_loop; goto end_loop;
} }
} }
end_loop: end_loop:
endwin(); // end ncurses mode endwin(); // end ncurses mode
fflush(stderr); fflush(stderr);
debug("Selected option: %s", options[highlight]); debug("Selected option: %s", options[highlight]);
+22 -23
View File
@@ -1,18 +1,18 @@
#ifndef UI_H #ifndef UI_H
#define UI_H #define UI_H
#include "utils.h"
#include <ctype.h>
#include <limits.h>
#include <ncurses.h>
#include <pwd.h>
#include <regex.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <ncurses.h>
#include <unistd.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <pwd.h> #include <unistd.h>
#include <ctype.h> #define CMD_BUFFER 4096 // for fzfSelect
#include <limits.h>
#include <regex.h>
#include "utils.h"
#define CMD_BUFFER 4096 // for fzfSelect
#define RESULT_BUFFER 1024 // 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.
@@ -22,24 +22,23 @@ returns the path to the note.
If you want to bypass the input TUI set bypass to 1 and bypassvalue to the note name If you want to bypass the input TUI set bypass to 1 and bypassvalue to the note name
*/ */
char *createNewNote(char dirToVault[PATH_MAX], char *vaultFromDir, int bypass, char *bypassvalue, char *journalRegex, int debug); char *createNewNote(char dirToVault[PATH_MAX], char *vaultFromDir, int bypass, char *bypassvalue, char *journalRegex, int debug);
/*Uses ncurses to get an input from the user. /*
Chooses in which directory it want to create the vault.
Uses ncurses to get an input from the user.
Creates a new vault with this input. 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 **directoriesArray, int directoryCount, char **vaultsArray, int vaultCount, int bypass, char *bypassvalue, int debug);
// uses fzf and ripgrep to search inside the files to choose between all the options. // uses fzf and ripgrep to search inside the files to choose between all the options.
char* fzfSelect(char *pathToFiles, char *selectText, int shouldDebug); 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
options is the array of strings with all the options. TUI. options is the array of strings with all the options. optionsText is the text that will be
optionsText is the text that will be printed at the top (For example: "Please select ..."). printed at the top (For example: "Please select ..."). we distinguish options from extraOptions.
we distinguish options from extraOptions.
options could be the list of all notes or all vaults. options could be the list of all notes or all vaults.
extraOptions are options that are special and have a special color (for ex: "Delete vault", "Settings", etc.). extraOptions are options that are special and have a special color (for ex: "Delete vault",
note: extraOptions should be at the end of options. "Settings", etc.). note: extraOptions should be at the end of options. topText is printed between
topText is printed between options text and the start of options. options text and the start of options. middleText (usally \n) is printed between options and
middleText (usally \n) is printed between options and extraOptions. extraOptions. bottomText is printed bellow. topText and middle must be exactly one line. If you want
bottomText is printed bellow. empty lines use " " and not "". returns the selected option. */
topText and middle must be exactly one line. If you want empty lines use " " and not "". char *ncursesSelect(char **options, char *optionsText, int optionsNumber, int extraOptionsNumber, char *bottomText, char *middleText, char *topText, int debug);
returns the selected option. */
char* ncursesSelect(char **options, char *optionsText, int optionsNumber, int extraOptionsNumber, char *bottomText, char *middleText, char *topText, int debug);
#endif #endif
+441 -207
View File
@@ -1,8 +1,7 @@
#include "utils.h" #include "utils.h"
#include <stddef.h>
const char *supportedEditor[] = {"neovim", "vim", "nano"}; const char *supportedEditor[] = {"helix", "jed", "kakoune", "micro", "nano", "neovim", "vi", "vim"};
const int numEditors = 3; const int numEditors = 8;
int compareString(const void *a, const void *b) { int compareString(const void *a, const void *b) {
const char *str1 = *(const char **)a; const char *str1 = *(const char **)a;
@@ -12,82 +11,90 @@ int compareString(const void *a, const void *b) {
int reverseCompareString(const void *a, const void *b) { int reverseCompareString(const void *a, const void *b) {
const char *str1 = *(const char **)a; const char *str1 = *(const char **)a;
const char *str2 = *(const char **)b; const char *str2 = *(const char **)b;
return -1*strcmp(str1, str2); return -1 * strcmp(str1, str2);
} }
void getCurrentTime(int *hour, int *minute, int *second) { void getCurrentTime(int *hour, int *minute, int *second) {
time_t now = time(NULL); // Get current time in seconds since epoch time_t now = time(NULL); // Get current time in seconds since epoch
struct tm *local = localtime(&now); // Convert to local time structure struct tm *local = localtime(&now); // Convert to local time structure
*hour = local->tm_hour; // Extract hour *hour = local->tm_hour; // Extract hour
*minute = local->tm_min; // Extract minutes *minute = local->tm_min; // Extract minutes
*second = local->tm_sec; // Extract seconds *second = local->tm_sec; // Extract seconds
} }
void _debug(const int d, const char *file, const int line, const char *function, const char *message, ...) { // use for formatted debug void _debug(const int d, const char *file, const int line, const char *function, const char *message, ...) { // use for formatted debug
if (d) { if (d) {
fflush(stdout); fflush(stdout);
fflush(stderr); fflush(stderr);
va_list args; //variadic function stuff va_list args; // variadic function stuff
va_start(args, message); va_start(args, message);
int h, m, s; int h, m, s;
getCurrentTime(&h, &m, &s); getCurrentTime(&h, &m, &s);
fprintf(stderr, "\e[0;32m[DEBUG -- %d:%d:%d] From file %s line %d function %s:\e[0m\n", h, m, s, file, line, function); fprintf(stderr, "\e[0;32m[DEBUG -- %d:%d:%d] From file %s line %d function %s:\e[0m\n", h, m, s, file, line, function);
vfprintf(stderr, message, args); vfprintf(stderr, message, args);
fprintf(stderr, "\e[0m\n"); fprintf(stderr, "\e[0m\n");
va_end(args); va_end(args);
} }
} }
void _altDebug(const int d, const char *message, ...) { // use for less formal debuggin. (usefull if enumerating or making a list void _altDebug(const int d, const char *message,
if (d) { ...) { // use for less formal debuggin. (usefull if enumerating or making a list
fflush(stdout); if (d) {
fflush(stderr); fflush(stdout);
va_list args; fflush(stderr);
va_start(args, message); va_list args;
vfprintf(stderr, message, args); va_start(args, message);
va_end(args); vfprintf(stderr, message, args);
} va_end(args);
}
} }
void _error(const int shouldDebug, const int condition, const char *type, const char *file, const int line, const char *function, const char *message, ...) { // used for formatted errors void _error(const int shouldDebug, const int condition, const char *type, const char *file, const int line, const char *function, const char *message,
if (condition) { ...) { // used for formatted errors
int h, m, s; if (condition) {
getCurrentTime(&h, &m, &s); int h, m, s;
fprintf(stderr, "\e[0;31m[%s ERROR -- %d:%d:%d] From file %s line %d function %s:\n", type, h, m, s, file, line, function); getCurrentTime(&h, &m, &s);
if (errno != 0) { fprintf(stderr, "\e[0;31m[%s ERROR -- %d:%d:%d] From file %s line %d function %s:\n", type, h, m, s, file, line, function);
fprintf(stderr, " (System-level error message: %s)\n", strerror(errno)); if (errno != 0) {
} else { fprintf(stderr, " (System-level error message: %s)\n", strerror(errno));
fprintf(stderr, " (No system-level error; issue is application-level)\n"); } else {
fprintf(stderr, " (No system-level error; issue is application-level)\n");
}
va_list args;
va_start(args, message);
vfprintf(stderr, message, args);
va_end(args);
if (!shouldDebug) {
fprintf(stderr, "\nRunning notewrapper -V might give you more information.");
}
fprintf(stderr, "\e[0m\n");
exit(1);
} }
va_list args;
va_start(args, message);
vfprintf(stderr, message, args);
va_end(args);
if (!shouldDebug) {
fprintf(stderr, "\nRunning notewrapper -V might give you more information.");
}
fprintf(stderr, "\e[0m\n");
exit(1);
}
} }
static void copyDir(const char *source, const char *destination, const char **rsyncArgs, const int rsyncArgsNumber, const int shouldDebug) { 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); debug("Backuping... source: %s and destination: %s", source, destination);
debug("Rsync has %d extra arguments", rsyncArgsNumber);
pid_t pid = fork(); pid_t pid = fork();
error(pid < 0, "program", "fork() faild. pid = %d", pid); error(pid < 0, "program", "fork() faild. pid = %d", pid);
if (pid == 0) { if (pid == 0) {
// Child process: execute rsync // Child process: execute rsync
char **args = malloc((4 + rsyncArgsNumber)*sizeof(char*)); char **args = malloc((4 + rsyncArgsNumber) * sizeof(char *));
args[0] = "rsync"; args[0] = "rsync";
for (int i = 0; i < rsyncArgsNumber; i++) { for (int i = 0; i < rsyncArgsNumber; i++) {
args[i+1] = (char *)rsyncArgs[i]; args[i + 1] = (char *)rsyncArgs[i];
if (i == rsyncArgsNumber-1) { // last loop if (i == rsyncArgsNumber - 1) { // last loop
args[i+2] = (char*)source; args[i + 2] = (char *)source;
args[i+3] = (char*)destination; args[i + 3] = (char *)destination;
args[i+4] = NULL; // execvp expect last arg to be NULL args[i + 4] = NULL; // execvp expect last arg to be NULL
} debug("rsync command:");
for (int k = 0; k <= i + 4; k++) {
altDebug("%s ", args[k]);
}
altDebug("\n");
}
} }
execvp("rsync", args); execvp("rsync", args);
// If execvp returns, there was an error // If execvp returns, there was an error
@@ -107,19 +114,24 @@ static void ensureDir(const char *path, const int shouldDebug) {
} }
void initAppFilesAndDirs(const char *home, const int shouldDebug) { 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]; char config_dir[PATH_MAX];
char cache_dir[PATH_MAX]; char cache_dir[PATH_MAX];
snprintf(config_dir, sizeof(config_dir), snprintf(config_dir, sizeof(config_dir), "%s/.config/", home);
"%s/.config/notewrapper", home);
snprintf(cache_dir, sizeof(cache_dir), snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/", home);
"%s/.cache/notewrapper", home);
ensureDir(config_dir, shouldDebug); ensureDir(config_dir, shouldDebug);
ensureDir(cache_dir, shouldDebug); ensureDir(cache_dir, shouldDebug);
char config_file[PATH_MAX+12]; strncat(config_dir, "notewrapper/", PATH_MAX);
strncat(cache_dir, "notewrapper/", PATH_MAX);
ensureDir(config_dir, shouldDebug);
ensureDir(cache_dir, shouldDebug);
char config_file[PATH_MAX + 12];
snprintf(config_file, sizeof(config_file), "%s/config.json", config_dir); snprintf(config_file, sizeof(config_file), "%s/config.json", config_dir);
FILE *f = fopen(config_file, "r"); FILE *f = fopen(config_file, "r");
@@ -130,28 +142,34 @@ void initAppFilesAndDirs(const char *home, const int shouldDebug) {
debug("Creating default config file."); debug("Creating default config file.");
FILE *w = fopen(config_file, "w"); FILE *w = fopen(config_file, "w");
error(!w, "program", "fopen failed opening %s"); error(!w, "program", "fopen failed opening %s");
fprintf(w, fprintf(w, // TODO CHANGE default json
"{\n" "{\n"
" \"directory\": \"~/Documents/Notes/\",\n" " \"directory\": [\"~/Documents/\"],\n" // I personally use ~/Documents/Notes. But the
" \"render\": true,\n" // dir doesn't exist most times. So for the
" \"jumpToEndOfFileOnLaunch\": true,\n" // default user it is better to put ~/Documents
" \"editor\": \"neovim\",\n" " \"render\": true,\n"
" \"journalRegex\": \".*journal.*\",\n" " \"jumpToEndOfFileOnLaunch\": true,\n"
" \"dateEntry\": \"# %%Y %%m %%d %%a\",\n" " \"editor\": \"neovim\",\n"
" \"newLineOnOpening\": true,\n" " \"journalRegex\": \".*journal.*\",\n"
" \"backup\": {\n" " \"dateEntry\": \"# %%Y %%m %%d %%a\",\n"
" \"enable\": false,\n" " \"newLineOnOpening\": true,\n"
" \"directory\": \"/path/to/backup\",\n" " \"backup\": {\n"
" \"interval\": \"weekly\",\n" " \"enable\": false,\n"
" \"rsyncArgs\": [\"-Lqah\", \"--update\"]\n" " \"directory\": {\n"
" }\n" " \"~/Documents/\": \"path/to/backup/\"\n"
"}\n"); " },\n"
" \"interval\": \"weekly\",\n"
" \"rsyncArgs\": [\"-Lqah\", \"--update\"]\n"
" }\n"
"}\n");
fclose(w); fclose(w);
} }
void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const char *homeDir, const int interval, const char **rsyncArguments, const int rsyncArgumentsNumber, const int shouldDebug) { 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; int shouldBackup = 0;
time_t now = time(NULL); time_t now = time(NULL);
debug("Time since epoch is %ld", (long)now); debug("Time since epoch is %ld", (long)now);
char cacheFilePATH[PATH_MAX]; char cacheFilePATH[PATH_MAX];
@@ -160,6 +178,7 @@ void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const cha
struct stat st; struct stat st;
if (stat(cacheFilePATH, &st) != 0) { // file does not exist if (stat(cacheFilePATH, &st) != 0) { // file does not exist
shouldBackup = 1; shouldBackup = 1;
debug("Creating (for the first time) %s", cacheFilePATH);
FILE *cacheFile = fopen(cacheFilePATH, "w"); FILE *cacheFile = fopen(cacheFilePATH, "w");
if (!cacheFile) { if (!cacheFile) {
error(1, "program", "Failed to open cache file for writing: %s", cacheFilePATH); error(1, "program", "Failed to open cache file for writing: %s", cacheFilePATH);
@@ -168,6 +187,7 @@ void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const cha
fclose(cacheFile); fclose(cacheFile);
} else { } else {
FILE *cacheFile = fopen(cacheFilePATH, "r"); FILE *cacheFile = fopen(cacheFilePATH, "r");
debug("reading %s", cacheFilePATH);
if (!cacheFile) { if (!cacheFile) {
error(1, "program", "Failed to open cache file for reading: %s", cacheFilePATH); error(1, "program", "Failed to open cache file for reading: %s", cacheFilePATH);
} }
@@ -183,8 +203,13 @@ void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const cha
if (endptr == line || (*endptr != '\n' && *endptr != '\0')) { if (endptr == line || (*endptr != '\n' && *endptr != '\0')) {
error(1, "program", "Invalid timestamp in cache file: %s", line); error(1, "program", "Invalid timestamp in cache file: %s", line);
} }
double deltaTime = difftime(now, lastBackupTime);
if (difftime(now, lastBackupTime) > interval) { if (deltaTime > interval || forceBackup) {
if (!forceBackup) {
debug("(difftime) %f is greater than (interval) %d -> backuping...", deltaTime, interval);
} else {
debug("Backup was forced. backuping...");
}
shouldBackup = 1; shouldBackup = 1;
cacheFile = fopen(cacheFilePATH, "w"); cacheFile = fopen(cacheFilePATH, "w");
if (!cacheFile) { if (!cacheFile) {
@@ -192,25 +217,57 @@ void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const cha
} }
fprintf(cacheFile, "%ld\n", (long)now); fprintf(cacheFile, "%ld\n", (long)now);
fclose(cacheFile); fclose(cacheFile);
} else {
debug("(difftime) %f is smaller than (interval) %d -> no need to backup.", deltaTime, interval);
} }
} }
if (shouldBackup) { if (shouldBackup) {
copyDir(pathOfVaults, pathOfBackup, rsyncArguments, rsyncArgumentsNumber, shouldDebug); *backupMessage = 1;
debug("A backup is needed");
for (int i = 0; i < sourceNumber; i++) {
debug("%s", destinationDirectoryArray[i]);
if (destinationDirectoryArray[i]) { // if we don't want to backup it, it was set to NULL
copyDir(sourceDirectoryArray[i], destinationDirectoryArray[i], rsyncArguments, rsyncArgumentsNumber, shouldDebug);
}
}
} }
} }
int doesEditorExist (char *editorToCheck, int shouldDebug) { // Some exectuables have not exaclty the same name as the editor. int isEditorValid(char *editorToCheck, int useDefaultEditor,
char *editor; int shouldDebug) { // check if editor is supported and if it is installed
if (strcmp(editorToCheck, "neovim") == 0) { // check if supported
editor = strdup("nvim"); // we must use strdup and not just copy as we would have modified editorToOpen in main if (useDefaultEditor) { // we use a different error message if it defaulted to $EDITOR
error(!isStringInArray(editorToCheck, supportedEditor, numEditors), "user",
"%s is not a supported editor.\n(defaulted to $EDITOR as neither \"editor\" was set "
"in the configuration file nor -e/--editor <editor> was set.)\n See "
"https://github.com/tomasriveral/NoteWrapper#editor-support for a list of supported "
"editors.",
editorToCheck);
} else {
error(!isStringInArray(editorToCheck, supportedEditor, numEditors), "user",
"%s is not a supported editor.\n See "
"https://github.com/tomasriveral/NoteWrapper#editor-support for a list of supported "
"editors.",
editorToCheck);
} }
else { // check if installed
editor = strdup(editorToCheck); char *editor;
if (strcmp(editorToCheck, "neovim") == 0) { // some executables are not name the same as the project
editor = strdup("nvim"); // we must use strdup and not just copy as we would have modified
// editorToOpen in main
} else if (strcmp(editorToCheck, "helix") == 0) {
editor = strdup("hx");
} else if (strcmp(editorToCheck, "kakoune") == 0) {
editor = strdup("kak");
} else {
editor = strdup(editorToCheck);
} }
char *path_env = getenv("PATH"); char *path_env = getenv("PATH");
error(!path_env, "program", "getenv(\"PATH\") failed to get your path. NoteWrapper is unable to check if your desired editor is installed\n"); error(!path_env, "program",
debug("Your PATH is %s\n", path_env); "getenv(\"PATH\") failed to get your path. NoteWrapper is unable to check if your "
"desired editor is installed\n");
debug("Your PATH is %s", path_env);
char *paths = strdup(path_env); // duplicate because strtok modifies the string char *paths = strdup(path_env); // duplicate because strtok modifies the string
char *dir = strtok(paths, ":"); char *dir = strtok(paths, ":");
while (dir) { while (dir) {
@@ -230,43 +287,46 @@ int doesEditorExist (char *editorToCheck, int shouldDebug) { // Some exectua
} }
char *getFormatedTime(char *format, int shouldDebug) { char *getFormatedTime(char *format, int shouldDebug) {
debug("Inputed time format is %s", format); debug("Inputed time format is %s", format);
time_t t = time(NULL); time_t t = time(NULL);
struct tm *tm = localtime(&t); struct tm *tm = localtime(&t);
char *buf = malloc(BUFFER_SIZE); char *buf = malloc(BUFFER_SIZE);
strftime(buf, BUFFER_SIZE, format, tm); strftime(buf, BUFFER_SIZE, format, tm);
debug("Formated time is %s", buf); debug("Formated time is %s", buf);
return buf; return buf;
} }
int isStringInArray(const char *string, const char **array, const int len) { int isStringInArray(const char *string, const char **array, const int len) {
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
if (strcmp(string, array[i]) == 0) { if (strcmp(string, array[i]) == 0) {
return 1; return 1;
}
} }
} return 0;
return 0;
} }
int isStringInFile(const char *path, const char *string, const int shouldDebug) { int isStringInFile(const char *path, const char *string, const int shouldDebug) {
FILE *file = fopen(path, "r"); FILE *file = fopen(path, "r");
error(file == NULL, "program", "While searching for \"%s\", we could not open file %s", string, path); error(file == NULL, "program", "While searching for \"%s\", we could not open file %s", string, path);
char buf[BUFFER_SIZE]; char buf[BUFFER_SIZE];
while (fgets(buf, BUFFER_SIZE, file) != NULL) { while (fgets(buf, BUFFER_SIZE, file) != NULL) {
if (strstr(buf, string) != NULL) { if (strstr(buf, string) != NULL) {
debug("%s contains the substring %s", buf, string); debug("%s contains the substring %s", buf, string);
return 1; return 1;
}
} }
} debug("The string %s is not inside %s", string, path);
debug("The string %s is not inside %s", string, path); return 0;
return 0;
} }
void appendToFile(const char *path, const char *string, const int shouldDebug) { void appendToFile(const char *path, const char *string, const int shouldDebug) {
FILE *file = fopen(path, "r"); FILE *file = fopen(path, "r");
char lastLine[1024] = {0}; char lastLine[1024] = {0};
error(file == NULL, "program", "could not open %s", path); error(file == NULL, "program", "could not open %s", path);
char buffer[BUFFER_SIZE]; // it does not matter if we have a small buffer. string is relatively small (most time \n or the name of the file). So it is under BUFFER_SIZE. If the last line is more than BUFFER_SIZE. It can't be equal to string char buffer[BUFFER_SIZE]; // it does not matter if we have a small buffer. string is relatively
// small (most time \n or the name of the file). So it is under
// BUFFER_SIZE. If the last line is more than BUFFER_SIZE. It can't be
// equal to string
// Read file line by line to get the last one // Read file line by line to get the last one
while (fgets(buffer, sizeof(buffer), file) != NULL) { while (fgets(buffer, sizeof(buffer), file) != NULL) {
@@ -293,17 +353,17 @@ void appendToFile(const char *path, const char *string, const int shouldDebug) {
} }
void sanitize(char *string) { void sanitize(char *string) {
size_t stringLenght = strlen(string); size_t stringLenght = strlen(string);
for (size_t i = 0; i < stringLenght; i++) { for (size_t i = 0; i < stringLenght; i++) {
if ((!isalnum((unsigned char)string[i]) && strchr("~/\\:*?\"\'|!$[]{}<>\n\r\t", string[i]))) { // replace unwanted chars by '_' if ((!isalnum((unsigned char)string[i]) && strchr("~/\\:*?\"\'|!$[]{}<>\n\r\t", string[i]))) { // replace unwanted chars by '_'
string[i] = '_'; string[i] = '_';
}
}
// removes any leading '.
size_t i = 0;
while (i < stringLenght && string[i] == '.') {
string[i++] = '_';
} }
}
// removes any leading '.
size_t i = 0;
while (i < stringLenght && string[i] == '.') {
string[i++] = '_';
}
} }
// both functions are from https://stackoverflow.com/a/5467788 // both functions are from https://stackoverflow.com/a/5467788
@@ -311,119 +371,293 @@ void sanitize(char *string) {
// remove() can't delete directories with files // remove() can't delete directories with files
// so it walks the file tree and deletes it's content before removing the directory // so it walks the file tree and deletes it's content before removing the directory
int unlink_cb(const char *filePath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { int unlink_cb(const char *filePath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) {
(void)sb; (void)sb;
(void)typeflag; (void)typeflag;
(void)ftwbuf; (void)ftwbuf;
int shouldDebug = 1; //normally shouldDebug should be passed to the function. However, it's to difficult here and the best it to set it to 1. int shouldDebug = 1; // normally shouldDebug should be passed to the function. However, it's to
// difficult here and the best it to set it to 1.
// some sanity checks to be sure that we don't delete something we shouldn't // some sanity checks to be sure that we don't delete something we shouldn't
error(strcmp(filePath, "") == 0, "program", "filePath is empty. Refusing to delete directory"); error(strcmp(filePath, "") == 0, "program", "filePath is empty. Refusing to delete directory");
error(filePath[0] == '.', "program", "%s starts with \".\". For security reasons, use absolute path and not relative path.", filePath); error(filePath[0] == '.', "program", "%s starts with \".\". For security reasons, use absolute path and not relative path.", filePath);
int rv = remove(filePath); int rv = remove(filePath);
error(rv, "program", "remove() failed to delete %s", filePath); error(rv, "program", "remove() failed to delete %s", filePath);
return rv; return rv;
} }
int rmrf(char *path, int shouldDebug) { int rmrf(char *path, int shouldDebug) {
error(strcmp(path, "/") == 0, "program", "Refusing to delete root directory"); error(strcmp(path, "/") == 0, "program", "Refusing to delete root directory");
return nftw(path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS); return nftw(path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
} }
int openEditor(char *path, char *editor, int render, int shouldJumpToEndOfFile, int shouldDebug) { int openEditor(char *path, char *editor, int render, int shouldJumpToEndOfFile, int shouldDebug) {
// this ensures that ncurses won't affect the editor behaviour // this ensures that ncurses won't affect the editor behaviour
pid_t editor_pid = fork(); pid_t editor_pid = fork();
error(editor_pid < 0, "program", "fork() failed."); error(editor_pid < 0, "program", "fork() failed.");
if (editor_pid == 0) { // instead of reusing part of the codes, for any new editor copy an example and adapt it. This
// ========================= // is in case we need a custom fix for an editor.
// CHILD: launch editor
// =========================
// ---- NEOVIM / VIM ---- if (editor_pid == 0) {
if (strcmp(editor, "neovim") == 0 || strcmp(editor, "vim") == 0) { // =========================
const char *bin = (strcmp(editor, "neovim") == 0) ? "nvim" : "vim"; // CHILD: launch editor
// =========================
if (render) { // ---- NEOVIM / VIM ----
if (shouldJumpToEndOfFile) { if (strcmp(editor, "neovim") == 0 || strcmp(editor, "vim") == 0) {
debug("Running %s +:$ +:Vivify %s", bin, path); const char *bin = (strcmp(editor, "neovim") == 0) ? "nvim" : "vim";
execlp(bin, bin, "+:$", "+:Vivify", path, NULL);
} else {
debug("Running %s +:Vivify %s", bin, path);
execlp(bin, bin, "+:Vivify", path, NULL);
}
} else {
if (shouldJumpToEndOfFile) {
debug("Running %s +:$ %s", bin, path);
execlp(bin, bin, "+:$", path, NULL);
} else {
debug("Running %s %s", bin, path);
execlp(bin, bin, path, NULL);
}
}
error(1, "program", "execlp() failed."); if (render) {
} if (shouldJumpToEndOfFile) {
debug("Running %s +:$ +:Vivify %s", bin, path);
execlp(bin, bin, "+:$", "+:Vivify", path, NULL);
} else {
debug("Running %s +:Vivify %s", bin, path);
execlp(bin, bin, "+:Vivify", path, NULL);
}
} else {
if (shouldJumpToEndOfFile) {
debug("Running %s +:$ %s", bin, path);
execlp(bin, bin, "+:$", path, NULL);
} else {
debug("Running %s %s", bin, path);
execlp(bin, bin, path, NULL);
}
}
// ---- NANO ---- error(1, "program", "execlp() failed.");
else if (strcmp(editor, "nano") == 0) { // ---- MICRO ----
} else if (strcmp(editor, "micro") == 0) {
// If render enabled → spawn viv in parallel if (render) {
if (render) { if (shouldJumpToEndOfFile) {
debug("Running the editor..."); debug("Running MICRO_VIVIFY=1 micro %s +99999", path);
pid_t viv_pid = fork(); setenv("MICRO_VIVIFY", "1", 1);
error(viv_pid < 0, "program", "fork() failed."); execlp("micro", "micro", path, "+999999", NULL);
} else {
debug("Running MICRO_VIVIFY=1 micro %s", path);
setenv("MICRO_VIVIFY", "1", 1);
execlp("micro", "micro", path, NULL);
}
} else {
if (shouldJumpToEndOfFile) {
debug("Running micro %s +999999", path);
execlp("micro", "micro", path, "+999999", NULL);
} else {
debug("Running micro %s", path);
execlp("micro", "micro", path, NULL);
}
}
if (viv_pid == 0) { error(1, "program", "execlp() failed.");
// GRANDCHILD → viv
char viv_path[PATH_MAX];
strncpy(viv_path, path, PATH_MAX - 1);
viv_path[PATH_MAX - 1] = '\0';
if (shouldJumpToEndOfFile) {
strncat(viv_path, ":99999",
PATH_MAX - strlen(viv_path) - 1);
} }
debug("Running viv %s", viv_path); // ---- NANO ----
execlp("viv", "viv", viv_path, NULL); else if (strcmp(editor, "nano") == 0) {
error(1, "program", "execlp() failed.");
}
// IMPORTANT: do NOT wait for viv
}
// Now run nano (this replaces the child process) // If render enabled → spawn viv in parallel
if (shouldJumpToEndOfFile) { if (render) {
debug("Running nano + %s", path); debug("Running Vivify...");
execlp("nano", "nano", "+", path, NULL); pid_t viv_pid = fork();
} else { error(viv_pid < 0, "program", "fork() failed.");
debug("Running nano %s", path);
execlp("nano", "nano", path, NULL);
}
error(1, "program", "execlp() failed."); if (viv_pid == 0) {
} // GRANDCHILD → viv
// ---- UNKNOWN EDITOR ---- char viv_path[PATH_MAX];
else { strncpy(viv_path, path, PATH_MAX - 1);
error(1, "program", "Unknown editor."); viv_path[PATH_MAX - 1] = '\0';
}
if (shouldJumpToEndOfFile) {
strncat(viv_path, ":99999", PATH_MAX - strlen(viv_path) - 1);
}
debug("Running viv %s", viv_path);
execlp("viv", "viv", viv_path, NULL);
error(1, "program", "execlp() failed.");
}
// IMPORTANT: do NOT wait for viv
}
// Now run nano (this replaces the child process)
if (shouldJumpToEndOfFile) {
debug("Running nano + %s", path);
execlp("nano", "nano", "+", path, NULL);
} else {
debug("Running nano %s", path);
execlp("nano", "nano", path, NULL);
}
error(1, "program", "execlp() failed.");
// ---- HELIX ----
} else if (strcmp(editor, "helix") == 0) {
// If render enabled → spawn viv in parallel
if (render) {
debug("Running Vivify...");
pid_t viv_pid = fork();
error(viv_pid < 0, "program", "fork() failed.");
if (viv_pid == 0) {
// GRANDCHILD → viv
char viv_path[PATH_MAX];
strncpy(viv_path, path, PATH_MAX - 1);
viv_path[PATH_MAX - 1] = '\0';
if (shouldJumpToEndOfFile) {
strncat(viv_path, ":99999", PATH_MAX - strlen(viv_path) - 1);
}
debug("Running viv %s", viv_path);
execlp("viv", "viv", viv_path, NULL);
error(1, "program", "execlp() failed.");
}
// IMPORTANT: do NOT wait for viv
}
// Now run helix (this replaces the child process)
if (shouldJumpToEndOfFile) {
strncat(path, ":99999", PATH_MAX);
debug("Running hx %s", path);
execlp("hx", "hx", path, NULL);
} else {
debug("Running hx %s", path);
execlp("hx", "hx", path, NULL);
}
error(1, "program", "execlp() failed.");
}
// ---- kakoune ----
else if (strcmp(editor, "kakoune") == 0) {
// If render enabled → spawn viv in parallel
if (render) {
debug("Running Vivify...");
pid_t viv_pid = fork();
error(viv_pid < 0, "program", "fork() failed.");
if (viv_pid == 0) {
// GRANDCHILD → viv
char viv_path[PATH_MAX];
strncpy(viv_path, path, PATH_MAX - 1);
viv_path[PATH_MAX - 1] = '\0';
if (shouldJumpToEndOfFile) {
strncat(viv_path, ":99999", PATH_MAX - strlen(viv_path) - 1);
}
debug("Running viv %s", viv_path);
execlp("viv", "viv", viv_path, NULL);
error(1, "program", "execlp() failed.");
}
// IMPORTANT: do NOT wait for viv
}
// Now run nano (this replaces the child process)
if (shouldJumpToEndOfFile) {
debug("Running kak + %s", path);
execlp("kak", "kak", "+", path, NULL);
} else {
debug("Running kak %s", path);
execlp("kak", "kak", path, NULL);
}
error(1, "program", "execlp() failed.");
// ---- VI ----
} else if (strcmp(editor, "vi") == 0) {
// If render enabled → spawn viv in parallel
if (render) {
debug("Running Vivify...");
pid_t viv_pid = fork();
error(viv_pid < 0, "program", "fork() failed.");
if (viv_pid == 0) {
// GRANDCHILD → viv
char viv_path[PATH_MAX];
strncpy(viv_path, path, PATH_MAX - 1);
viv_path[PATH_MAX - 1] = '\0';
if (shouldJumpToEndOfFile) {
strncat(viv_path, ":99999", PATH_MAX - strlen(viv_path) - 1);
}
debug("Running viv %s", viv_path);
execlp("viv", "viv", viv_path, NULL);
error(1, "program", "execlp() failed.");
}
// IMPORTANT: do NOT wait for viv
}
// Now run vi (this replaces the child process)
if (shouldJumpToEndOfFile) {
debug("Running vi +:$ %s", path);
execlp("vi", "vi", "+:$", path, NULL);
} else {
debug("Running vi %s", path);
execlp("vi", "vi", path, NULL);
}
error(1, "program", "execlp() failed.");
// ---- Jed ----
} else if (strcmp(editor, "jed") == 0) {
// If render enabled → spawn viv in parallel
if (render) {
debug("Running Vivify...");
pid_t viv_pid = fork();
error(viv_pid < 0, "program", "fork() failed.");
if (viv_pid == 0) {
// GRANDCHILD → viv
char viv_path[PATH_MAX];
strncpy(viv_path, path, PATH_MAX - 1);
viv_path[PATH_MAX - 1] = '\0';
if (shouldJumpToEndOfFile) {
strncat(viv_path, ":99999", PATH_MAX - strlen(viv_path) - 1);
}
debug("Running viv %s", viv_path);
execlp("viv", "viv", viv_path, NULL);
error(1, "program", "execlp() failed.");
}
// IMPORTANT: do NOT wait for viv
}
// Now run jed (this replaces the child process)
if (shouldJumpToEndOfFile) {
debug("Running jed %s -g 99999999", path);
execlp("jed", "jed", path, "-g", "99999999", NULL);
} else {
debug("Running jed %s", path);
execlp("jed", "jed", path, NULL);
}
error(1, "program", "execlp() failed.");
// ---- UNKNOWN EDITOR ----
} else {
error(1, "program", "Unknown editor.");
}
} }
// ========================= // =========================
// PARENT: wait ONLY editor // PARENT: wait ONLY editor
// ========================= // =========================
int status; int status;
while (waitpid(editor_pid, &status, 0) == -1) { // we can't just use waitpid(). Because when resizing the terminal, waitpid() is returned so we loop to see if there is not a problem while (waitpid(editor_pid, &status, 0) == -1) { // we can't just use waitpid(). Because when resizing the terminal, waitpid() is
if (errno != EINTR) { // returned so we loop to see if there is not a problem
perror("waitpid"); if (errno != EINTR) {
break; perror("waitpid");
} break;
}
} }
return 0; return 0;
} }
+37 -31
View File
@@ -1,52 +1,53 @@
#ifndef UTILS_H #ifndef UTILS_H
#define UTILS_H #define UTILS_H
#include <cjson/cJSON.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <ftw.h>
#include <limits.h>
#include <ncurses.h>
#include <pwd.h>
#include <regex.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <dirent.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h>
#include <ftw.h>
#include <pwd.h>
#include <ncurses.h>
#include <ctype.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <limits.h>
#include <cjson/cJSON.h>
#include <regex.h>
#include <errno.h>
#include <time.h> #include <time.h>
#include <ftw.h> #include <unistd.h>
#define BUFFER_SIZE 256 //standard buffer size. #ifndef VERSION
// the three supported values are "daily" "weekly" and "monthly" (for months we use the avarage lenght of a month in a non leap year). All values were calculated on my loyal TI-30 ECO RS #define VERSION "dev"
#endif
#define BUFFER_SIZE 256 // standard buffer size.
// the three supported values are "daily" "weekly" and "monthly" (for months we use the avarage
// lenght of a month in a non leap year). All values were calculated on my loyal TI-30 ECO RS
#define DAILY 86400 #define DAILY 86400
#define WEEKLY 604800 #define WEEKLY 604800
#define MONTHLY 2628000 // calculated from the average lenght of a non leap year #define MONTHLY 2628000 // calculated from the average lenght of a non leap year
#define debug(message, ...) \ #define debug(message, ...) _debug(shouldDebug, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__)
_debug(shouldDebug, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__) #define altDebug(message, ...) _altDebug(shouldDebug, message, ##__VA_ARGS__)
#define altDebug(message, ...) \ #define error(condition, type, message, ...) _error(shouldDebug, condition, type, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__)
_altDebug(shouldDebug, message, ##__VA_ARGS__)
#define error(condition, type, message, ...) \
_error(shouldDebug, condition, type, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__)
// you must edit this two values if you want to add suport for an editor // 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 char *supportedEditor[]; // array of supported editors
extern const int numEditors; // number of supported editors extern const int numEditors; // number of supported editors
// compares two strings alphabetically.
//compares two strings alphabetically. // this function is used for qsort
//this function is used for qsort
int compareString(const void *a, const void *b); int compareString(const void *a, const void *b);
// compares two strings in reversed alphabetical order // compares two strings in reversed alphabetical order
// this function is used for qsort // this function is used for qsort
int reverseCompareString(const void *a, const void *b); int reverseCompareString(const void *a, const void *b);
// checks if editor is supported and if it installed.
// this basically checks all the dirs from your path for the editor. This is a safety check. // this basically checks all the dirs from your path for the editor. This is a safety check.
// If the executable from an editor is not the editor name (for example neovim and nvim), you must handle at the start of the function. // If the executable from an editor is not the editor name (for example neovim and nvim), you must
int doesEditorExist(char *editorToCheck, int debug); // handle at the start of the function. This can return an error and stop the program.
int isEditorValid(char *editorToCheck, int useDefaultEditor, int debug);
// please use the macro debug instead of _debug. // please use the macro debug instead of _debug.
//formated debugging. // formated debugging.
void _debug(const int d, const char *file, const int line, const char *function, const char *message, ...); void _debug(const int d, const char *file, const int line, const char *function, const char *message, ...);
//use for less formal debuggin. (usefull if enumerating or making a list). // use for less formal debuggin. (usefull if enumerating or making a list).
void _altDebug(const int d, const char *message, ...); void _altDebug(const int d, const char *message, ...);
// formated error. // 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, ...); void _error(const int shouldDebug, const int condition, const char *type, const char *file, const int line, const char *function, const char *message, ...);
@@ -63,8 +64,8 @@ void appendToFile(const char *path, const char *string, const int shouldDebug);
// replace unwanted chars by '_'. // replace unwanted chars by '_'.
// '.' is replaced if it is only the first two chars // '.' is replaced if it is only the first two chars
void sanitize(char *string); void sanitize(char *string);
//from https://stackoverflow.com/a/5467788. // from https://stackoverflow.com/a/5467788.
//deletes an entire directory. Use with parsimony and carefullness. // deletes an entire directory. Use with parsimony and carefullness.
int rmrf(char *path, int shouldDebug); int rmrf(char *path, int shouldDebug);
// Inputs are the path to the file, the editor to open and some rendering option. // Inputs are the path to the file, the editor to open and some rendering option.
// render: if we render the .md file with Vivify. // render: if we render the .md file with Vivify.
@@ -77,6 +78,11 @@ int openEditor(char *path, char *editor, int render, int shouldJumpToEndOfFile,
char *getFormatedTime(char *format, int shouldDebug); char *getFormatedTime(char *format, int shouldDebug);
/* Caclulates if we need to do another backup (by reading ~/.cache/NoteWrapper/backupTime.txt. /* Caclulates if we need to do another backup (by reading ~/.cache/NoteWrapper/backupTime.txt.
If need launches in the background rsync to do the backuping. If need launches in the background rsync to do the backuping.
rsyncArgs is the array of arguments to be passed to rsync. Do not inclue destination or source. It will be added in the function.*/ Each pair of source/destination will launch one rsync process.
void handleBackups(const char *pathOfVaults, const char *pathOfBackup, const char *homeDir, const int interval, const char **rsyncArgs, const int rsyncArgsNumber, const int shouldDebug); If a pair don't need to be backed up, the destination should be set to NULL.
rsyncArgs is the array of arguments to be passed to rsync. Do not inclue destination or source. It
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);
#endif #endif