oldpyn/src/pyn/note.py
2025-11-05 08:30:00 +11:00

176 lines
5.1 KiB
Python

from datetime import datetime
import pathlib
import re
import os
import logging
import subprocess
import click
import sys
"""
try:
import gnureadline as readline
except:
import readline
"""
logger = logging.getLogger(__name__)
DEFAULTS = {
"note_directory": "~/Documents/notes",
"template": "basic.md",
"filename_format": "{date}-{title}.{ext}",
"filepath_format": "{date.year}/{date.month}/{filename}",
"date_format": "%Y-%m-%d",
}
DEFAULT_TEMPLATE = """---
date: {datetime}
notebook: {notebook}
---
# {title}
"""
def fuzzymatch(query, values):
# tokenise
tokens = re.split(r"\W", query.lower())
logger.debug("Tokens: %r", tokens)
# create a none-greedy matcher
matcher = re.compile(".*".join(tokens))
matches = []
for x in values:
m = matcher.search(x.lower())
if m:
start, end = m.span()
# sort by length ascending, start pos ascending but reverse filename for dates
matches.append((end - start, start, x))
return [x for _, _, x in sorted(matches, reverse=True)]
def slugify(s):
s = s.lower().strip()
s = re.sub(r"[^\w\s-]", "", s)
s = re.sub(r"[\s_-]+", "-", s)
s = re.sub(r"^-+|-+$", "", s)
return s
class PyNote(object):
def __init__(self, config):
self.config = {}
self.config.update(DEFAULTS)
self.config.update(config)
self.directory = pathlib.Path(self.config["note_directory"]).expanduser()
self.directory.mkdir(exist_ok=True)
self._l = len(str(self.directory)) + 1
self.has_git = (self.directory / ".git").exists()
def filename(self, title: str, date=None, ext="md"):
date = date or datetime.now()
date_str = date.strftime(self.config["date_format"])
return self.config["filename_format"].format(
date=date_str, title=slugify(title), ext=ext
)
def filepath(self, filename: str, notebook=None):
fp = self.directory
if notebook:
fp = fp / notebook
fp.mkdir(exist_ok=True)
return fp / filename
def relative_path(self, filename: str) -> str:
return str(filename)[self._l :]
def markdown_file(self, notebook: str, title: str, date=None, create=False):
date = date or datetime.now()
filename = self.filename(title, date)
filepath = self.filepath(filename, notebook)
if not filepath.exists() and create:
with open(filepath, "w") as f:
f.write(
DEFAULT_TEMPLATE.format(
datetime=date.isoformat(timespec="seconds"),
notebook=notebook,
title=title.title(),
)
)
return str(self.directory / filepath)
def files(self, suffix=None):
result = []
c = len(str(self.directory)) + 1
for dirpath, dirnames, filenames in self.directory.walk(top_down=True):
prefix = str(dirpath)[c:]
for d in [x for x in dirnames if x.startswith(".")]:
dirnames.remove(d)
if suffix:
filenames = [x for x in filenames if x.endswith(suffix)]
result.extend([os.path.join(prefix, x) for x in filenames])
return result
def find(self, query: str, suffix=None) -> list[str]:
# print(process.extract(" ".join(query), self.files()))
logger.debug("find: %s [%s]", query, suffix)
result = list(fuzzymatch(query, self.files(suffix)))
return result
def select(self, query: str, suffix=None, entry=None) -> str:
logger.info("Select: %s", query)
options = self.find(query, suffix)
if not options:
raise click.BadParameter(f"No matches for '{query}'")
if len(options) == 1:
entry = 1
else:
sys.stderr.write(f"\nMultiple notes for '{query}'\n\n")
for i, option in enumerate(options):
sys.stderr.write(f"{i + 1:2d}: {option}\n")
selected = input("\nItem: ")
if not selected:
raise click.Abort()
entry = int(selected)
item = options[entry - 1]
sys.stderr.write(f"{item}\n\n")
return str(self.directory / item)
def search(self, terms: list[str]):
cmd = ["grep", "-irn", str(self.directory), "--exclude-dir", ".git"]
for term in terms:
cmd.append("-e")
cmd.append(term)
p = subprocess.run(cmd, cwd=self.directory, capture_output=True, check=True)
for line in p.stdout.decode("utf-8").strip().split("\n"):
file, lineno, snippet = line.split(":")
notebook, _, filename = self.relative_path(file).rpartition("/")
yield notebook, filename, int(lineno), snippet
def git_cmd(self, args: list[str]) -> None:
if not self.has_git:
logger.debug("Skipping git command: %r", args)
return
cmd = ["git", "-C", str(self.directory)]
cmd.extend(args)
logger.info("Executing: %r", cmd)
subprocess.check_call(cmd)