delete command, code smell
This commit is contained in:
parent
a273e270c5
commit
9d99d45d68
2 changed files with 80 additions and 29 deletions
|
@ -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`
|
||||
|
|
101
journal.py
101
journal.py
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue