Skip to content

Instantly share code, notes, and snippets.

@JohnStarich
Last active December 26, 2018 07:52
Show Gist options
  • Save JohnStarich/67a24f16483bf956b761138da727071b to your computer and use it in GitHub Desktop.
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.
#!/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