Source code for lnote

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright notice
# ----------------
#
# Copyright (C) 2013-2014 Daniel Jung
# Contact: djungbremen@gmail.com
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
#
"""Write notes in LaTeX format, directly from the command line. Organize the
notes in notebooks. Automatically create a notebook for the current day if no
other notebook is selected. View and edit the notes of each day or notebook.
Select one or more notebooks or a certain timerange and export them to a PDF
file, or merge them into a new notebook.

The default data directory is "~/data/lnote". It can be overwritten by setting
the environment variable LNOTE_DIR.

To do:
--> add -l/--line options to select a line or a range of lines
--> add a search command (full text search feature), return notebook names
--> add a command "clear" (or similar) to delete all temporary directories
--> do not overwrite graphic files when submitting a figure, instead rename

Ideas for the future:
--> work with hardlinks instead of copying the graphics files? Not good if
    renaming will be possible in the future.

Written by Daniel Jung, Jacobs University Bremen (2012).
"""
__created__ = '2012-10-29'
__modified__ = '2013-11-29'
import commands
import glob
import fnmatch
import os
import shutil
import subprocess
import sys
import time

try:
    import optparse2 as optparse
except ImportError:
    import optparse


# set default data directory, create it if it does not exist
LNOTE_DIR = os.path.expanduser(os.environ.get('LNOTE_DIR', '~/data/lnote'))
if not os.path.isdir(LNOTE_DIR):
    os.makedirs(LNOTE_DIR)


#=====================#
# Command definitions #
#=====================#


[docs]def create(*args): """Create a notebook.""" # 2012-10-30 - 2012-10-30 op = optparse.OptionParser(usage='%prog create [options] NAME ' + '[NAME2 NAME3 ...]', version=__modified__, description=create.__doc__) op.add_option('-t', '--template', default='', type=str, help='create new notebook from template ' + '(duplicate an existing notebook)') op.add_option('-v', '--verbose', default=False, action='store_true', help='be verbose') if len(args) == 0: args = ['--help'] opts, posargs = op.parse_args(args=list(args)) # check if template exists if opts.template \ and not os.path.isdir(os.path.join(LNOTE_DIR, opts.template)): print >>sys.stderr, 'lnote create: template "%s" ' % opts.template + \ 'not found' sys.exit(1) for name in posargs: # check if notebook with that name already exists if name in os.listdir(LNOTE_DIR): print >>sys.stderr, 'lnote create: cannot create notebook ' + \ '"%s": file exists' % name continue # create notebook os.mkdir(os.path.join(LNOTE_DIR, name)) if opts.verbose: print 'mkdir %s' % os.path.join(LNOTE_DIR, name) dst = os.path.join(LNOTE_DIR, name, name+'.tex') if opts.template: # copy the template file src = os.path.join(LNOTE_DIR, opts.template, opts.template+'.tex') shutil.copyfile(src, dst) if opts.verbose: print 'cp %s %s' % (src, dst) else: open(dst, 'w').close() if opts.verbose: print 'echo -n > %s' % dst # copy all other files from the template as well if opts.template: for filename in os.listdir(os.path.join(LNOTE_DIR, opts.template)): if filename != opts.template+'.tex': src = os.path.join(LNOTE_DIR, opts.template, filename) dst = os.path.join(LNOTE_DIR, name) shutil.copy(src, dst) if opts.verbose: print 'cp %s %s' % (src, dst)
[docs]def listn(*args): """List notebooks.""" # 2012-10-30 - 2013-11-08 op = optparse.OptionParser(usage='%prog list [options] ' + '[PATTERN [PATTERN2 PATTERN3 ...]]', version=__modified__, description=listn.__doc__) op.add_option('-l', '--long', default=False, action='store_true', help='use a long listing format. The columns, from left ' + 'to right: 1. number of files, ' + '2. linecount of tex-file, ' + '3. total size in bytes, ' + '4. modification time, 5. notebook name') opts, posargs = op.parse_args(args=list(args)) if len(posargs) == 0: posargs = ['*'] # select notebooks according to patterns try: notebooks = select_notebooks(*posargs, unique=True, forgiving=True) except SelectNotebookError as e: print >>sys.stderr, \ 'lnote list: notebook not found: %s' % e sys.exit(1) notebooks.sort() # display selected notebooks if opts.long: for notebook in notebooks: # collect some information about the notebook dirsize = get_size(dirpath(notebook)) dirsize_digits = len(str(get_size(LNOTE_DIR))) modtime = get_mtime(dirpath(notebook)) filecount = len(os.listdir(dirpath(notebook))) with open(texpath(notebook), 'r') as f: linecount = len(f.readlines()) print '% 3i % 4i % *i %s %s' \ % (filecount, linecount, dirsize_digits, dirsize, time.ctime(modtime), notebook) else: printcols(notebooks)
[docs]def rename(*args): """Rename a notebook.""" # 2012-10-30 - 2012-10-30 op = optparse.OptionParser(usage='%prog rename [options] OLD_NAME ' + 'NEW_NAME', version=__modified__, description=rename.__doc__) op.add_option('-v', '--verbose', default=False, action='store_true', help='be verbose') if len(args) == 0: args = ['--help'] opts, posargs = op.parse_args(args=list(args)) if len(posargs) != 2: print >>sys.stderr, 'lnote rename: wrong number of parameters, ' + \ 'expecting exactly two (old and new notebook name)' old, new = posargs # check if notebook exists if old not in os.listdir(LNOTE_DIR): print >>sys.stderr, 'lnote rename: cannot rename "%s": ' % old + \ 'no such file' sys.exit(1) # check if notebook with that name already exists if new in os.listdir(LNOTE_DIR): print >>sys.stderr, 'lnote rename: cannot rename notebook to ' + \ '"%s": file exists' % new sys.exit(1) # rename notebook olddir = os.path.join(LNOTE_DIR, old) newdir = os.path.join(LNOTE_DIR, new) os.rename(olddir, newdir) if opts.verbose: print 'mv %s %s' % (olddir, newdir) oldtex = os.path.join(LNOTE_DIR, new, old+'.tex') newtex = os.path.join(LNOTE_DIR, new, new+'.tex') os.rename(oldtex, newtex) if opts.verbose: print 'mv %s %s' % (oldtex, newtex)
[docs]def merge(*args): """Merge several notebooks into another (append all source notebooks to the target notebook). If the target notebook does not exist, create it.""" # 2012-10-31 - 2012-11-23 op = optparse.OptionParser(usage='%prog merge [options] ' + 'SOURCE_NOTEBOOKS TARGET_NOTEBOOK', version=__modified__, description=merge.__doc__) op.add_option('-v', '--verbose', default=False, action='store_true', help='be verbose') op.add_option('-d', '--date', default=False, action='store_true', help='show date of notebooks as marginal notes') if len(args) == 0: args = ['--help'] opts, posargs = op.parse_args(args=list(args)) notebooks = select_notebooks(*posargs[:-1]) new = select_notebook(posargs[-1], forgiving=True) # check if all notebooks exist for notebook in notebooks: if notebook not in os.listdir(LNOTE_DIR): print >>sys.stderr, 'lnote merge: cannot load notebook ' + \ '"%s": no such file' % notebook sys.exit(1) # merge notebooks for notebook in notebooks: # load tex-file of source notebook and append it to target notebook with open(texpath(notebook), 'r') as f: lines = f.readlines() linebreak('--notebook', new) if opts.date: marginnote('--notebook', new, '\\texttt{%s}' % notebook) append_text(new, ''.join(lines).strip()) if opts.verbose: print 'cat %s >> %s' % (texpath(notebook), texpath(new)) # copy all other files from the source notebook for filename in os.listdir(os.path.join(LNOTE_DIR, notebook)): if filename != notebook+'.tex': src = os.path.join(LNOTE_DIR, notebook, filename) dst = os.path.join(LNOTE_DIR, new) shutil.copy(src, dst) if opts.verbose: print 'cp %s %s' % (src, dst)
[docs]def edit(*args): """Open notebook in a text editor. If multiple notebooks are given, open them side by side.""" # 2012-10-31 - 2012-11-01 op = optparse.OptionParser(usage='%prog edit [options] NOTEBOOK ' + '[NOTEBOOK2 NOTEBOOK3 ...]', version=__modified__, description=edit.__doc__) op.add_option('-e', '--editor', default=os.environ.get('LNOTE_EDITOR', 'vim'), help='set editor. Default can be overwritten with ' + 'environment variable LNOTE_EDITOR') opts, posargs = op.parse_args(args=list(args)) if len(posargs) == 0: posargs = [date2filename(*jdn2greg(opt2day('today')))] # select notebooks notebooks = select_notebooks(*posargs, unique=True) # open notebooks side by side paths = [texpath(notebook) for notebook in notebooks] subprocess.call([opts.editor] + paths, stderr=subprocess.STDOUT, stdout=None if opts.editor in ('vi', 'vim') else open('/dev/null', 'w'))
[docs]def delete(*args): """Delete a notebook.""" # 2012-10-31 - 2012-10-31 op = optparse.OptionParser(usage='%prog delete [options] NOTEBOOK ' + '[NOTEBOOK2 NOTEBOOK3 ...]', version=__modified__, description=delete.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-f', '--force', default=False, action='store_true', help='ignore nonexistent notebooks, never prompt') op.add_option('-t', '--test', default=False, action='store_true', help='test mode, only show what would have been deleted') if len(args) == 0: args = ['--help'] opts, posargs = op.parse_args(args=list(args)) # select notebooks try: notebooks = select_notebooks(*posargs, unique=True, forgiving=opts.force) except SelectNotebookError as e: print >>sys.stderr, \ 'lnote delete: notebook not found: %s' % e sys.exit(1) # delete notebooks for notebook in notebooks: # prompt if not opts.test and not opts.force: message = 'delete notebook "%s"? ' % notebook answer = raw_input(message).lower() if not answer or not 'yes'.startswith(answer): continue # delete if opts.test: print 'would have deleted "%s"' % notebook else: shutil.rmtree(os.path.join(LNOTE_DIR, notebook))
[docs]def export(*args): """Export a notebook or selection of notebooks (for now only to PDF format).""" # 2012-10-31 - 2012-11-23 op = optparse.OptionParser(usage='%prog export [options] ' + '[PATTERN [PATTERN2 PATTERN3 ...]] ' + 'OUTPUT_FILE', version=__modified__, description=export.__doc__) op.add_option('-m', '--export-merge', dest='mergetemp', default='temp-export-merge', help='set temporary notebook name that is used to merge ' + 'the selected notebooks') op.add_option('-c', '--export-compile', dest='compiletemp', default='/tmp/export-compile', help='set temporary directory that is used to compile the ' + 'LaTeX file of the merged notebook') op.add_option('-v', '--verbose', default=False, action='store_true', help='be verbose, print compiler messages') op.add_option('-d', '--date', default=False, action='store_true', help='show date of notebooks as marginal notes') if len(args) == 0: args = ['--help'] opts, posargs = op.parse_args(args=list(args)) if len(posargs) == 1: posargs = ['today', posargs[0]] # select notebooks notebooks = select_notebooks(*posargs[:-1]) if len(notebooks) == 0: print >>sys.stderr, 'lnote export: no notebook selected' sys.exit(1) # select target target = posargs[-1] if '.' not in target: target += '.pdf' # merge notebooks into temporary notebook delete(opts.mergetemp, '--force') mergeargs = notebooks + [opts.mergetemp] if opts.date: mergeargs.append('--date') merge(*mergeargs) # copy files over to temporary compile directory #if os.path.exists(opts.compiletemp): ## aux file must not be deleted #shutil.rmtree(opts.compiletemp) if not os.path.exists(opts.compiletemp): os.makedirs(opts.compiletemp) for filename in os.listdir(dirpath(opts.mergetemp)): if filename != opts.mergetemp+'.tex': src = os.path.join(dirpath(opts.mergetemp), filename) shutil.copy(src, opts.compiletemp) # add LaTeX preample and epilog to the tex-file and copy it over preamble = '\documentclass{scrartcl}\n' + \ '\usepackage{amsmath}\n\usepackage{amsthm}\n' + \ '\usepackage{graphicx}\n\usepackage{commath}\n' + \ '\\begin{document}\n\n' epilog = '\\end{document}' with open(texpath(opts.mergetemp), 'r') as f: lines = f.readlines() with open(os.path.join(opts.compiletemp, opts.mergetemp+'.tex'), 'w') as f: f.write(preamble.strip()+'\n') f.write(''.join(lines).strip()+'\n') f.write(epilog+'\n') # use pdflatex to compile PDF document cdcmd = 'cd %s' % opts.compiletemp latexcmd = 'pdflatex %s' % opts.mergetemp+'.tex' #os.system('%s;%s' % (cdcmd, latexcmd)) subprocess.call('%s;%s' % (cdcmd, latexcmd), shell=True, stderr=None if opts.verbose else subprocess.STDOUT, stdout=None if opts.verbose else open('/dev/null', 'w')) try: shutil.copyfile(os.path.join(opts.compiletemp, opts.mergetemp+'.pdf'), target) except IOError: print >>sys.stderr, \ 'lnote export: export failed' + \ ('' if opts.verbose else ', use --verbose to get details')
[docs]def path(*args): """Get path of the directory of a certain notebook. """ # 2013-11-08 - 2013-11-08 op = optparse.OptionParser(usage='%prog path [options] NOTEBOOK ' + '[NOTEBOOK2 NOTEBOOK3 ...]', version=__modified__, description=view.__doc__) op.add_option('-t', '--tex', default=False, action='store_true', help='get path of the tex file instead of the directory') opts, posargs = op.parse_args(args=list(args)) if len(posargs) == 0: posargs = ['today'] # select notebooks notebooks = select_notebooks(*posargs) if len(notebooks) == 0: print >>sys.stderr, 'lnote path: no notebook selected' sys.exit(1) if opts.tex: for notebook in notebooks: print texpath(notebook) else: for notebook in notebooks: print dirpath(notebook)
[docs]def view(*args): """View a notebook or a selection of notebooks. To do: --> add -l/--line option to select a line or a range of lines""" # 2012-11-01 - 2012-11-23 op = optparse.OptionParser(usage='%prog view [options] NOTEBOOK ' + '[NOTEBOOK2 NOTEBOOK3 ...]', version=__modified__, description=view.__doc__) op.add_option('-e', '--export', default=False, action='store_true', help='view exported version (for now only PDF)') op.add_option('-p', '--pdfviewer', default=os.environ.get('LNOTE_PDFVIEWER', 'okular'), help='set PDF viewer. Default can be overwritten with ' + 'environment variable LNOTE_PDFVIEWER') op.add_option('-m', '--view-merge', dest='mergetemp', default='temp-view-merge', help='set temporary notebook name that is used to merge ' + 'the selected notebooks') op.add_option('-c', '--view-compile', dest='compiletemp', default='/tmp/view-compile', help='set temporary directory that is used to compile the ' + 'LaTeX file of the merged notebook') op.add_option('-v', '--verbose', default=False, action='store_true', help='be verbose, print compiler messages') op.add_option('-d', '--date', default=False, action='store_true', help='show date of notebooks as marginal notes') opts, posargs = op.parse_args(args=list(args)) if len(posargs) == 0: posargs = ['today'] # select notebooks notebooks = select_notebooks(*posargs) if len(notebooks) == 0: print >>sys.stderr, 'lnote view: no notebook selected' sys.exit(1) if opts.export: # compile the LaTeX file of the selected notebooks pdffile = os.path.join(dirpath(opts.mergetemp), opts.mergetemp+'.pdf') exportargs = notebooks+[pdffile, '--export-merge', opts.mergetemp, '--export-compile', opts.compiletemp] if opts.verbose: exportargs.append('--verbose') if opts.date: exportargs.append('--date') export(*exportargs) # open the PDF file in a PDF viewer (if not already open) lines = subprocess.check_output('ps aux', shell=True).strip().splitlines() for line in lines: if opts.pdfviewer in line and pdffile in line: if opts.verbose: print 'lnote view: document still open in PID %s' \ % line.split()[1] break else: subprocess.call('%s %s &' % (opts.pdfviewer, pdffile), shell=True, stderr=None if opts.verbose else subprocess.STDOUT, stdout=None if opts.verbose else open('/dev/null', 'w')) else: # merge notebooks into temporary notebook and display the tex-file delete(opts.mergetemp, '--force') mergeargs = notebooks+[opts.mergetemp] if opts.date: mergeargs.append('--date') merge(*mergeargs) with open(texpath(opts.mergetemp), 'r') as f: text = ''.join(f.readlines()) if text[0] == '\n': text = text[1:] if text[-1] == '\n': text = text[:-1] print text
[docs]def text(*args): """Add text to a notebook. This can be any LaTeX-compatible code. If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-10-29' __modified__ = '2012-10-31' op = optparse.OptionParser(usage='%prog text [options] TEXT...', version=__modified__, description=text.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-f', '--file', default=None, help='read text from the given text file') op.add_option('-i', '--stdin', default=False, action='store_true', help='read text from standard input') if len(args) == 0: args = ['--help'] op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) # select notebook try: notebook = select_notebook(opts.notebook) except SelectNotebookError: print >>sys.stderr, \ 'lnote text: notebook not found: %s' % opts.notebook sys.exit(1) # append text from command line arguments append_text(notebook, ' '.join(posargs)) # append text from the given text file if opts.file: append_text(notebook, open(opts.file, 'r').read()) # read text from standard input if opts.stdin: stdin = sys.stdin.read().strip() append_text(notebook, stdin)
[docs]def linebreak(*args): """Add a linebreak (an empty line) to a notebook. If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-10-31' __modified__ = '2012-10-31' op = optparse.OptionParser(usage='%prog linebreak [options]', version=__modified__, description=linebreak.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-#', '--number', default=1, type=int, help='set number of linebreaks') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) if len(posargs) != 0: print >>sys.stderr, \ 'lnote linebreak: not expecting any positional parameters' sys.exit(1) if opts.number < 0: op.error('bad number of linebreaks: %i. ' % opts.number+\ 'Must be non-negative integer') # select notebook notebook = select_notebook(opts.notebook) # append empty lines for i in xrange(opts.number): append_text(notebook, '\n')
[docs]def section(*args): """Add a section to a notebook. If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-10-31' __modified__ = '2012-11-19' op = optparse.OptionParser(usage='%prog section [options] TITLE...', version=__modified__, description=section.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-l', '--level', default=1, type=int, help='set level') op.add_option('-#', '--numbered', default=False, action='store_true', help='add numbered section') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) # determine section level if opts.level == 1: texcmd = 'section' elif opts.level == 2: texcmd = 'subsection' elif opts.level == 3: texcmd = 'subsubsection' else: op.error('bad section level: %i. Must be out of (1, 2, 3)' % opts.level) # numbered sections if not opts.numbered: texcmd += '*' # select notebook notebook = select_notebook(opts.notebook) # add the section title = ' '.join(posargs) linebreak('--notebook', notebook) #'--number', '3' text('--notebook', notebook, '\%s{%s}' % (texcmd, title))
[docs]def paragraph(*args): """Add a titled paragraph to a notebook. If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-10-31' __modified__ = '2012-10-31' op = optparse.OptionParser(usage='%prog paragraph [options] TITLE...', version=__modified__, description=paragraph.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-l', '--level', default=1, type=int, help='set level') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) # determine section level if opts.level == 1: texcmd = 'paragraph' elif opts.level == 2: texcmd = 'subparagraph' else: op.error('bad paragraph level: %i. Must be out of (1, 2)' % opts.level) # select notebook notebook = select_notebook(opts.notebook) # add the paragraph title = ' '.join(posargs) linebreak('--notebook', notebook) text('--notebook', notebook, '\%s{%s}' % (texcmd, title))
[docs]def equation(*args): """Add an equation to a notebook. If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-10-31' __modified__ = '2012-10-31' op = optparse.OptionParser(usage='%prog equation [options] TITLE...', version=__modified__, description=equation.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-#', '--numbered', default=False, action='store_true', help='add numbered equation') op.add_option('-l', '--label', default='', help='set label') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) # select notebook notebook = select_notebook(opts.notebook) # add the equation eq = ' '.join(posargs) s = '' if opts.numbered else '*' text('--notebook', notebook, '\\begin{equation%s}' % s) text('--notebook', notebook, eq) if opts.label: text('--notebook', notebook, '\label{%s}' % opts.label) text('--notebook', notebook, '\\end{equation%s}' % s)
[docs]def figure(*args): """Add a figure to a notebook. If the selected notebook does not exist, it is created. By default, today's notebook is used. To do: --> determine size of the graphics file and choose width and height accordingly --> do not overwrite existing graphics files (maybe only by option), instead rename the file""" __created__ = '2012-10-31' __modified__ = '2012-11-20' op = optparse.OptionParser(usage='%prog figure [options] TITLE...', version=__modified__, description=figure.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-c', '--caption', default='', help='set caption') op.add_option('-l', '--label', default='', help='set label') opts, posargs = op.parse_args(args=list(args)) # select notebook notebook = select_notebook(opts.notebook) # check if all graphics files exist for graphicsfile in posargs: if not os.path.isfile(graphicsfile): print >>sys.stderr, \ 'lnote figure: cannot copy %s: no such file' % graphicsfile sys.exit(1) # add the figure linebreak('--notebook', notebook) text('--notebook', notebook, '\\begin{figure}') text('--notebook', notebook, '\centering') for graphicsfile in posargs: # copy the graphics file into the notebook directory shutil.copy(graphicsfile, os.path.join(LNOTE_DIR, notebook)) # add graphics file text('--notebook', notebook, '\includegraphics[width=.72\\textwidth]{%s}'\ % os.path.basename(graphicsfile)) if opts.caption: text('--notebook', notebook, '\caption{%s}' % opts.caption) if opts.label: text('--notebook', notebook, '\label{%s}' % opts.label) else: if len(posargs) == 1: text('--notebook', notebook, '\label{%s}' % os.path.basename(posargs[0]).rsplit('.', 1)[0]) text('--notebook', notebook, '\\end{figure}')
[docs]def prune(*args): """Remove the last line of a notebook. If the selected notebook does not exist, it is created. By default, today's notebook is used. To do: --> negative numbers remove lines from the beginning of the notebook""" __created__ = '2012-10-31' __modified__ = '2012-11-01' op = optparse.OptionParser(usage='%prog prune [options]', version=__modified__, description=prune.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-#', '--number', default=1, type=int, help='set number of lines to remove') op.add_option('-f', '--force', default=False, action='store_true', help='never prompt') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) if len(posargs) != 0: print >>sys.stderr, 'lnote prune: not expecting any positional parameters' sys.exit(1) if opts.number < 0: op.error('bad number of lines: %i. ' % opts.number+\ 'Must be non-negative integer') if opts.number == 0: return # select notebook notebook = select_notebook(opts.notebook) # copy the whole file, but leave out the last couple of lines with open(texpath(notebook), 'r') as f: lines = f.readlines() if len(lines[-opts.number:]) == 0: return # prompt if not opts.force: # print the lines select = ''.join(lines[-opts.number:]) #.strip() sys.stdout.write('\033[7m%s\033[0m' % select) message = 'remove the above line%s from notebook "%s"? ' \ % (plural(opts.number), notebook) answer = raw_input(message).lower() if not answer or not 'yes'.startswith(answer): return # overwrite tex-file, leave out the last couple of lines with open(texpath(notebook), 'w') as f: for line in lines[:-opts.number]: f.write(line)
[docs]def item(*args): """Add a list item to a notebook. If the notebook ends with a list of the chosen type, continue that list. If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-11-01' __modified__ = '2012-11-01' op = optparse.OptionParser(usage='%prog item [options] TEXT...', version=__modified__, description=item.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.add_option('-t', '--type', default='itemize', help='set list type') op.add_option('-l', '--label', default=None, type=str, help='set label') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) ITEMIZE = 1 ENUMERATE = 2 DESCRIPTION = 3 if 'itemize'.startswith(opts.type.lower()): listtype = ITEMIZE elif 'enumerate'.startswith(opts.type.lower()): listtype = ENUMERATE elif 'description'.startswith(opts.type.lower()): listtype = DESCRIPTION else: print >>sys.stderr, 'lnote item: unknown list type: %s' % opts.type sys.exit(1) if listtype is ITEMIZE: listtypename = 'itemize' elif listtype is ENUMERATE: listtypename = 'enumerate' elif listtype is DESCRIPTION: listtypename = 'description' # select notebook try: notebook = select_notebook(opts.notebook) except SelectNotebookError: print >>sys.stderr, \ 'lnote list: notebook not found: %s' % opts.notebook sys.exit(1) # check last line of the notebook with open(texpath(notebook), 'r') as f: lines = f.readlines() #while not lines[-1].strip(): #lines = lines[:-1] if '\\end{%s}' % listtypename in lines[-1]: prune('--notebook', notebook, '--force') else: append_text(notebook, '\\begin{%s}' % listtypename) # append item if opts.label is None: append_text(notebook, '\item %s' % ' '.join(posargs)) else: append_text(notebook, '\item[%s] %s' % (opts.label, ' '.join(posargs))) # end list environment append_text(notebook, '\\end{%s}' % listtypename)
[docs]def marginnote(*args): """Add a marginal note to the notebook (using the LaTeX command "marginpar"). If the selected notebook does not exist, it is created. By default, today's notebook is used.""" __created__ = '2012-11-15' __modified__ = '2012-11-15' op = optparse.OptionParser(usage='%prog marginnote [options] TEXT...', version=__modified__, description=marginnote.__doc__) op.add_option('-n', '--notebook', default=None, help='select notebook') op.allow_interspersed_args = False opts, posargs = op.parse_args(args=list(args)) # select notebook notebook = select_notebook(opts.notebook) # add the marginal note notetext = ' '.join(posargs) text('--notebook', notebook, '\marginpar{%s}' % notetext) #=====================# # Auxiliary functions # #=====================#
[docs]def printcols(strlist, ret=False): """Print the strings in the given list in column by column (similar the bash command "ls"), respecting the width of the shell window. If ret is True, give back the resulting string instead of printing it to stdout.""" __created__ = '2012-09-26' __modified__ = '2012-11-19' # based on tb.misc.printcols (written 2011-09-13) # former tb.printcols from 2011-02-13 until 2011-08-02 # former mytools.printcols if len(strlist) == 0: return numstr = len(strlist) cols = get_cols() maxwidth = max([len(s) for s in strlist]) numcols = cols/(maxwidth+2) numrows = int(ceil(1.*numstr/numcols)) # print the list result = '' for rind in xrange(numrows): for cind in xrange(numcols): sind = cind*numrows+rind if sind < numstr: result += strlist[sind]+' '*(maxwidth-len(strlist[sind])+2) result = result.rstrip()+'\n' # return or print result if ret: return result.rstrip() else: print result.rstrip()
[docs]def ceil(x): """Return the ceiling of x. This exists as a substitute for numpy.ceil, to avoid importing the huge numpy module just for this function.""" __created__ = '2012-10-29' __modified__ = '2012-10-29' if int(x) == x or x <= 0: return int(x) else: return int(x)+1
[docs]def get_cols(): """Try to get the width of the shell window (will only work on Unix systems).""" try: return int(commands.getoutput('tput cols')) except ValueError: # return default width return 80 #def require_file(filename): #"""If the file does not exist, create it, and also all directories along the #given path.""" #__created__ = '2012-10-29' #__modified__ = '2012-10-29' ## copied from pool (written 2012-09-19 until 2012-09-20) #filename = os.path.expanduser(filename) #if not os.path.isfile(filename): #path, fname = os.path.split(filename) #if not os.path.isdir(path): #os.makedirs(path) #open(filename, 'w').close() #if not os.path.isfile(filename): #raise IOError, 'unable to create file' #def require_day(day): #"""If the tex-file and the directory for the given day do not yet exist, #create them.""" #__created__ = '2012-10-29' #__modified__ = '2012-10-29' #require_file(texpath(day))
[docs]def texpath(notebook): """Return the path to the tex-file of the given notebook.""" __created__ = '2012-10-29' __modified__ = '2012-10-31' return os.path.join(LNOTE_DIR, notebook, notebook+'.tex')
[docs]def dirpath(notebook): """Return the path to the directory of the given notebook.""" __created__ = '2012-10-29' __modified__ = '2012-10-31' return os.path.join(LNOTE_DIR, notebook) #def append_linebreak(notebook): #"""Append linebreak to the given notebook.""" #__created__ = '2012-10-29' #__modified__ = '2012-10-31' #with open(texpath(notebook), 'a') as texfile: #texfile.write(' a\n')
[docs]def append_text(notebook, text): """Append text to the given notebook.""" __created__ = '2012-10-29' __modified__ = '2012-10-31' if text != '\n': text = text.strip() text += '\n' with open(texpath(notebook), 'a') as texfile: texfile.write(text)
[docs]class DayFormatError(BaseException): __created__ = '2012-10-31' __modified__ = '2012-10-31'
[docs]def opt2day(opt): """Convert option string to a certain date. Return as Julian day number (integer).""" __created__ = '2012-10-29' __modified__ = '2012-10-31' opt = opt.strip() now = time.localtime() if 'today'.startswith(opt.lower()): return greg2jdn(now.tm_year, now.tm_mon, now.tm_mday) elif 'yesterday'.startswith(opt.lower()): return greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)-1 elif 'tomorrow'.startswith(opt.lower()): return greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)+1 elif 'first'.startswith(opt.lower()): possible = fnmatch.filter(os.listdir(LNOTE_DIR), '????-??-??') possible.sort() first = possible[0] year, mon, mday = first.split('-') thatday = greg2jdn(year, mon, mday) return thatday, thatday+1 elif 'last'.startswith(opt.lower()): possible = fnmatch.filter(os.listdir(LNOTE_DIR), '????-??-??') possible.sort() last = possible[-1] year, mon, mday = last.split('-') return greg2jdn(year, mon, mday) elif opt.count('/') == 2: format = '%Y/%m/%d' if len(opt.split('/')[0]) == 4 else '%y/%m/%d' try: then = time.strptime(opt, format) except ValueError: raise DayFormatError, 'bad day format: %s' % opt return greg2jdn(then.tm_year, then.tm_mon, then.tm_mday) elif opt.count('-') == 2: format = '%Y-%m-%d' if len(opt.split('-')[0]) == 4 else '%y-%m-%d' try: then = time.strptime(opt, format) except ValueError: raise DayFormatError, 'bad day format: %s' % opt return greg2jdn(then.tm_year, then.tm_mon, then.tm_mday) elif opt.count('.') == 2: if opt[-1] == '.': opt += str(now.tm_year) format = '%d.%m.%Y' if len(opt.split('.')[-1]) == 4 else '%d.%m.%y' try: then = time.strptime(opt, format) except ValueError: raise DayFormatError, 'bad day format: %s' % opt return greg2jdn(then.tm_year, then.tm_mon, then.tm_mday) else: raise DayFormatError, 'bad day format: %s' % opt
[docs]def opt2days(opt): """Convert option string to a list of dates. Return list of integers (Julian day numbers).""" __created__ = '2012-10-29' __modified__ = '2012-10-30' return [opt2day(x) for x in opt.split(',')]
[docs]class DayRangeFormatError(DayFormatError): __created__ = '2012-10-31' __modified__ = '2012-10-31'
[docs]def opt2dayrange(opt): """Convert option string to a timerange. Return as tuple with two integers (JDN1, JDN2+1) (Julian day numbers).""" ### support ranges like "lastweek" or "thisweek" or "nextweek" etc. __created__ = '2012-10-29' __modified__ = '2012-10-31' opt = opt.strip() months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] now = time.localtime() try: if not opt: # return "empty" range, not selecting any days return (opt2day('today'),)*2 if opt.count('-') > 1: raise ValueError, 'only one dash (-) allowed in timerange' if opt.count('-') == 1: begin, end = opt.split('-') for m, mon in enumerate(months): if begin and mon.startswith(begin.lower()): rbegin = greg2jdn(now.tm_year, m+1, 1) break else: if 'today'.startswith(begin.lower()): rbegin = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday) elif 'yesterday'.startswith(begin.lower()): rbegin = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)-1 elif 'tomorrow'.startswith(begin.lower()): rbegin = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)+1 elif 'first'.startswith(begin.lower()): possible = fnmatch.filter(os.listdir(LNOTE_DIR), '????-??-??') possible.sort() first = possible[0] year, mon, mday = first.split('-') rbegin = greg2jdn(year, mon, mday) elif begin and begin.count('/')+begin.count('-')+begin.count('.') == 0: rbegin = greg2jdn(int(begin), 1, 1) elif begin and begin.count('/') == 1: year, mon = begin.split('/') rbegin = greg2jdn(year, mon, 1) else: rbegin = opt2day(begin) if begin else 0 for m, mon in enumerate(months): if end and mon.startswith(end.lower()): rend = greg2jdn(now.tm_year, m+2, 1) break else: if 'today'.startswith(end.lower()): rend = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)+1 elif 'yesterday'.startswith(end.lower()): rend = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday) elif 'tomorrow'.startswith(end.lower()): rend = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)+2 elif 'last'.startswith(end.lower()): possible = fnmatch.filter(os.listdir(LNOTE_DIR), '????-??-??') possible.sort() last = possible[-1] year, mon, mday = last.split('-') rend = greg2jdn(year, mon, mday)+1 elif end and end.count('/')+end.count('-')+end.count('.') == 0: rend = greg2jdn(int(end)+1, 1, 1) elif end and end.count('/') == 1: year, mon = end.split('/') rend = greg2jdn(year, int(mon)+1, 1) else: rend = opt2day(end)+1 if end else opt2day('today')+1 return rbegin, rend else: for m, mon in enumerate(months): if mon.startswith(opt.lower()): return greg2jdn(now.tm_year, m+1, 1), greg2jdn(now.tm_year, m+2, 1) else: if 'today'.startswith(opt.lower()): today = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday) return today, today+1 elif 'yesterday'.startswith(opt.lower()): yesterday = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)-1 return yesterday, yesterday+1 elif 'tomorrow'.startswith(opt.lower()): tomorrow = greg2jdn(now.tm_year, now.tm_mon, now.tm_mday)+1 return tomorrow, tomorrow+1 elif 'first'.startswith(opt.lower()): possible = fnmatch.filter(os.listdir(LNOTE_DIR), '????-??-??') possible.sort() first = possible[0] year, mon, mday = first.split('-') thatday = greg2jdn(year, mon, mday) return thatday, thatday+1 elif 'last'.startswith(opt.lower()): possible = fnmatch.filter(os.listdir(LNOTE_DIR), '????-??-??') possible.sort() last = possible[-1] year, mon, mday = last.split('-') thatday = greg2jdn(year, mon, mday) return thatday, thatday+1 elif opt and opt.count('/')+opt.count('-')+opt.count('.') == 0: return greg2jdn(int(opt), 1, 1), greg2jdn(int(opt)+1, 1, 1) elif opt and opt.count('/') == 1: year, mon = opt.split('/') return greg2jdn(year, mon, 1), greg2jdn(year, int(mon)+1, 1) else: j = opt2day(opt) return j, j+1 except (DayFormatError, ValueError): raise DayRangeFormatError, 'bad dayrange format: %s' % opt
[docs]def opt2dayranges(opt): """Convert option string to a list of timeranges. Return as list of tuples with two integers (JDN1, JDN2+1) (Julian day numbers).""" __created__ = '2012-10-29' __modified__ = '2012-10-31' return [opt2dayrange(x) for x in opt.split(',')]
[docs]def greg2jdn(year, mon, mday): """Convert Gregorian date to Julian day number. Reference: http://en.wikipedia.org/wiki/Julian_Date""" __created__ = '2012-10-30' __modified__ = '2012-10-30' year, mon, mday = int(year), int(mon), int(mday) a = (14-mon)/12 y = year+4800-a m = mon+12*a-3 return mday+(153*m+2)/5+365*y+y/4-y/100+y/400-32045
[docs]def jdn2greg(jdn): """Convert Julian day number to Gregorian date. Reference: http://en.wikipedia.org/wiki/Julian_Date""" __created__ = '2012-10-31' __modified__ = '2012-10-31' J = int(jdn+.5) j = J+32044 g = j/146097; dg = j%146097 c = (dg/36524+1)*3/4; dc = dg-c*36524 b = dc/1461; db = dc%1461 a = (db/365+1)*3/4; da = db-a*365 y = g*400+c*100+b*4+a m = (da*5+308)/153-2 d = da-(m+4)*153/5+122 year = y-4800+(m+2)/12 mon = (m+2)%12+1 mday = d+1 return year, mon, mday
[docs]def date2filename(year, mon, mday): """Convert Gregorian date to filename.""" __created__ = '2012-10-31' __modified__ = '2012-10-31' return '%04i-%02i-%02i' % (year, mon, mday)
[docs]def select_notebook(pattern, forgiving=False): """Select a single notebook based on the given pattern. If forgiving is True, create a new notebook if the given one doesn't exist. Otherwise, only create a new notebook if pattern is None or empty.""" __created__ = '2012-10-31' __modified__ = '2012-10-31' if pattern: # try to find a notebook with that name if pattern in os.listdir(LNOTE_DIR): notebook = pattern else: # try to interprete string as a day try: year, mon, mday = jdn2greg(opt2day(pattern)) notebook = date2filename(year, mon, mday) if notebook not in os.listdir(LNOTE_DIR): create(notebook) except DayFormatError: if forgiving: notebook = pattern create(notebook) else: raise SelectNotebookError, pattern else: # use today year, mon, mday = jdn2greg(opt2day('today')) notebook = date2filename(year, mon, mday) if notebook not in os.listdir(LNOTE_DIR): create(notebook) return notebook
[docs]class SelectNotebookError(BaseException): __created__ = '2012-10-31' __modified__ = '2012-10-31'
def select_notebooks(*patterns, **kwargs): """Select notebooks based on the given string patterns. If unique is True, each notebook will be added to the list only once. If skipmissing is True, no exception is raised if there is nothing found for a pattern.""" __created__ = '2012-10-31' __modified__ = '2013-11-29' # get keyword arguments unique = kwargs.pop('unique', False) forgiving = kwargs.pop('forgiving', False) if len(kwargs) != 0: raise TypeError, \ 'select_notebooks() got an unexpected keyword argument \'%s\'' \ % kwargs.keys()[0] notebooks = [] for pattern in patterns: # first try to glob raised = False try: results = glob.glob(os.path.join(LNOTE_DIR, pattern)) except: raised = True if not raised and len(results) != 0: #results.sort() for result in results: notebook = os.path.basename(result) if not unique or notebook not in notebooks: notebooks.append(notebook) else: # try to interprete the pattern as a day or dayrange try: dayrange = opt2dayrange(pattern) except DayRangeFormatError: if forgiving: continue else: raise SelectNotebookError, pattern # select all notebooks according to dayrange dirnames = os.listdir(LNOTE_DIR) dirnames.sort() for dirname in dirnames: try: date = time.strptime(dirname, '%Y-%m-%d') except ValueError: continue jdn = greg2jdn(date.tm_year, date.tm_mon, date.tm_mday) if jdn >= dayrange[0] and jdn < dayrange[1]: if not unique or dirname not in notebooks: notebooks.append(dirname) return notebooks
[docs]def plural(number): """If abs(number) == 1, return "", otherwise return "s". For conveniently appending the plural "s" to words depending on some number.""" __created__ = '2012-11-02' __modified__ = '2012-11-02' return '' if abs(number) == 1 else 's'
[docs]def get_size(path): """Return total size of a file or directory in bytes. Reference: http://stackoverflow.com/questions/1392413/calculating-a-directory-size-using-python """ __created__ = '2013-11-08' __modified__ = '2013-11-08' if os.path.isdir(path): total_size = 0 for dirpath, dirnames, filenames in os.walk(path): for f in filenames: fp = os.path.join(dirpath, f) total_size += os.path.getsize(fp) return total_size elif os.path.isfile(path): return os.path.getsize(path) else: return -1
[docs]def get_mtime(path): """Return last modification time of a file or all files within a directory tree (in seconds since the epoch). """ __created__ = '2013-11-08' __modified__ = '2013-11-08' if os.path.isdir(path): mtime = os.path.getmtime(path) for dirpath, dirnames, filenames in os.walk(path): newmtime = os.path.getmtime(dirpath) if newmtime > mtime: mtime = newmtime for f in filenames: fp = os.path.join(dirpath, f) newmtime = os.path.getmtime(fp) if newmtime > mtime: mtime = newmtime return mtime elif os.path.isfile(path): return os.path.getmtime(path) else: return -1 #==============# # Main program # #==============# # map commands (second element of sys.argv) to functions
_cmd2func = { ## here, positional parameters are notebooks ## 'create' : create, 'c' : create, 'delete' : delete, 'd' : delete, 'rename' : rename, 'r' : rename, 'list' : listn, 'l' : listn, 'edit' : edit, 'e' : edit, 'merge' : merge, 'm' : merge, 'export' : export, 'x' : export, 'view' : view, 'v' : view, 'path' : path, ## here, positional parameters are not notebooks but text ## ## instead, notebooks are selected using the -n option ## 'text' : text, 't' : text, 'linebreak' : linebreak, 'b' : linebreak, 'equation' : equation, 'q' : equation, 'section' : section, 's' : section, 'paragraph' : paragraph, 'p' : paragraph, 'item' : item, 'i' : item, 'prune' : prune, 'u' : prune, 'marginnote': marginnote, 'n' : marginnote, ## here, positional parameters are files (graphics files) ## 'figure' : figure, 'f' : figure }
[docs]def call(): """This function should be called by an executable file with the following content: >>> import sys, lnote >>> if __name__ == '__main__': >>> sys.exit(lnote.call())""" # 2012-10-29 - 2012-11-15 # return words for custom tab completion if len(sys.argv) == 2 and sys.argv[1] == '--comp-words': keys = _cmd2func.keys() keys.sort() print ' '.join(keys) sys.exit(0) # to enable custom tab completion, add the following lines to your .bashrc # (see http://aplawrence.com/Unix/customtab.html#ixzz27bkFS2Y0): #lnotewords='create edit equation figure linebreak list paragraph prune #rename section text merge delete export view' #notebooks="`ls ~/data/lnote` today yesterday tomorrow" #_lnote() #{ #local curw #COMPREPLY=() #curw=${COMP_WORDS[COMP_CWORD]} #cur1=${COMP_WORDS[1]} #if [ $COMP_CWORD == 1 ] #then #COMPREPLY=($(compgen -W '$lnotewords' -- $curw)) #else #if [ "$cur1" == "figure" ] || [ "$cur1" == "f" ] #then #COMPREPLY=($(compgen -A file -- $curw)) #elif [ "$cur1" == "create" ] || [ "$cur1" == "c" ] \ #|| [ "$cur1" == "delete" ] || [ "$cur1" == "d" ] \ #|| [ "$cur1" == "rename" ] || [ "$cur1" == "r" ] \ #|| [ "$cur1" == "list" ] || [ "$cur1" == "l" ] \ #|| [ "$cur1" == "edit" ] || [ "$cur1" == "e" ] \ #|| [ "$cur1" == "merge" ] || [ "$cur1" == "m" ] \ #|| [ "$cur1" == "view" ] || [ "$cur1" == "v" ] \ #|| [ "$cur1" == "export" ] || [ "$cur1" == "x" ] #then #COMPREPLY=($(compgen -W '$notebooks' -- $curw)) #else #COMPREPLY=($(compgen -W '' -- $curw)) #fi #fi #return 0 #} #complete -F _lnote -o dirnames lnote if len(sys.argv) == 1 or sys.argv[1] in ('-?', '--help'): # display help cmds = {} for cmd, func in _cmd2func.iteritems(): func = func.__name__ if func not in cmds: cmds[func] = {'longs': [], 'shorts': []} if len(cmd) == 1: cmds[func]['shorts'].append(cmd) else: cmds[func]['longs'].append(cmd) keys = cmds.keys() keys.sort() cmdstrings = [] for key in keys: cmd = cmds[key] cmdstring = '' if len(cmd['longs']) != 0: cmdstring += cmd['longs'][0] if len(cmd['shorts']) != 0: cmdstring += ' (' cmdstring += ', '.join(short for short in cmd['shorts']) cmdstring += ')' else: cmdstring += ', '.join(short for short in cmd['shorts']) cmdstrings.append(cmdstring) print __doc__ print print 'Available commands (with shortcuts):' printcols(cmdstrings) print print 'To get help to a specific command,', print 'use "-?", e.g. "lnote create -?"' else: # execute command try: func = _cmd2func[sys.argv[1]] except KeyError: print >>sys.stderr, '%s: command not found. ' % sys.argv[1] + \ 'Type "lnote -?" for a list of lnote commands' sys.exit(1) func(*sys.argv[2:]) sys.exit(0)
if __name__ == '__main__': call() sys.exit(0)