Run (org-babel-tangle)
(C-c C-v t) to get the code placed in that path.
Notes Query
A module to perform a Search function on this personal wiki via Emacs.
Inspired on
+Using
Using
Definitions
(setq smp/nq--python-interpreter "python3")
(setq smp/nq--python-module (file-truename "~/repos/codigoparallevar/scripts/notes-query"))
(setq smp/nq--sqlitefts-index (file-truename "~/.cache/plibrarian/db.sqlite3"))
Run (org-babel-tangle)
(C-c C-v t) to get the code placed in that path.
Python code
This module makes calls to a small python wrapper for SQLite FTS5.
#!/usr/bin/env python3
import sqlite3
import sys
import os
import json
def main(mode, path, query):
db = sqlite3.connect(path)
cur = db.cursor()
cur.execute('SELECT note_id, title, top_level_title, is_done, is_todo FROM note_search(?);', (query,))
matches = cur.fetchall()
if mode == 'json':
results = []
for rank, (note_id, title, top_level_title, is_done, is_todo) in enumerate(matches):
data = dict(rank=rank + 1,
note_id=note_id,
title=title,
top_level_title=top_level_title,
is_done=bool(is_done),
is_todo=bool(is_todo),
) # , percent=match.percent)
results.append(data)
print(json.dumps(results))
elif mode == 'org':
doc_cache = {}
for rank, match in enumerate(matches):
print(f"{rank + 1:2d}) [[id:{match[0]}][{match[1].strip()} ({match[2]})]]")
if __name__ == '__main__':
if len(sys.argv) != 4 or sys.argv[1] not in ('json', 'org'):
print("Notes-Query")
print("Usage: {} <json|org> <path> <query>".format(sys.argv[0]))
exit(0)
main(sys.argv[1], sys.argv[2], sys.argv[3])
This code takes a mode (org) as first argument, xapian dir as second, and a query as third.
Emacs code
(defun smp/search-notes (query)
(interactive "sQuery? ")
(let ((process "*notes-query*")
(buffer "*notes-query*"))
(switch-to-buffer buffer)
(read-only-mode -1)
(erase-buffer)
(insert (concat "* " query "\n"))
(backward-page)
(start-process process buffer
smp/nq--python-interpreter
smp/nq--python-module
"org"
smp/nq--sqlitefts-index
query)
(org-mode)
(read-only-mode 1)))
: smp/search-notes
Index generator code
#!/usr/bin/env python3
import os
import sys
import collections
import logging
import json
from datetime import datetime
import sqlite3
import shutil
import inotify.adapters
import org_rw
import xdg
Config = collections.namedtuple('Config', ['path'])
BASE_DIR = os.path.join(xdg.XDG_CONFIG_HOME, "plibrarian")
CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
DB_PATH = os.path.join(xdg.XDG_CACHE_HOME, "plibrarian", "db.sqlite3")
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
'IN_CLOSE_WRITE',
'IN_DELETE',
'IN_MOVED_FROM',
'IN_MOVED_TO',
'IN_DELETE_SELF',
'IN_MOVE_SELF',
)
def create_db(path):
if os.path.exists(path):
os.unlink(path)
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path), exist_ok=True)
db = sqlite3.connect(path)
db.execute('CREATE VIRTUAL TABLE note_search USING fts5(note_id, title, body, doc_id, top_level_title, is_done, is_todo, tokenize="trigram");')
return db
def add_to_index(doc, cur):
for hl in doc.getAllHeadlines():
topLevelHeadline = hl
while isinstance(topLevelHeadline.parent, org_rw.Headline):
topLevelHeadline = topLevelHeadline.parent
cur.execute('''INSERT INTO note_search(note_id, title, body, doc_id, top_level_title, is_done, is_todo) VALUES (?, ?, ?, ?, ?, ?, ?);''',
(
hl.id,
hl.title.get_text().strip(),
'\n'.join(hl.doc.dump_headline(hl, recursive=False)),
hl.doc.id,
topLevelHeadline.title.get_text().strip(),
hl.is_done,
hl.is_todo,
))
def load_config():
if not os.path.exists(CONFIG_PATH):
return None
with open(CONFIG_PATH, 'rt') as f:
data = json.load(f)
return Config(path=os.path.expanduser(data['path']))
def load_all(top_dir_abs):
top = top_dir_abs
docs = {}
for root, dirs, files in os.walk(top):
for name in files:
if ".org" not in name:
continue
path = os.path.join(root, name)
try:
doc = org_rw.load(open(path), extra_cautious=True)
docs[path] = doc
except Exception as err:
import traceback
traceback.print_exc()
print(f"== On {path}")
sys.exit(1)
return docs
def gen_initial(docs):
db = create_db(DB_PATH)
cur = db.cursor()
for path, doc in docs.items():
changed = False
add_to_index(doc, cur)
cur.close()
db.commit()
return db
def update_index(docs, fpath, db):
if os.path.exists(fpath):
try:
with open(fpath, 'rt') as f:
doc = org_rw.load(f, extra_cautious=True)
except org_rw.NonReproducibleDocument:
logging.warning("Error loading document")
return
else:
doc = None
docs[fpath] = doc
# Delete old info
cur = db.cursor()
if doc.id is not None:
cur.execute('DELETE FROM note_search WHERE doc_id = ?;', (doc.id,))
logging.debug("Deleted docid: {}".format(doc.id))
# Create new info
add_to_index(doc, cur)
else:
cur.execute('DELETE FROM note_search;')
for path, doc in docs.items():
changed = False
add_to_index(doc, cur)
cur.close()
db.commit()
def event_loop(notifier, docs, db):
file_changes = set()
for event in notifier.event_gen(yield_nones=True):
if event is None:
if len(file_changes):
for fpath in file_changes:
logging.info("Reloading {}".format(fpath))
update_index(docs, fpath, db)
file_changes = set()
## Commit all info
# Xapian
db.commit()
continue
(ev, types, directory, file) = event
if not any([type in MONITORED_EVENT_TYPES for type in types]):
continue
filepath = os.path.join(directory, file)
if '/.git/' in filepath or os.path.basename(filepath).startswith('.'):
# Ignoring GIT or hidden files
continue
# Mark path to update when next "commit" comes
file_changes.add(filepath)
def main():
config = load_config()
if config is None:
logging.error("Configuration not found on: {}".format(CONFIG_PATH))
exit(0)
notifier = inotify.adapters.InotifyTree(config.path)
logging.info("Loading files...")
docs = load_all(config.path)
logging.info("{} files loaded".format(len(docs)))
logging.info("Creating index...")
db = gen_initial(docs)
logging.info("Index created")
logging.info("Starting event loop")
event_loop(notifier, docs, db)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
main()
Dependencies
pip install inotify
pip install git+https://code.codigoparallevar.com/kenkeiras/org-rw@dev/render-as-dom
Config
{
"path": "~/.logs/brain"
}