Last active
December 26, 2018 07:52
-
-
Save JohnStarich/67a24f16483bf956b761138da727071b to your computer and use it in GitHub Desktop.
A handy script to quickly begin writing notes in your favorite editor for any given subject. Useful for writing notes in school for multiple subjects.
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 configparser import ConfigParser | |
from datetime import datetime | |
from os import path | |
import argparse | |
import itertools | |
import os | |
import re | |
import shutil | |
import sys | |
import textwrap | |
try: | |
import dateparser | |
except ImportError: | |
print('Install dateparser to continue: pip3 install dateparser', | |
file=sys.stderr) | |
sys.exit(1) | |
def prepare_parser(): | |
description = textwrap.dedent(""" | |
A handy script to quickly begin writing notes in your favorite editor | |
for any given subject. | |
Example directory structure: | |
~/.notesrc contents: | |
NOTES_BASE=~/school | |
NOTES_FORMAT=%m-%d | |
NOTES_EXTENSION=.txt | |
~/school/ | |
ConcurrentProgramming/ | |
01-30.txt | |
01-31.txt | |
SoftwareTesting/ | |
01-31.txt | |
Usage for this example: | |
Write today's notes for Software Testing: | |
notes SoftwareTesting | |
notes Sof | |
Open yesterday's notes for Concurrent Programming: | |
notes ConcurrentProgramming yesterday | |
notes Co yesterday | |
notes Co January 30 | |
You can override the notes home directory and other notes properties | |
by adding the variables to your `~/.notesrc` | |
""") | |
parser = argparse.ArgumentParser( | |
description=description, | |
formatter_class=RawDescriptionAndDefaultArgs, | |
) | |
parser.add_argument('--home', default=default_notes_home(), | |
help='Base directory for all notes. All subdirectory ' | |
'names are considered subject names.') | |
parser.add_argument('--interactive', action='store_true', | |
default=os.isatty(sys.stdout.fileno()), | |
help='Force opening the editor instead of printing ' | |
'the note file path. Defaults to isatty().') | |
parsers_list = [parser] | |
subparsers = parser.add_subparsers() | |
parsers_list += [ | |
add_subparser(subparsers, edit, edit_init, aliases=['e']), | |
add_subparser(subparsers, search, search_init, aliases=['s']), | |
] | |
return parsers_list | |
def add_subparser(subparsers, run, init, **init_kwargs): | |
parser = subparsers.add_parser(run.__name__, **init_kwargs) | |
parser.set_defaults(func=run) | |
init(parser) | |
return parser | |
def search_init(parser): | |
parser.add_argument('--subject', help='Subject to search inside') | |
parser.add_argument('query', nargs='+', help='Search terms') | |
def search(args): | |
query = ' '.join(args.query) | |
note_path = args.home | |
if args.subject is not None: | |
note_path = get_subject_dir(args.home, args.subject) | |
os.chdir(note_path) | |
silver_searcher = shutil.which('ag') | |
if silver_searcher is not None: | |
os.execvp('ag', ['ag', query]) | |
else: | |
os.execvp('grep', ['grep', '-RE', query, '.']) | |
def edit_init(parser): | |
parser.add_argument('subject', help='Directory to store notes in, ' | |
'relative to notes home directory.') | |
parser.add_argument('date', nargs='*', | |
help='Relative or absolute date, defaults to current ' | |
'date.\ne.g. two days ago, last wednesday, november 3') | |
parser.add_argument('--editor', default='vi', | |
help='Desired editor for opening the chosen note. ' | |
'Defaults to $EDITOR if set.') | |
parser.add_argument('--date-format', default='%Y-%m-%d', | |
help="Date format string for opening or creating " | |
"notes.") | |
parser.add_argument('--extension', default='.txt', | |
help='File extension used when opening and creating ' | |
'notes.') | |
def edit(args): | |
# get subject path | |
subject_path = get_subject_dir(args.home, args.subject, parser.error) | |
# check date string | |
datestr = ' '.join(args.date).strip() | |
if len(datestr) == 0: | |
date = datetime.now() | |
else: | |
date = parse_date(datestr) | |
if date is None: | |
parser.error('Unknown date format: "%s"' % datestr) | |
notes_file = date.strftime(args.date_format) + args.extension | |
full_path = path.join(subject_path, notes_file) | |
# run the editor, or print out full path | |
if args.interactive: | |
executable_path = shutil.which(args.editor) | |
if executable_path is None: | |
parser.error('Invalid editor "%s": executable not found on PATH' | |
% args.editor) | |
os.chdir(subject_path) | |
os.execvp(executable_path, [executable_path, full_path]) | |
else: | |
print(full_path, end='') | |
def get_subject_dir(home, subject, error=None): | |
paths = glob_ignore_case(home, subject) | |
if len(paths) == 0: | |
error('No subject found with name: "%s"' % subject) | |
if len(paths) == 1 or path.basename(paths[0]) == subject: | |
# use first path if we only have one, | |
# or if the glob includes an exact match | |
return paths[0] | |
else: | |
error('Multiple subjects found with name: "%s*"\n\t%s' | |
% (subject, '\n\t'.join(paths))) | |
def load_notesrc(): | |
notesrc = path.join(path.expanduser('~'), '.notesrc') | |
if not path.exists(notesrc): | |
return | |
p = ConfigParser(interpolation=None) | |
with open(notesrc) as f: | |
p.read_file(itertools.chain(['[top]\n'], f)) | |
for k, v in p.items('top'): | |
env = k.upper() | |
if env not in os.environ: | |
os.environ[env] = v | |
def parse_date(s): | |
# handle past, present, and future a little better | |
next_str = 'next ' | |
this_str = 'this ' | |
last_str = 'last ' | |
config = {} | |
if s.startswith(next_str): | |
s = s[len(next_str):] | |
config['PREFER_DATES_FROM'] = 'future' | |
elif s.startswith(this_str): | |
s = s[len(this_str):] | |
config['PREFER_DATES_FROM'] = 'current_period' | |
elif s.startswith(last_str): | |
s = s[len(last_str):] | |
config['PREFER_DATES_FROM'] = 'past' | |
return dateparser.parse(s, settings=config) | |
def glob_ignore_case(directory_path, subpath): | |
regex = re.compile('^' + re.escape(subpath) + '.*', re.IGNORECASE) | |
paths = [] | |
for p in os.listdir(args.home): | |
if bool(regex.match(p)): | |
paths.append(path.join(directory_path, p)) | |
return paths | |
def default_notes_home(): | |
return path.join(path.expanduser('~'), 'notes') | |
def get_default(parsers, attr): | |
for parser in parsers: | |
default = parser.get_default(attr) | |
if default is not None: | |
return default | |
return None | |
class RawDescriptionAndDefaultArgs( | |
argparse.ArgumentDefaultsHelpFormatter, | |
argparse.RawDescriptionHelpFormatter): | |
pass | |
if __name__ == '__main__': | |
parsers = prepare_parser() | |
parser = parsers[0] | |
args = parser.parse_args() | |
# override with environment information | |
load_notesrc() | |
overrides = { | |
'home': 'NOTES_HOME', | |
'date_format': 'NOTES_FORMAT', | |
'extension': 'NOTES_EXTENSION', | |
'editor': 'EDITOR', | |
} | |
for attr, env in overrides.items(): | |
if hasattr(args, attr): | |
default = get_default(parsers, attr) | |
if getattr(args, attr) == default: | |
setattr(args, attr, os.getenv(env, default)) | |
# check notes home directory | |
args.home = path.expanduser(args.home) | |
if not path.isdir(args.home): | |
parser.error('Invalid notes home directory: "%s"' % args.home) | |
# run subcommand | |
args.func(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment