diff --git a/NOTES b/NOTES new file mode 100644 index 0000000..c52ecae --- /dev/null +++ b/NOTES @@ -0,0 +1,23 @@ ++ requires config file (default repo, open command, ...) +- printing should include templating engine and several templates for bib types and output + * chose existing engine +- tests... +- import command for interactive import with auto bib + * basic title and author search in pdf + * online services (scholar, etc.) +- add command does not require pdf -> add from bib + +About strings: +-------------- +- pybtex seems to store entries as utf-8 (TODO: check) +- so assumption is made that everything is utf-8 +- conversions are performed at print time + +Config values: +-------------- +[papers] +open-cmd = open +edit-cmd = edit +import-copy = True +import-move = False +terminal-encoding = from locale or utf8 diff --git a/papers/beets_ui.py b/papers/beets_ui.py new file mode 100644 index 0000000..b633fd6 --- /dev/null +++ b/papers/beets_ui.py @@ -0,0 +1,58 @@ +# This file contains functions taken from the user interface of the beet +# tool (http://beets.radbox.org). +# +# Copyright 2013, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +import locale +import sys +from ConfigParser import NoOptionError + + +class UserError(Exception): + """UI exception. Commands should throw this in order to display + nonrecoverable errors to the user. + """ + pass + + +def _encoding(config): + """Tries to guess the encoding used by the terminal.""" + # Configured override? + try: + return config.get('papers', 'terminal-encoding') + except NoOptionError: + # Determine from locale settings. + try: + return locale.getdefaultlocale()[1] or 'utf8' + except ValueError: + # Invalid locale environment variable setting. To avoid + # failing entirely for no good reason, assume UTF-8. + return 'utf8' + + +def input_(): + """Get input and decodes the result to a Unicode string. + Raises a UserError if stdin is not available. The prompt is sent to + stdout rather than stderr. A printed between the prompt and the + input cursor. + """ + # raw_input incorrectly sends prompts to stderr, not stdout, so we + # use print() explicitly to display prompts. + # http://bugs.python.org/issue1927 + try: + resp = raw_input() + except EOFError: + raise UserError('stdin stream ended while input required') + return resp.decode(sys.stdin.encoding or 'utf8', 'ignore') diff --git a/papers/commands/add_cmd.py b/papers/commands/add_cmd.py index cd035ea..9b05911 100644 --- a/papers/commands/add_cmd.py +++ b/papers/commands/add_cmd.py @@ -8,7 +8,7 @@ def parser(subparsers, config): return parser -def command(config, pdffile, bibfile): +def command(config, ui, pdffile, bibfile): """ :param pdffilepath path (no url yet) to a pdf or ps file :param bibtex bibtex file (in .bib, .bibml or .yaml format. diff --git a/papers/commands/add_library_cmd.py b/papers/commands/add_library_cmd.py index 60dd773..645dd08 100644 --- a/papers/commands/add_library_cmd.py +++ b/papers/commands/add_library_cmd.py @@ -9,7 +9,7 @@ def parser(subparsers, config): return parser -def command(config, bibfile): +def command(config, ui, bibfile): """ :param bibtex bibtex file (in .bib, .bibml or .yaml format. """ diff --git a/papers/commands/edit_cmd.py b/papers/commands/edit_cmd.py index d567705..8bec6c9 100644 --- a/papers/commands/edit_cmd.py +++ b/papers/commands/edit_cmd.py @@ -14,7 +14,7 @@ def parser(subparsers, config): return parser -def command(config, reference): +def command(config, ui, reference): rp = repo.Repository.from_directory() key = rp.citekey_from_ref(reference, fatal=True) filepath = rp.path_to_paper_file(key, 'bib') diff --git a/papers/commands/import_cmd.py b/papers/commands/import_cmd.py index 3d9e029..fb4b039 100644 --- a/papers/commands/import_cmd.py +++ b/papers/commands/import_cmd.py @@ -19,7 +19,7 @@ def parser(subparsers, config): return parser -def command(config, bibpath, copy): +def command(config, ui, bibpath, copy): """ :param pdffilepath path (no url yet) to a pdf or ps file :param bibtex bibtex file (in .bib, .bibml or .yaml format. diff --git a/papers/commands/init_cmd.py b/papers/commands/init_cmd.py index 03db14a..bd98eb3 100644 --- a/papers/commands/init_cmd.py +++ b/papers/commands/init_cmd.py @@ -12,7 +12,7 @@ def parser(subparsers, config): return parser -def command(config): +def command(config, ui): """Create a .papers directory""" papersdir = os.getcwd() + '/.papers' if not os.path.exists(papersdir): diff --git a/papers/commands/list_cmd.py b/papers/commands/list_cmd.py index 3951cb8..27286f7 100644 --- a/papers/commands/list_cmd.py +++ b/papers/commands/list_cmd.py @@ -11,7 +11,7 @@ def parser(subparsers, config): return parser -def command(config): +def command(config, ui): rp = repo.Repository.from_directory() articles = [] for n, p in enumerate(rp.all_papers()): diff --git a/papers/commands/open_cmd.py b/papers/commands/open_cmd.py index 01a7266..490c75e 100644 --- a/papers/commands/open_cmd.py +++ b/papers/commands/open_cmd.py @@ -13,7 +13,7 @@ def parser(subparsers, config): return parser -def command(config, citekey): +def command(config, ui, citekey): rp = repo.Repository.from_directory() paper = rp.paper_from_ref(citekey, fatal=True) try: diff --git a/papers/commands/websearch_cmd.py b/papers/commands/websearch_cmd.py index c49f4af..f12b70f 100644 --- a/papers/commands/websearch_cmd.py +++ b/papers/commands/websearch_cmd.py @@ -3,11 +3,14 @@ import urllib def parser(subparsers, config): - parser = subparsers.add_parser('websearch', help="launch a search on Google Scholar") - parser.add_argument("search_string", help="the search query (anything googly is possible)") + parser = subparsers.add_parser('websearch', + help="launch a search on Google Scholar") + parser.add_argument("search_string", + help="the search query (anything googly is possible)") return parser -def command(config, search_string): - url = 'https://scholar.google.fr/scholar?q={}&lr='.format(urllib.quote_plus(search_string)) +def command(config, ui, search_string): + url = ("https://scholar.google.fr/scholar?q=%s&lr=" + % (urllib.quote_plus(search_string))) webbrowser.open(url) diff --git a/papers/configs.py b/papers/configs.py index 83bf3b6..934458d 100644 --- a/papers/configs.py +++ b/papers/configs.py @@ -10,12 +10,15 @@ except KeyError: DEFAULT_IMPORT_COPY = 'yes' DEFAULT_IMPORT_MOVE = 'no' +DEFAULT_COLOR = 'yes' + CONFIG = ConfigParser.SafeConfigParser({ 'open-cmd': DEFAULT_OPEN_CMD, 'edit-cmd': DEFAULT_EDIT_CMD, 'import-copy': DEFAULT_IMPORT_COPY, 'import-move': DEFAULT_IMPORT_MOVE, + 'color': DEFAULT_COLOR, }) CONFIG.add_section('papers') diff --git a/papers/papers b/papers/papers index f384621..994940f 100755 --- a/papers/papers +++ b/papers/papers @@ -5,6 +5,7 @@ import argparse import collections +from papers.ui import UI from papers import configs from papers import commands @@ -20,6 +21,7 @@ cmds = collections.OrderedDict([ ]) config = configs.read_config() +ui = UI(config) parser = argparse.ArgumentParser(description="research papers repository") subparsers = parser.add_subparsers(title="valid commands", dest="command") @@ -29,6 +31,7 @@ for cmd_mod in cmds.values(): args = parser.parse_args() args.config = config +args.ui = ui cmd = args.command del args.command diff --git a/papers/ui.py b/papers/ui.py new file mode 100644 index 0000000..85cda81 --- /dev/null +++ b/papers/ui.py @@ -0,0 +1,65 @@ +from beets_ui import _encoding, input_ + +from color import colored + + +class UI: + """UI class. Stores configuration parameters and system information. + """ + + def __init__(self, config): + self.encoding = _encoding(config) + self.color = config.getboolean('papers', 'color') + + def colored(self, s, *args, **kwargs): + if self.color: + return colored(s, *args, **kwargs) + else: + return s + + def print_(self, *strings): + """Like print, but rather than raising an error when a character + is not in the terminal's encoding's character set, just silently + replaces it. + """ + txt = [s.encode(self.encoding, 'replace') + if isinstance(s, unicode) else s + for s in strings] + print(' '.join(txt)) + + def input_choice(self, options, option_chars, default=None, question=''): + """Ask the user to chose between a set of options. The iser is asked + to input a char corresponding to the option he choses. + + :param options: list of strings + list of options + :param option_chars: list of chars + chars used to identify options, should be lowercase and not + contain duplicates + :param default: int + default if no option is accepted, if None answer is required + :param question: string + :returns: int + the index of the chosen option + """ + displayed_chars = [s.upper() if i == default else s + for i, s in enumerate(option_chars)] + option_str = ', '.join(["[%s]%s" % (self.colored(c, 'cyan'), o) + for c, o in zip(displayed_chars, options)]) + self.print_(question, option_str) + while True: + answer = input_() + if answer is None or answer == '': + if default is not None: + return default + else: + try: + return option_chars.index(answer.lower()) + except ValueError: + pass + self.print_('Incorrect option.', option_str) + + def input_yn(self, question='', default='y'): + d = 0 if default in (True, 'y', 'yes') else 1 + return (True, False)[self.input_choice(['yes', 'no'], ['y', 'n'], + default=d, question=question)]