Last active
June 28, 2024 05:03
-
-
Save Mrfiregem/67ccfea9f3182745c6d6f588ee79bead to your computer and use it in GitHub Desktop.
A one-file note taking program with colored output
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from pathlib import Path | |
from typing import Any | |
import argparse | |
import datetime | |
import json | |
import os | |
import sys | |
import textwrap | |
import uuid | |
Json = Any | |
class Colors: | |
RESET: str = "\x1b[0m" | |
BODY: str = "\x1b[32m" | |
DUE: str = "\x1b[33;1m" | |
OVERDUE: str = "\x1b[31;1m" | |
ID: str= "\x1b[30;1m" | |
POST: str = "\x1b[4m" | |
DIM: str = "\x1b[30m" | |
BOLD: str = "\x1b[1m" | |
def dateStr(userDate) -> str: | |
return datetime.date.fromisoformat(userDate).isoformat() | |
def getConfirmation(prompt: str) -> bool: | |
while True: | |
confirm: str = input(f"{prompt}. Continue? [y/N]: ") | |
letter: str = confirm.strip().lower()[0] | |
if letter in ("y", "n", ""): | |
return letter == "y" | |
print("Invalid answer...") | |
def getXdgDataHome() -> str: | |
return os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) | |
def getNoteFilePath() -> str: | |
return os.getenv("NOTE_FILE", os.path.join(getXdgDataHome(), "user_notes.json")) | |
def readNoteFile() -> Json | None: | |
# Get path to json file | |
note_file = Path(getNoteFilePath()) | |
# Ensure directory containing file exists | |
Path(getXdgDataHome()).mkdir(parents=True, exist_ok=True) | |
# If json file exists, parse into object; else return empty array | |
if note_file.exists(): | |
try: | |
with open(getNoteFilePath(), "a+") as f: | |
f.seek(0) | |
return json.load(f) | |
except Exception as e: | |
print(e, file=sys.stderr) | |
return None | |
else: | |
return json.loads("[]") | |
def groupNotesByDueDate(data: Json) -> dict[str, list[Json]]: | |
result: dict[str, list[Json]] = {"NotDue": [], "Overdue": [], "Due": []} | |
curdate = datetime.date.today() | |
for note in data: | |
duedate: str | None = note.get("due_date", None) | |
if not duedate: | |
result["NotDue"].append(note) | |
elif datetime.date.fromisoformat(duedate) < curdate: | |
result["Overdue"].append(note) | |
elif datetime.date.fromisoformat(duedate) == curdate: | |
result["Due"].append(note) | |
else: | |
if duedate in result: | |
result[duedate].append(note) | |
else: | |
result[duedate] = [note] | |
return result | |
def printNote(note: Json) -> None: | |
nbody = textwrap.wrap(note['body']) | |
nmeta: str = f"{Colors.DIM}({Colors.RESET} Posted: {Colors.POST}{note['post_date']}{Colors.RESET}{Colors.DIM}; {Colors.RESET}ID: {Colors.POST}{note['id']}{Colors.RESET} {Colors.DIM})" | |
print( | |
f"\t{Colors.DIM}┌─{Colors.RESET}{nmeta}{Colors.RESET}", | |
end=f"\n\t{Colors.DIM}├─╼\n", | |
) | |
for line in nbody: | |
print(f"\t{Colors.DIM}│ {Colors.BODY}{line}{Colors.RESET}") | |
print(f"{Colors.DIM}\t└─╼{Colors.RESET}\n") | |
def printNotesList(args) -> None: | |
# Read notes from file into dict | |
groupedNotes: dict[str, list[Json]] = groupNotesByDueDate(readNoteFile()) | |
# Print expired notes | |
if len(groupedNotes["Overdue"]) > 0: | |
print( | |
f"\n{Colors.BOLD}{Colors.DIM}{'*'*18:^21}{Colors.OVERDUE}{'OVERDUE NOTES':^21}{Colors.DIM}{'*'*18:^21}{Colors.RESET}\n" | |
) | |
for n in sorted(groupedNotes["Overdue"], key=lambda item: item['post_date']): | |
printNote(n) | |
# Exit if overdue-only flag is passed | |
if args.overdue: | |
return | |
# Print notes due today | |
if len(groupedNotes["Due"]) > 0: | |
print( | |
f"\n{Colors.BOLD}{Colors.DIM}{'*'*18:^21}{Colors.DUE}{'NOTES DUE TODAY':^21}{Colors.DIM}{'*'*18:^21}{Colors.RESET}\n" | |
) | |
for n in sorted(groupedNotes["Due"], key=lambda item: item['post_date']): | |
printNote(n) | |
# Print notes with a due date | |
for date, notes in sorted(groupedNotes.items()): | |
if date not in ["Due", "Overdue", "NotDue"] and len(notes) > 0: | |
print( | |
f"\n{Colors.BOLD}{Colors.DIM}{'*'*18:^21}{Colors.RESET}{Colors.BOLD}{f"NOTES DUE {date}":^21}{Colors.DIM}{'*'*18:^21}{Colors.RESET}\n" | |
) | |
for n in notes: | |
printNote(n) | |
# Print notes without a due date | |
if len(groupedNotes["NotDue"]) > 0: | |
print( | |
f"\n{Colors.BOLD}{Colors.DIM}{'*'*18:^21}{Colors.RESET}{Colors.BOLD}{'OTHER NOTES':^21}{Colors.DIM}{'*'*18:^21}{Colors.RESET}\n" | |
) | |
for n in sorted(groupedNotes["NotDue"], key=lambda item: item['post_date']): | |
printNote(n) | |
def addNote(args) -> None: | |
# Initialize temp dictionary to store note data | |
userNote: dict[str, str] = { | |
"id": f"{uuid.uuid4()}", | |
"post_date": datetime.date.today().isoformat(), | |
"body": args.msg, | |
} | |
if args.due: | |
userNote["due_date"] = args.due | |
# Get note list as Json object | |
noteList: Json = readNoteFile() | |
if noteList is None: | |
sys.exit(1) | |
# Add new note to json array | |
noteList.append(userNote) | |
# Write new list back to file | |
with open(getNoteFilePath(), "w") as f: | |
json.dump(noteList, f) | |
def removeNote(args) -> None: | |
# Read in note array | |
noteList: Json = readNoteFile() | |
# Find and delete the matching ID | |
for i in range(len(noteList)): | |
if noteList[i]["id"] == args.id: | |
noteList.pop(i) | |
break | |
# Save back the file | |
with open(getNoteFilePath(), "w") as f: | |
json.dump(noteList, f) | |
def removeAll(args) -> None: | |
if args.confirm or getConfirmation("Removing all notes"): | |
with open(getNoteFilePath(), "w") as f: | |
json.dump([], f) | |
def main() -> None: | |
# Build command-line argument parser | |
parser = argparse.ArgumentParser(description="A simple note-taking program") | |
subcmds = parser.add_subparsers(title="subcommands", required=True) | |
parser.add_argument( | |
"-n", | |
"--nocolor", | |
help="remove color from output", | |
action="store_false", | |
dest="color", | |
) | |
# Add sub-command to view notes file | |
parser_list = subcmds.add_parser( | |
"ls", | |
help="List notes", | |
aliases=["list", "show"], | |
description="show every note you've created", | |
) | |
parser_list.add_argument( | |
"-o", | |
"--overdue", | |
action="store_true", | |
help="only show notes whose due date expired", | |
) | |
parser_list.set_defaults(func=printNotesList) | |
# Add sub-command to add a new note to the notes file | |
parser_add = subcmds.add_parser( | |
"add", | |
help="create a new note", | |
aliases=["new"], | |
description="create a new note with an optional due date", | |
) | |
parser_add.add_argument("msg", help="the body of the new note") | |
parser_add.add_argument( | |
"-d", | |
"--due", | |
type=dateStr, | |
required=False, | |
default=None, | |
help="group notes by due date (YYYY-MM-DD)", | |
) | |
parser_add.set_defaults(func=addNote) | |
# Add a sub-command to remove a note by ID or interactively | |
parser_del = subcmds.add_parser( | |
"rm", | |
help="remove note by id", | |
aliases=["remove", "del"], | |
description="remove a specific note", | |
) | |
parser_del.add_argument("id", type=str, help="the unique ID to remove") | |
parser_del.set_defaults(func=removeNote) | |
# Add command to remove all notes | |
parser_clear = subcmds.add_parser( | |
"clear", | |
help="remove all notes", | |
aliases=["purge"], | |
description="remove every note you've created", | |
) | |
parser_clear.add_argument( | |
"-c", "--confirm", action="store_true", help="bypass confirmation" | |
) | |
parser_clear.set_defaults(func=removeAll) | |
# Parse user arguments into object | |
args = parser.parse_args() | |
# Disable color output if stdout is not the terminal | |
if os.fstat(0) != os.fstat(1): | |
args.color = False | |
# Remove color output if user desires | |
if not args.color: | |
Colors.RESET = "" | |
Colors.BODY = "" | |
Colors.DUE = "" | |
Colors.ID = "" | |
Colors.POST = "" | |
Colors.OVERDUE = "" | |
Colors.DIM = "" | |
Colors.BOLD = "" | |
# Run associated function of subcommand | |
args.func(args) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output of
note -h