initial impl

This commit is contained in:
Sofía Aritz 2025-06-09 02:08:26 +02:00
parent 947bb17c64
commit af72a6dc31
Signed by: sofia
GPG key ID: 5A1485B4CCCDDB4A

170
journal.py Executable file
View file

@ -0,0 +1,170 @@
#!/usr/bin/env python3
from typing import Dict, Any, Optional, List
from datetime import datetime, date
from pathlib import Path
from dataclasses import dataclass
import subprocess
import getpass
import argparse
import os
import shlex
FRONTMATTER_SEPARATOR = "+++"
JOURNAL_DIR = Path(os.path.expanduser(os.environ.get("JOURNAL_DIR", "~/.journal")))
EDITOR = os.environ.get("EDITOR", "nano"),
def input_with_confirmation(question: str, default: Optional[bool] = None) -> str:
while True:
answer = input(question).strip()
if not answer:
print("Input cannot be empty.")
continue
if default is True:
prompt = f"You entered '{answer}'. Is this correct? (Y/n): "
elif default is False:
prompt = f"You entered '{answer}'. Is this correct? (y/N): "
else:
prompt = f"You entered '{answer}'. Is this correct? (y/n): "
confirm = input(prompt).strip().lower()
if confirm in ("y", "yes"):
return answer
elif confirm in ("n", "no"):
print("Let's try again.")
continue
elif confirm == "" and default is not None:
if default:
return answer
else:
print("Let's try again.")
continue
else:
print("Please answer with 'y' or 'n'.")
@dataclass
class Entry:
plaintext_path: Path
encrypted_path: Path
def decrypt(self, passphrase: str):
subprocess.run([
"gpg", "--batch", "--yes", "--passphrase", passphrase,
"-o", str(self.plaintext_path), "-d", str(self.encrypted_path),
], check=True)
def encrypt(self, passphrase: str):
subprocess.run([
"gpg", "--symmetric", "--cipher-algo", "AES256", "--batch",
"--yes", "--passphrase", passphrase, "-o", str(self.encrypted_path),
str(self.plaintext_path),
], check=True)
self.plaintext_path.unlink()
def edit(self, passphrase: str, editor: str):
self.decrypt(passphrase)
editor_cmd = shlex.split(editor) if isinstance(editor, str) else list(editor)
subprocess.run(editor_cmd + [str(self.plaintext_path)])
self.encrypt(passphrase)
@classmethod
def create(cls, base_path: Path, passphrase: str, title: str, strftime="%d-%m-%Y", frontmatter_separator=FRONTMATTER_SEPARATOR):
date = datetime.now().strftime(strftime)
ydate = datetime.now().strftime("%Y-%m-%d")
filename = f"{date} - {title}"
plaintext_path = base_path / filename
encrypted_path = plaintext_path.with_suffix(plaintext_path.suffix + ".gpg")
initial_content = f'{frontmatter_separator}\ndate = "{ydate}"\n{frontmatter_separator}\n\n# {title}\n\n'
plaintext_path.write_text(initial_content, encoding="utf-8")
entry = cls(plaintext_path=plaintext_path, encrypted_path=encrypted_path)
entry.encrypt(passphrase)
return entry
def load_entries(base_path: Path) -> List[Entry]:
entries = []
for encrypted_file in base_path.glob("*.gpg"):
plaintext_path = encrypted_file.with_suffix("")
entries.append(Entry(plaintext_path=plaintext_path, encrypted_path=encrypted_file))
return sorted(entries, key=lambda e: e.encrypted_path.name)
def choose_entry(entries: List[Entry]) -> Optional[Entry]:
for i, entry in enumerate(entries):
print(f"* [{i}] {entry.encrypted_path.name}")
while True:
selection = input("Select an entry by number (or 'q' to cancel): ").strip().lower()
if selection == "q":
print("Cancelled")
return None
elif not selection.isdigit():
print("Please enter a valid number or press 'q' to quit.")
continue
idx = int(selection)
if 0 <= idx < len(entries):
return entries[idx]
else:
print(f"Selection out of range. Please enter between 0 and {len(entries)-1}.")
def create_entry(passphrase: str, base_path: Path, editor: str):
title = input_with_confirmation("Enter entry title: ", default=True)
entry = Entry.create(base_path, passphrase, title)
entry.edit(passphrase, editor)
def edit_entry(passphrase: str, base_path: Path, editor: str):
entries = load_entries(base_path)
if len(entries) == 0:
print("No entries found.")
return
entry = choose_entry(entries)
if not entry:
print("No entry chosen. Quitting.")
return
entry.edit(passphrase, editor)
def main():
parser = argparse.ArgumentParser(description="Journal")
parser.add_argument(
"command",
choices=["create", "edit"],
help="Command to run (create a new entry, or view and edit an existing one)",
)
parser.add_argument(
"--journal-dir",
type=Path,
default=JOURNAL_DIR,
help="Path to your journal directory (default from JOURNAL_DIR env or ~/.journal)",
)
parser.add_argument(
"--editor",
default=EDITOR,
help="Editor to use (default $EDITOR or nano)",
)
args = parser.parse_args()
passphrase = getpass.getpass("Enter passphrase: ")
base_path = args.journal_dir
base_path.mkdir(parents=True, exist_ok=True)
if args.command == "create":
create_entry(passphrase, base_path, args.editor)
elif args.command == "edit":
edit_entry(passphrase, base_path, args.editor)
else:
parser.print_help()
if __name__ == "__main__":
main()