Compare commits

...
3 Commits
Author SHA1 Message Date
tris af14c80803 Added templating for render 2026-07-08 12:25:22 +10:00
tris 44549aaff3 Added basic rendering support and some cleanup 2026-01-29 09:37:15 +11:00
tris e9cc2cc3d0 Minor changes 2025-12-04 09:19:05 +11:00
7 changed files with 151 additions and 11 deletions
+9
View File
@@ -0,0 +1,9 @@
# PyN
CLI tool for manipulating text notes
### Git support
If the notes directory is under git control then modifications will be auto staged.
You can then execute the `pyn sync` command to pull and push from the upstream server.
+54 -5
View File
@@ -1,6 +1,6 @@
from pyn.common import parser, logger from pyn.common import parser, logger
from pyn.config import Config from pyn.config import Config
from pyn import notes from pyn import notes, templating
import sys import sys
import os import os
@@ -29,6 +29,10 @@ def cmd_add_note(config, title, notebook):
if not body: if not body:
notes.edit_file(config, filename) notes.edit_file(config, filename)
notes.run_git(
config, ["add", str(filename.relative_to(config.directory))], quiet=True
)
add_note = group.add_parser("add", aliases=["a"]) add_note = group.add_parser("add", aliases=["a"])
add_note.add_argument("--notebook", "-n", help="add to this notebook [general]") add_note.add_argument("--notebook", "-n", help="add to this notebook [general]")
@@ -53,7 +57,7 @@ def cmd_del_note(config: Config, title: str, notebook: str, yes: bool) -> None:
del_note = group.add_parser("delete", aliases=["del"]) del_note = group.add_parser("delete", aliases=["del"])
del_note.add_argument("--notebook", "-n", help="add to this notebook [general]") del_note.add_argument("--notebook", "-n", help="add to this notebook [general]")
del_note.add_argument("--yes", action="store_true", help="dont prompt for confirmation") del_note.add_argument("--yes", action="store_true", help="dont prompt for confirmation")
del_note.add_argument("title", help="title for note", nargs="?") del_note.add_argument("title", help="title for note", nargs="*")
del_note.set_defaults(func=cmd_del_note) del_note.set_defaults(func=cmd_del_note)
### List notes ### List notes
@@ -63,7 +67,7 @@ def cmd_list_notes(config: Config, notebook: str, pretty: bool) -> None:
for filename in notes.get_file_list(config, notebook): for filename in notes.get_file_list(config, notebook):
if pretty: if pretty:
e = notes.file_entry(config, filename) e = notes.file_entry(config, filename)
print(f"{e.notebook:>20s}: {e.filename}") print(f"\033[32m{e.notebook:>20s}\033[0m: {e.filename}")
else: else:
print(filename) print(filename)
@@ -79,10 +83,13 @@ list_notes.set_defaults(func=cmd_list_notes)
def cmd_edit_note(config, title, notebook: str) -> None: def cmd_edit_note(config, title, notebook: str) -> None:
filename = notes.search_filenames(config, title, notebook) filename = notes.search_filenames(config, title, notebook)
notes.edit_file(config, filename) notes.edit_file(config, filename)
notes.run_git(
config, ["add", str(filename.relative_to(config.directory))], quiet=True
)
edit_note = group.add_parser("edit", aliases=["e"]) edit_note = group.add_parser("edit", aliases=["e"])
edit_note.add_argument("title", help="note title", nargs="?") edit_note.add_argument("title", help="note title", nargs="*")
edit_note.add_argument("--notebook", "-n", help="limit to this notebook") edit_note.add_argument("--notebook", "-n", help="limit to this notebook")
edit_note.set_defaults(func=cmd_edit_note) edit_note.set_defaults(func=cmd_edit_note)
@@ -95,6 +102,48 @@ def cmd_show_note(config, title, notebook: str) -> None:
show_note = group.add_parser("show", aliases=["s", "cat"]) show_note = group.add_parser("show", aliases=["s", "cat"])
show_note.add_argument("title", help="note title", nargs="?") show_note.add_argument("title", help="note title", nargs="*")
show_note.add_argument("--notebook", "-n", help="limit to this notebook") show_note.add_argument("--notebook", "-n", help="limit to this notebook")
show_note.set_defaults(func=cmd_show_note) show_note.set_defaults(func=cmd_show_note)
def cmd_render_note(config, title, notebook: str) -> None:
filename = notes.search_filenames(config, title, notebook)
with open(filename, "r", encoding="utf-8") as input_file:
text = input_file.read()
html = templating.render_note(text, title=filename.stem)
print(html)
render_note = group.add_parser("render")
render_note.add_argument("title", help="note title", nargs="*")
render_note.add_argument("--notebook", "-n", help="limit to this notebook")
render_note.set_defaults(func=cmd_render_note)
### Git
def cmd_git(config: Config, cmd: list[str]) -> None:
output = notes.run_git(config, cmd)
sys.stdout.write(output)
git = group.add_parser("git", help="run a git command in the note directory")
git.add_argument("cmd", help="git command", nargs="*")
git.set_defaults(func=cmd_git)
### Sync
def cmd_sync(config: Config) -> None:
output = notes.run_git(config, ["pull"])
sys.stdout.write(f"Pull: {output}\n")
output = notes.run_git(config, ["push"])
sys.stdout.write(f"Push: {output}\n")
git = group.add_parser("sync", help="synchronise the notes")
git.set_defaults(func=cmd_sync)
+25 -3
View File
@@ -3,6 +3,7 @@ import pathlib
import sys import sys
from pyn.config import Config from pyn.config import Config
from pyn.common import logger
from datetime import datetime from datetime import datetime
from shutil import copyfileobj from shutil import copyfileobj
@@ -76,19 +77,25 @@ def get_file_list(
dirs[:] = [d for d in dirs if d[0] != "."] dirs[:] = [d for d in dirs if d[0] != "."]
for filename in files: for filename in files:
yield d / filename if filename.endswith(".md"):
yield d.relative_to(root) / filename
def search_filenames( def search_filenames(
config: Config, title: str, notebook: str | None = None config: Config, title: str | list[str], notebook: str | None = None
) -> pathlib.Path: ) -> pathlib.Path:
root = config.directory / notebook if notebook else config.directory root = config.directory / notebook if notebook else config.directory
files = "\n".join((str(x) for x in get_file_list(config, notebook)))
if isinstance(title, list):
title = " ".join(title)
cmd = ["fzf", "-1", "-i"] cmd = ["fzf", "-1", "-i"]
if title: if title:
cmd.extend(["-q", title]) cmd.extend(["-q", title])
try: try:
output = subprocess.check_output(cmd, cwd=root) output = subprocess.check_output(cmd, cwd=root, input=files.encode())
file_path = root / output.decode("utf-8").strip() file_path = root / output.decode("utf-8").strip()
return file_path return file_path
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@@ -123,6 +130,7 @@ def search_contents(
if full_line: if full_line:
cmd.append("-x") cmd.append("-x")
logger.debug("Executing: %r", cmd)
try: try:
output = subprocess.check_output(cmd) output = subprocess.check_output(cmd)
for line in output.decode("utf-8").strip().split("\n"): for line in output.decode("utf-8").strip().split("\n"):
@@ -132,3 +140,17 @@ def search_contents(
if e.returncode == 1: if e.returncode == 1:
return return
raise raise
def run_git(config: Config, cmd: list[str], quiet: bool = False) -> str:
if not (config.directory / ".git").is_dir():
if quiet:
return ""
raise RuntimeError("Notes not currently tracked")
cmd = ["git", "-C", str(config.directory)] + cmd
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
return output.decode("utf-8").strip()
except subprocess.CalledProcessError as e:
raise RuntimeError(str(e))
+43
View File
@@ -0,0 +1,43 @@
<!doctype html5>
<html>
<head>
<title>{{ title }}</title>
<style>
BODY {
font-family: Arial, Helvetica, sans-serif;
font-size: 10pt;
}
DIV.page {
margin: 20px;
}
HR {
color: #DDD;
}
H1, H2, H3 {
margin-bottom: 5px;
}
P {
margin-top: 2px;
margin-bottom: 2px;
}
H1 {
font-size: 16pt;
}
H2 {
font-size: 14pt;
}
H3 {
font-size: 12pt;
}
DIV.page P:first-of-type {
color: #999;
text-align: right;
}
</style>
</head>
<body>
<div class="page">
{{ content|safe }}
</div>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
from jinja2 import Environment, PackageLoader, select_autoescape
import markdown
env = Environment(loader=PackageLoader("pyn"), autoescape=select_autoescape())
def render_note(note: str, title: str = "") -> str:
content = markdown.markdown(note)
template = env.get_template("render_note.html")
return template.render(content=content, title=title)
+7 -3
View File
@@ -4,6 +4,10 @@ from pyn.commands import group as parent_group
from shutil import get_terminal_size from shutil import get_terminal_size
import sys import sys
LIST_CHAR = "-"
COMPLETE_CHAR = "x"
PENDING_PATTERN = r"^[+\-\*]\s+\[ \]"
COMPLETED_PATTERN = r"^[+\-\*]\s+\[" + COMPLETE_CHAR + r"\]"
p = parent_group.add_parser("todo", help="manipulate todo items") p = parent_group.add_parser("todo", help="manipulate todo items")
group = p.add_subparsers() group = p.add_subparsers()
@@ -35,10 +39,10 @@ def cmd_list_pending(config: Config, notebook: str):
entry = notes.file_entry(config, result.filename) entry = notes.file_entry(config, result.filename)
if entry.notebook != nb: if entry.notebook != nb:
nb = entry.notebook nb = entry.notebook
print(f"\nNotebook: {entry.notebook}") print(f"\nNotebook: \033[32m{entry.notebook}\033[0m")
name = result.filename.name name = result.filename.name
sys.stdout.write(result.snippet) sys.stdout.write(result.snippet)
sys.stdout.write(" " * (cols - len(result.snippet) - len(name))) sys.stdout.write(" " * (cols - len(result.snippet) - len(name) - 1))
sys.stdout.write(name) sys.stdout.write(name)
sys.stdout.write("\n") sys.stdout.write("\n")
@@ -62,7 +66,7 @@ def cmd_mark_complete(
item_str = " ".join(item) item_str = " ".join(item)
results = list( results = list(
notes.search_contents( notes.search_contents(
config, f"* [{unchecked}] {item}", notebook, full_line=True config, f"* [{unchecked}] {item_str}", notebook, full_line=True
) )
) )
+2
View File
@@ -8,6 +8,8 @@ authors = [
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"markdown (>=3.10.1,<4.0.0)",
"jinja2 (>=3.1.6,<4.0.0)"
] ]
[project.scripts] [project.scripts]