From 786cd64e3b63b5d8c59eb9f05fd5271b7446cec8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Feb 2013 18:03:50 +0530 Subject: [PATCH] Linux installer: Install completion for zsh in addition to bash --- src/calibre/linux.py | 214 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 196 insertions(+), 18 deletions(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index a801e10488..c50bf52f84 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -123,6 +123,169 @@ os.remove(os.path.abspath(__file__)) # }}} +class ZshCompleter(object): # {{{ + + def __init__(self, opts): + self.opts = opts + self.dest = None + base = os.path.dirname(self.opts.staging_sharedir) + for x in ('vendor-completions', 'vendor-functions', 'site-functions'): + c = os.path.join(base, 'zsh', x) + if os.path.isdir(c) and os.access(c, os.W_OK): + self.dest = os.path.join(c, '_calibre') + self.commands = {} + + def get_options(self, parser, cover_opts=('--cover',), opf_opts=('--opf',), + file_map={}): + options = parser.option_list + for group in parser.option_groups: + options += group.option_list + for opt in options: + lo, so = opt._long_opts, opt._short_opts + if opt.takes_value(): + lo = [x+'=' for x in lo] + so = [x+'+' for x in so] + ostrings = lo + so + if len(ostrings) > 1: + ostrings = u'{%s}'%','.join(ostrings) + else: + ostrings = ostrings[0] + exclude = u'' + if opt.dest is None: + exclude = u"'(- *)'" + h = opt.help or '' + h = h.replace('"', "'").replace('[', '(').replace( + ']', ')').replace('\n', ' ').replace(':', '\\:') + h = h.replace('%default', type(u'')(opt.default)) + arg = '' + if opt.takes_value(): + arg = ':"%s":'%h + if opt.dest in {'to_dir', 'outbox', 'with_library', 'library_path'}: + arg += "'_path_files -/'" + elif opt.choices: + arg += "(%s)"%'|'.join(opt.choices) + elif set(file_map).intersection(set(opt._long_opts)): + k = set(file_map).intersection(set(opt._long_opts)) + exts = file_map[tuple(k)[0]] + if exts: + arg += "'_files -g \"%s\"'"%(' '.join('*.%s'%x for x in + tuple(exts) + tuple(x.upper() for x in exts))) + else: + arg += "_files" + elif (opt.dest in {'pidfile', 'attachment'}): + arg += "_files" + elif set(opf_opts).intersection(set(opt._long_opts)): + arg += "'_files -g \"*.opf\"'" + elif set(cover_opts).intersection(set(opt._long_opts)): + arg += "'_files -g \"%s\"'"%(' '.join('*.%s'%x for x in + tuple(pics) + tuple(x.upper() for x in pics))) + + help_txt = u'"[%s]"'%h + yield u'%s%s%s%s '%(exclude, ostrings, help_txt, arg) + + def opts_and_exts(self, name, op, exts, cover_opts=('--cover',), + opf_opts=('--opf',), file_map={}): + if not self.dest: return + exts = set(exts).union(x.upper() for x in exts) + pats = ('*.%s'%x for x in exts) + extra = ("'*:filename:_files -g \"%s\"' "%' '.join(pats),) + opts = '\\\n '.join(tuple(self.get_options( + op(), cover_opts=cover_opts, opf_opts=opf_opts, file_map=file_map)) + extra) + txt = '_arguments -s \\\n ' + opts + self.commands[name] = txt + + def opts_and_words(self, name, op, words, takes_files=False): + if not self.dest: return + extra = ("'*:filename:_files' ",) if takes_files else () + opts = '\\\n '.join(tuple(self.get_options(op())) + extra) + txt = '_arguments -s \\\n ' + opts + self.commands[name] = txt + + def do_calibredb(self, f): + import calibre.library.cli as cli + from calibre.customize.ui import available_catalog_formats + parsers, descs = {}, {} + for command in cli.COMMANDS: + op = getattr(cli, '%s_option_parser'%command) + args = [['t.epub']] if command == 'catalog' else [] + p = op(*args) + if isinstance(p, tuple): + p = p[0] + parsers[command] = p + lines = [x.strip().partition('.')[0] for x in p.usage.splitlines() if x.strip() and + not x.strip().startswith('%prog')] + descs[command] = lines[0] + + f.write('\n_calibredb_cmds() {\n local commands; commands=(\n') + f.write(' {-h,--help}":Show help"\n') + f.write(' "--version:Show version"\n') + for command, desc in descs.iteritems(): + f.write(' "%s:%s"\n'%( + command, desc.replace(':', '\\:').replace('"', '\''))) + f.write(' )\n _describe -t commands "calibredb command" commands \n}\n') + + subcommands = [] + for command, parser in parsers.iteritems(): + exts = [] + if command == 'catalog': + exts = [x.lower() for x in available_catalog_formats()] + elif command == 'set_metadata': + exts = ['opf'] + exts = set(exts).union(x.upper() for x in exts) + pats = ('*.%s'%x for x in exts) + extra = ("'*:filename:_files -g \"%s\"' "%' '.join(pats),) if exts else () + if command in {'add', 'add_format'}: + extra = ("'*:filename:_files' ",) + opts = '\\\n '.join(tuple(self.get_options( + parser)) + extra) + txt = ' _arguments -s \\\n ' + opts + subcommands.append('(%s)'%command) + subcommands.append(txt) + subcommands.append(';;') + + f.write('\n_calibredb() {') + f.write( + r''' + local context curcontext="$curcontext" state line + typeset -A opt_args + local ret=1 + + _arguments -C \ + '1: :_calibredb_cmds' \ + '*::arg:->args' \ + && ret=0 + + case $state in + (args) + case $line[1] in + (-h|--help|--version) + _message 'no more arguments' && ret=0 + ;; + %s + esac + ;; + esac + + return ret + '''%'\n '.join(subcommands)) + f.write('\n}\n\n') + + def write(self): + if self.dest: + self.commands['calibredb'] = ' _calibredb "$@"' + with open(self.dest, 'wb') as f: + f.write('#compdef ' + ' '.join(self.commands)+'\n') + self.do_calibredb(f) + f.write('case $service in\n') + for c, txt in self.commands.iteritems(): + if isinstance(txt, type(u'')): + txt = txt.encode('utf-8') + if isinstance(c, type(u'')): + c = c.encode('utf-8') + f.write(b'%s)\n%s\n;;\n'%(c, txt)) + f.write('esac\n') +# }}} + class PostInstall: def task_failed(self, msg): @@ -217,7 +380,7 @@ class PostInstall: def setup_completion(self): # {{{ try: - self.info('Setting up bash completion...') + self.info('Setting up bash/zsh completion...') from calibre.ebooks.metadata.cli import option_parser as metaop, filetypes as meta_filetypes from calibre.ebooks.lrf.lrfparser import option_parser as lrf2lrsop from calibre.gui2.lrf_renderer.main import option_parser as lrfviewerop @@ -229,6 +392,7 @@ class PostInstall: from calibre.ebooks.oeb.polish.main import option_parser as polish_op, SUPPORTED from calibre.ebooks import BOOK_EXTENSIONS input_formats = sorted(all_input_formats()) + zsh = ZshCompleter(self.opts) bc = os.path.join(os.path.dirname(self.opts.staging_sharedir), 'bash-completion') if os.path.exists(bc): @@ -240,6 +404,9 @@ class PostInstall: f = os.path.join(self.opts.staging_etc, 'bash_completion.d/calibre') if not os.path.exists(os.path.dirname(f)): os.makedirs(os.path.dirname(f)) + if zsh.dest: + self.info('Installing zsh completion to:', zsh.dest) + self.manifest.append(zsh.dest) self.manifest.append(f) complete = 'calibre-complete' if getattr(sys, 'frozen_path', None): @@ -247,20 +414,27 @@ class PostInstall: self.info('Installing bash completion to', f) with open(f, 'wb') as f: + def o_and_e(*args, **kwargs): + f.write(opts_and_exts(*args, **kwargs)) + zsh.opts_and_exts(*args, **kwargs) + def o_and_w(*args, **kwargs): + f.write(opts_and_words(*args, **kwargs)) + zsh.opts_and_words(*args, **kwargs) + f.write('# calibre Bash Shell Completion\n') - f.write(opts_and_exts('calibre', guiop, BOOK_EXTENSIONS)) - f.write(opts_and_exts('lrf2lrs', lrf2lrsop, ['lrf'])) - f.write(opts_and_exts('ebook-meta', metaop, - list(meta_filetypes()), cover_opts=['--cover', '-c'], - opf_opts=['--to-opf', '--from-opf'])) - f.write(opts_and_exts('ebook-polish', polish_op, - [x.lower() for x in SUPPORTED], cover_opts=['--cover', '-c'], - opf_opts=['--opf', '-o'])) - f.write(opts_and_exts('lrfviewer', lrfviewerop, ['lrf'])) - f.write(opts_and_exts('ebook-viewer', viewer_op, input_formats)) - f.write(opts_and_words('fetch-ebook-metadata', fem_op, [])) - f.write(opts_and_words('calibre-smtp', smtp_op, [])) - f.write(opts_and_words('calibre-server', serv_op, [])) + o_and_e('calibre', guiop, BOOK_EXTENSIONS) + o_and_e('lrf2lrs', lrf2lrsop, ['lrf'], file_map={'--output':['lrs']}) + o_and_e('ebook-meta', metaop, + list(meta_filetypes()), cover_opts=['--cover', '-c'], + opf_opts=['--to-opf', '--from-opf']) + o_and_e('ebook-polish', polish_op, + [x.lower() for x in SUPPORTED], cover_opts=['--cover', '-c'], + opf_opts=['--opf', '-o']) + o_and_e('lrfviewer', lrfviewerop, ['lrf']) + o_and_e('ebook-viewer', viewer_op, input_formats) + o_and_w('fetch-ebook-metadata', fem_op, []) + o_and_w('calibre-smtp', smtp_op, []) + o_and_w('calibre-server', serv_op, []) f.write(textwrap.dedent(''' _ebook_device_ls() { @@ -335,6 +509,7 @@ class PostInstall: complete -o nospace -C %s ebook-convert ''')%complete) + zsh.write() except TypeError as err: if 'resolve_entities' in str(err): print 'You need python-lxml >= 2.0.5 for calibre' @@ -451,7 +626,7 @@ def options(option_parser): opts.extend(opt._long_opts) return opts -def opts_and_words(name, op, words): +def opts_and_words(name, op, words, takes_files=False): opts = '|'.join(options(op)) words = '|'.join([w.replace("'", "\\'") for w in words]) fname = name.replace('-', '_') @@ -481,12 +656,15 @@ def opts_and_words(name, op, words): } complete -F _'''%(opts, words) + fname + ' ' + name +"\n\n").encode('utf-8') +pics = {'jpg', 'jpeg', 'gif', 'png', 'bmp'} -def opts_and_exts(name, op, exts, cover_opts=('--cover',), opf_opts=()): +def opts_and_exts(name, op, exts, cover_opts=('--cover',), opf_opts=(), + file_map={}): opts = ' '.join(options(op)) exts.extend([i.upper() for i in exts]) exts='|'.join(exts) fname = name.replace('-', '_') + spics = '|'.join(tuple(pics) + tuple(x.upper() for x in pics)) special_exts_template = '''\ %s ) _filedir %s @@ -507,7 +685,7 @@ def opts_and_exts(name, op, exts, cover_opts=('--cover',), opf_opts=()): cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="%(opts)s" - pics="@(jpg|jpeg|png|gif|bmp|JPG|JPEG|PNG|GIF|BMP)" + pics="@(%(pics)s)" case "${prev}" in %(extras)s @@ -526,7 +704,7 @@ def opts_and_exts(name, op, exts, cover_opts=('--cover',), opf_opts=()): esac } -complete -o filenames -F _'''%dict( +complete -o filenames -F _'''%dict(pics=spics, opts=opts, extras=extras, exts=exts) + fname + ' ' + name +"\n\n"