delete command, code smell

This commit is contained in:
Sofía Aritz 2025-06-09 22:18:45 +02:00
parent a273e270c5
commit 9d99d45d68
Signed by: sofia
GPG key ID: 5A1485B4CCCDDB4A
2 changed files with 80 additions and 29 deletions

View file

@ -3,22 +3,22 @@
## Usage
```sh
usage: journal [-h] [--journal-dir JOURNAL_DIR] [--editor EDITOR] {create,edit}
usage: journal [-h] [--journal-dir JOURNAL_DIR] [--editor EDITOR] {create,edit,delete}
Journal
positional arguments:
{create,edit} Command to run (create a new entry, or view and edit an existing one)
{create,edit,delete} Command to run (create a new entry, or view and edit an existing one)
options:
-h, --help show this help message and exit
--journal-dir JOURNAL_DIR
Path to your journal directory (default from JOURNAL_DIR env or ~/.journal)
Path to your journal directory (default $JOURNAL_DIR or ~/.journal)
--editor EDITOR Editor to use (default $EDITOR or nano)
```
## Installation
With `python3` installed, create a symlink from `journal.py` to a directory on your $PATH.
With `python3` and `gpg` installed, create a symlink from `journal.py` to a directory on your $PATH.
E.g. `ln -s ./journal.py ~/.local/bin/journal`

View file

@ -10,12 +10,35 @@ import getpass
import argparse
import os
import shlex
import logging
import sys
FRONTMATTER_SEPARATOR = "+++"
JOURNAL_DIR = Path(os.path.expanduser(os.environ.get("JOURNAL_DIR", "~/.journal")))
EDITOR = os.environ.get("EDITOR", "nano"),
logger = logging.getLogger(__name__)
def confirm(statement: str, default: Optional[bool] = None) -> Optional[bool]:
if default is True:
prompt = f"{statement} (Y/n): "
elif default is False:
prompt = f"{statement} (y/N): "
else:
prompt = f"{statement} (y/n): "
confirm = input(prompt).strip().lower()
if confirm in ("y", "yes"):
return True
elif confirm in ("n", "no"):
return False
elif confirm == "" and default is not None:
return default
else:
return None
def input_with_confirmation(question: str, default: Optional[bool] = None) -> str:
while True:
answer = input(question).strip()
@ -23,39 +46,40 @@ def input_with_confirmation(question: str, default: Optional[bool] = None) -> st
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"):
confirmation = confirm(f"You entered '{answer}'. Is this correct?")
if confirmation == True:
return answer
elif confirm in ("n", "no"):
print("Let's try again.")
elif confirmation == False:
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'.")
continue
@dataclass
class Entry:
plaintext_path: Path
encrypted_path: Path
def title(self):
return self.encrypted_path.stem
def decrypt(self, passphrase: str):
subprocess.run([
result = subprocess.run([
"gpg", "--batch", "--yes", "--passphrase", passphrase,
"-o", str(self.plaintext_path), "-d", str(self.encrypted_path),
], check=True)
], capture_output=True, text=True)
combined_output = (result.stdout + result.stderr).lower()
if "decryption failed" in combined_output.lower():
logger.error("FATAL | Decryption failed: check your passphrase and try again.")
logger.debug(result.stderr.strip())
sys.exit(1)
if result.returncode != 0:
logger.error(f"FATAL | Decryption failed: exited with code {result.returncode}\n\n{result.stderr.strip()}")
sys.exit(1)
def encrypt(self, passphrase: str):
subprocess.run([
@ -71,6 +95,10 @@ class Entry:
subprocess.run(editor_cmd + [str(self.plaintext_path)])
self.encrypt(passphrase)
def delete(self):
self.plaintext_path.unlink(missing_ok=True)
self.encrypted_path.unlink(missing_ok=True)
@classmethod
def create(cls, base_path: Path, passphrase: str, title: str, strftime="%d-%m-%Y", frontmatter_separator=FRONTMATTER_SEPARATOR):
date = datetime.now().strftime(strftime)
@ -94,11 +122,13 @@ def load_entries(base_path: Path) -> List[Entry]:
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)
return sorted(entries, key=lambda e: e.title())
def choose_entry(entries: List[Entry]) -> Optional[Entry]:
print("== Available entries:")
for i, entry in enumerate(entries):
print(f"* [{i}] {entry.encrypted_path.name}")
print(f"* [{i}] {entry.title()}")
print("")
while True:
selection = input("Select an entry by number (or 'q' to cancel): ").strip().lower()
@ -133,19 +163,37 @@ def edit_entry(passphrase: str, base_path: Path, editor: str):
entry.edit(passphrase, editor)
def delete_entry(base_path: Path):
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
confirmation = confirm(f"Are you sure you want to delete entry '{entry.title()}'?", default=False)
if confirmation:
entry.delete()
else:
print("No changes were made. Quitting.")
return
def main():
parser = argparse.ArgumentParser(description="Journal")
parser.add_argument(
"command",
choices=["create", "edit"],
choices=["create", "edit", "delete"],
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)",
help="Path to your journal directory (default $JOURNAL_DIR or ~/.journal)",
)
parser.add_argument(
"--editor",
@ -155,14 +203,17 @@ def main():
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":
passphrase = getpass.getpass("Enter passphrase: ")
create_entry(passphrase, base_path, args.editor)
elif args.command == "edit":
passphrase = getpass.getpass("Enter passphrase: ")
edit_entry(passphrase, base_path, args.editor)
elif args.command == "delete":
delete_entry(base_path)
else:
parser.print_help()