- Lua 98.7%
- Makefile 0.8%
- Shell 0.5%
En visuel, <leader>tn crée l'entité depuis les mots sélectionnés : titre proposé depuis la sélection, slug depuis le titre, puis la sélection est remplacée par [libellé](slug.md) dans le buffer. La note est créée avant que le lien soit posé : renoncer laisse la phrase intacte. On propose ensuite d'ouvrir la note à côté, « non » par défaut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| demo | ||
| docs | ||
| lua/trace | ||
| plugin | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
trace.nvim
Tracer : poser une ligne datée qui dit ce qui s'est passé.
Paquet Neovim pour le système de notes spécifié dans docs/cahier-des-charges.md.
Le système est un dossier de fichiers markdown ; le paquet n'est qu'un éditeur
et quelques commandes. Aucune dépendance ne possède les données (N1) : si
tout est désinstallé demain, les notes restent lisibles avec rg et n'importe
quel éditeur.
Installation
Le paquet est hébergé sur https://forge.opytex.org/benjamin/trace.nvim, pas
sur GitHub : on donne donc l'URL complète plutôt qu'un user/repo.
-- lazy.nvim
{
url = "https://forge.opytex.org/benjamin/trace.nvim",
opts = {
root = "~/notes", -- obligatoire : la racine du corpus, jamais devinée
-- keymaps = false, -- pour ne pas poser les raccourcis <leader>t*
-- editor_opts = false, -- pour ne pas toucher suffixesadd/path/wrap
},
}
-- packer.nvim
use({
"https://forge.opytex.org/benjamin/trace.nvim",
config = function()
require("trace").setup({ root = "~/notes" })
end,
})
Pour travailler sur une copie locale du dépôt, remplacer url = par
dir = "/chemin/vers/trace.nvim".
La racine est explicite. Rien n'est cherché, rien n'est deviné : le paquet prévient au démarrage si elle manque ou n'existe pas, et refuse d'écrire.
Les opérations
| # | Commande | Raccourci | Ce qu'elle fait |
|---|---|---|---|
| O1 | :TraceEntry [texte] |
<leader>te |
Pose l'amorce datée dans le journal et vous y met en insertion, dans le buffer. Avec un texte en argument, écrit sans rien demander. |
| O2 | — | @ en insertion |
Propose les entités par titre, slug et alias, et insère [Libellé](slug.md). |
| — | :TraceInbox [texte] |
<leader>ti |
Jette une ligne datée dans inbox.md, depuis n'importe où, dans une fenêtre verticale à droite. |
| O3 | :TraceNew [kind] |
<leader>tn |
Crée une entité : trois questions — type, titre, slug proposé. En visuel, les mots sélectionnés deviennent le lien et la note qu'il cite. |
| O4 | :TraceOpen [slug] |
<leader>to |
Ouvre une entité par nom, titre ou alias. |
| O5 | :TraceGrep [motif] |
<leader>tg |
Recherche plein texte, à la frappe avec snacks ou telescope, sinon rg + quickfix. |
| O6 | :TraceBacklinks [slug] |
<leader>tb |
Tout ce qui cite une entité, agrégé et groupé par dossier, antéchronologique à l'intérieur de chacun, en quickfix. Suit le lien sous le curseur. |
| O7 | :TraceAlive |
<leader>ta |
Projets vivants, du plus figé au plus récent, en quickfix. |
| O8 | :TraceSince {AAAA[-MM]} |
<leader>ts |
Les entrées d'une période, le mois courant proposé, groupées par dossier, en quickfix. |
| O9 | :TraceClose [slug] |
<leader>tC |
Clôt un projet à la date du jour, après confirmation. Refuse de clore deux fois. |
| O10 | :TraceAsset [chemin] |
<leader>tA |
Copie une pièce dans le dossier de l'entité du buffer, la date, et insère son lien à la position du curseur —  pour une image, [](…) sinon — crochets vides, en insertion, pour dire ce qu'elle montre. Sans argument, propose les pièces récemment téléchargées ou capturées. |
| O11 | :TraceReveal |
<leader>tr |
Ouvre en externe la pièce sous le curseur, le dossier de l'entité citée par le lien sous le curseur, sinon celui de l'entité courante. |
| — | :TraceFollow |
gf, gx, <leader>tf |
Suit ce qui est sous le curseur : note dans nvim, pièce et URL dehors, note manquante en création. |
| O12 | :TraceIndex |
— | Reconstruit l'index en mémoire. |
| O13 | :TraceCheck |
<leader>tc |
Vérifie les invariants : diagnostics dans les buffers ouverts, liste complète en quickfix. |
Hors du corpus, chaque commande demande l'entité ; à l'intérieur d'une note, elle travaille sur celle-ci sans rien demander — sauf si l'opération ne sait pas la traiter (clore une personne), auquel cas elle demande aussi.
Deux règles suffisent à retrouver une touche : la lettre est l'initiale du nom
anglais de la commande, et la majuscule signale l'irréversible — tC
scelle un projet. tA la porte parce que ta est déjà pris par les projets
vivants : depuis qu'elle copie au lieu de déplacer, O10 ne détruit plus rien.
On écrit toujours dans le buffer, jamais dans une invite. Une invite de
saisie n'a ni complétion, ni abréviations, ni annulation : O1 et :TraceInbox
posent donc la ligne datée et vous y déposent en insertion, et O10 son lien à
l'endroit du curseur. Rien de tout cela n'est écrit sur le disque : c'est votre
:w qui décide.
Créer depuis la phrase
On écrit « vu avec Michel Bernard rue des Écoles », on sélectionne
Michel Bernard avec v, puis <leader>tn :
- le type, puis le titre — proposé depuis la sélection, et modifiable : « Michel » dans la phrase peut devenir « Bernard Michel » sur la fiche ;
- le slug, proposé depuis le titre corrigé, modifiable comme toujours ;
- la note est créée, et la sélection devient
[Michel Bernard](bernard-michel.md)— le libellé reste celui de la phrase ; - « Ouvrir la note à côté ? » : oui l'ouvre dans une fenêtre verticale, sur la synthèse ; non — la réponse par défaut — vous laisse dans la phrase.
La note d'abord, le lien ensuite : si on renonce en route, la phrase reste
intacte. Le lien n'est posé que dans le buffer, c'est :w qui décide.
Seule une sélection au caractère près, sur une ligne, est acceptée : un lien
ne se coupe pas (I4), et une sélection qui touche un lien existant est
refusée.
Suivre un lien, une pièce, une URL
Dans le corpus — et nulle part ailleurs — gf et gx passent par
:TraceFollow, qui regarde ce qu'il a sous le curseur :
- une note (
[Michel](bernard-michel.md)) s'ouvre dans nvim ; - une pièce s'ouvre avec l'application du système, via
vim.ui.open— un plan ou un PDF se lisent en grand, hors du terminal (§8). Qu'elle soit citée par un lien (,[](20270115_devis-colas.pdf)) ou en texte nu au fil de la phrase, les deux formes se suivent, y compris le curseur posé sur le!de l'image ; - une URL ouvre le navigateur ;
- un lien vers une note qui n'existe pas encore propose de la créer, et ne demande que le type : le slug vient du lien, le titre de son libellé.
C'est ce dernier point qui rend tenable « on lie librement, on crée plus tard » : un lien peut précéder sa fiche sans jamais produire de fiche vide.
Un mot ordinaire de la phrase, lui, ne nomme rien : gf dessus ne propose
pas de créer une entité « signalée ». Seuls un lien, un slug.md écrit en
toutes lettres, ou un mot qui porte déjà le nom d'une entité sont des cibles.
follow = false garde gf strictement natif.
Options
opts = {
root = "~/notes", -- obligatoire
picker = "auto", -- "snacks" | "telescope" | "native"
follow = true, -- gf / gx suivent notes, pièces et URLs dans le corpus
first_entry = "Ouverture du dossier.", -- false pour ne pas dater les notes neuves
inbox = "inbox.md",
asset_source = nil, -- dossiers d'où arrivent les pièces (O10)
keymaps = true,
completion = true,
editor_opts = true,
}
picker = "auto" prend snacks.picker s'il est là, telescope sinon, et retombe
sur vim.ui.select — qui ne demande rien d'installé.
Une règle départage les deux interfaces : on choisit → picker ; on parcourt
→ quickfix. Choisir une entité (O4), une pièce (O10) ou chercher à la frappe
(O5) passe par le picker. Les listes de lignes — rétroliens (O6), projets
vivants (O7), période (O8) — vont toujours en quickfix, même avec snacks
installé : une entrée tient sur une seule ligne physique (I4), donc la ligne
porte déjà le fait entier et un aperçu n'ajoute rien ; et la quickfix persiste,
se reparcourt avec ]q / [q et se traite en masse avec :cdo. C'est une
liste de travail, pas une sélection. Elle affiche slug │ texte, sans répéter
le chemin qu'elle montre déjà.
O6 et O8 groupent ces lignes par entité porteuse : les dossiers sont rangés du plus récemment actif au plus ancien, et à l'intérieur de chacun l'ordre reste antéchronologique, mentions sans date en queue de groupe. Trié par date seule, un dossier se retrouve éparpillé dans toute la liste et on lit une chronologie de la commune ; groupé, on lit l'histoire de chaque dossier. Le format ne change pas : c'est le tri qui groupe, pas un en-tête.
:TraceAsset accepte un chemin qui contient des espaces : tout l'argument est
le chemin, il n'y a rien d'autre à y lire. L'entité est celle du buffer, et
seulement elle — un slug donné à part écrirait le lien dans une note et le
fichier dans un autre dossier. Hors de la note d'une entité, la commande
refuse et le dit : le curseur doit être là où la pièce doit apparaître.
asset_source est la liste des dossiers où <leader>tA va chercher les pièces
à attacher : leurs fichiers non-markdown de premier niveau, du plus récent au
plus ancien — une pièce vient presque toujours d'être téléchargée ou capturée.
Par défaut, les dossiers de l'utilisateur donnés par xdg-user-dir
(DOWNLOAD, PICTURES, DESKTOP — soit ~/Téléchargements, ~/Images,
~/Bureau sur une machine française), puis $HOME. La question est posée une
seule fois, au setup(). La dernière entrée de la liste reste « saisir un
chemin… », et :TraceAsset <chemin> ne demande rien.
Les invariants
:TraceCheck est le contrat du système — et celui de tout agent qui écrirait
dans le corpus (N9). Les dix invariants du cahier des charges §3.5 sont
vérifiés, I10 excepté : un renommage passé ne se voit que sous la forme d'un
lien mort (I7).
Erreurs : I1 (une note par dossier, homonyme), I2 (unicité des slugs), I3
(date sur les projets seulement), I4 (une entrée = une ligne datée), I5
(journal antéchronologique), I6 (closed:), I9 (ni tag ni sous-dossier).
Avertissements : I7 (lien vers une entité à créer), I8 (pièce que sa note ne
cite nulle part).
Complétion des liens (O2)
@ en insertion ouvre la liste des entités, filtrée sur le titre, le slug et
les alias, et insère [Libellé](slug.md). La source est déclarée auprès de
blink.cmp au setup() — rien à ajouter à ta configuration blink. Pour la
déclarer à la main :
sources = {
per_filetype = { markdown = { inherit_defaults = true, "trace" } },
providers = { trace = { name = "trace", module = "trace.cmp" } },
}
Sans blink, le repli est natif : completefunc et @ qui déclenche
<C-x><C-u>. completion = false dans les options coupe tout.
Équivalents shell
N5 demande que chaque opération ait un équivalent portable documenté : le voici. Il fonctionne sans le paquet, sans nvim, depuis la racine du corpus — c'est le test de N1, « si je désinstalle tout demain ».
# O7 — projets vivants, du plus figé au plus récent
for f in projets/*/*.md; do grep -q '^closed:' "$f" && continue
printf '%s %s\n' "$(grep -om1 '^- [0-9-]\{10\}' "$f" | sed 's/^- //')" \
"$(basename "$f" .md)"; done | sort
# O6 — tout ce qui cite une entité, du plus récent au plus ancien
# (le paquet, lui, groupe ces mêmes lignes par dossier ; ajouter le nom de
# fichier et trier dessus suffit à retrouver le groupement à la main)
rg --no-filename -N 'bernard-michel' . | grep '^- 2' | sort -r
rg -l 'bernard-michel' . # juste les fichiers qui la citent
# O8 — les entrées d'une période (2026, 2026-10, un jour entier)
rg --no-filename -N '^- 2026-10' . | sort -r
# O4 — ouvrir une entité, sans connaître son slug
rg --files -g '*.md' . | fzf | xargs -r $EDITOR
# O5 — recherche plein texte
rg --smart-case 'DETR' .
# O13 — les pièces que leur note ne cite nulle part (I8), le plus coûteux à la main
for a in */*/[0-9]*_*; do case "$a" in *.md) continue;; esac
grep -qr "$(basename "$a")" . || echo "pièce orpheline : $a"; done
Les opérations d'écriture n'ont pas de ligne de shell, et c'est voulu :
leur équivalent est ouvrir le fichier et taper la ligne. Tracer (O1), créer
une entité (O3), clore (O9) ou attacher une pièce (O10) ne font rien qu'une
main ne puisse faire — une ligne - AAAA-MM-JJ — … en tête du journal, un
dossier et son <slug>.md, une ligne closed: AAAA-MM-JJ sous le titre, un
cp vers AAAAMMJJ_nom.ext et un  là où l'on veut le voir. Le paquet évite la
faute de frappe et la date à retaper ; il ne détient aucun geste.
Découvrir
docs/walkthrough.md — une visite guidée de vingt minutes
dans le bac à sable : un signalement trié, lié, devenu projet, photographié,
clos, et un corpus cassé exprès pour voir ce que :TraceCheck en dit.
Développement
make test # toute la suite, en headless (plenary)
make test FILE=tests/spec/log_spec.lua
make check # les invariants du corpus de référence
make demo # bac à sable : nvim isolé sur un corpus jetable
make demo-reset # repartir d'un corpus neuf
make demo ne touche ni la configuration de l'utilisateur ni ses vraies
notes. Le script isole XDG_* dans demo/, laisse lazy y installer ses
propres dépendances, copie le corpus de référence dans demo/notes/ et monte
le paquet depuis le dépôt (dir =). On peut y casser ce qu'on veut.
lua/trace/
config.lua racine, kinds, flags
util.lua chemins, lecture, écriture (buffer ou disque)
slug.lua slugs, préfixes de date, noms de pièces
entity.lua parsing d'une note, création, clôture
index.lua scan du corpus, cache mémoire (N2 : rien sur disque)
log.lua entrées de journal, pièces
search.lua rétroliens, périodes
check.lua invariants
ui.lua vim.ui.*, quickfix, ouverture externe
follow.lua ce que « suivre » veut dire selon la cible
pick.lua sélection et listes : snacks, telescope ou natif
api.lua les opérations O1 → O13
plugin/trace.lua commandes :Trace*
tests/corpus/ corpus de référence, conforme aux invariants
Chaque module est une fonction pure de ses entrées là où c'est possible :
entity.parse, log.insert_entry, check.check_file et slug.* se testent
sans disque ni interface.
État
Fait : O1 → O13, sur vim.ui.* et la liste de quickfix — donc sans aucune
dépendance obligatoire. snacks ou telescope, s'ils sont présents, habillent
les choix ; les listes de lignes restent en quickfix dans tous les cas.
blink.cmp porte O2 et se replie sur completefunc.
Reste : O14 (trace-log, le script shell de capture hors nvim) et
l'intégration nvim-lint pour que O13 devienne permanent plutôt que demandé.