diff --git a/beetsplug/fish.py b/beetsplug/fish.py new file mode 100644 index 000000000..b842ac70f --- /dev/null +++ b/beetsplug/fish.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2015, winters jean-marie. +# Copyright 2020, Justin Mayer +# +# 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. + +"""This plugin generates tab completions for Beets commands for the Fish shell +, including completions for Beets commands, plugin +commands, and option flags. Also generated are completions for all the album +and track fields, suggesting for example `genre:` or `album:` when querying the +Beets database. Completions for the *values* of those fields are not generated +by default but can be added via the `-e` / `--extravalues` flag. For example: +`beet fish -e genre -e albumartist` +""" + +from __future__ import division, absolute_import, print_function + +from beets.plugins import BeetsPlugin +from beets import library, ui +from beets.ui import commands +from operator import attrgetter +import os +BL_NEED2 = """complete -c beet -n '__fish_beet_needs_command' {} {}\n""" +BL_USE3 = """complete -c beet -n '__fish_beet_using_command {}' {} {}\n""" +BL_SUBS = """complete -c beet -n '__fish_at_level {} ""' {} {}\n""" +BL_EXTRA3 = """complete -c beet -n '__fish_beet_use_extra {}' {} {}\n""" + +HEAD = ''' +function __fish_beet_needs_command + set cmd (commandline -opc) + if test (count $cmd) -eq 1 + return 0 + end + return 1 +end + +function __fish_beet_using_command + set cmd (commandline -opc) + set needle (count $cmd) + if test $needle -gt 1 + if begin test $argv[1] = $cmd[2]; + and not contains -- $cmd[$needle] $FIELDS; end + return 0 + end + end + return 1 +end + +function __fish_beet_use_extra + set cmd (commandline -opc) + set needle (count $cmd) + if test $argv[2] = $cmd[$needle] + return 0 + end + return 1 +end +''' + + +class FishPlugin(BeetsPlugin): + + def commands(self): + cmd = ui.Subcommand('fish', help='generate Fish shell tab completions') + cmd.func = self.run + cmd.parser.add_option('-f', '--noFields', action='store_true', + default=False, + help='omit album/track field completions') + cmd.parser.add_option( + '-e', + '--extravalues', + action='append', + type='choice', + choices=library.Item.all_keys() + + library.Album.all_keys(), + help='include specified field *values* in completions') + return [cmd] + + def run(self, lib, opts, args): + # Gather the commands from Beets core and its plugins. + # Collect the album and track fields. + # If specified, also collect the values for these fields. + # Make a giant string of all the above, formatted in a way that + # allows Fish to do tab completion for the `beet` command. + home_dir = os.path.expanduser("~") + completion_dir = os.path.join(home_dir, '.config/fish/completions') + try: + os.makedirs(completion_dir) + except OSError: + if not os.path.isdir(completion_dir): + raise + completion_file_path = os.path.join(completion_dir, 'beet.fish') + nobasicfields = opts.noFields # Do not complete for album/track fields + extravalues = opts.extravalues # e.g., Also complete artists names + beetcmds = sorted( + (commands.default_commands + + commands.plugins.commands()), + key=attrgetter('name')) + fields = sorted(set( + library.Album.all_keys() + library.Item.all_keys())) + # Collect commands, their aliases, and their help text + cmd_names_help = [] + for cmd in beetcmds: + names = [alias for alias in cmd.aliases] + names.append(cmd.name) + for name in names: + cmd_names_help.append((name, cmd.help)) + # Concatenate the string + totstring = HEAD + "\n" + totstring += get_cmds_list([name[0] for name in cmd_names_help]) + totstring += '' if nobasicfields else get_standard_fields(fields) + totstring += get_extravalues(lib, extravalues) if extravalues else '' + totstring += "\n" + "# ====== {} =====".format( + "setup basic beet completion") + "\n" * 2 + totstring += get_basic_beet_options() + totstring += "\n" + "# ====== {} =====".format( + "setup field completion for subcommands") + "\n" + totstring += get_subcommands( + cmd_names_help, nobasicfields, extravalues) + # Set up completion for all the command options + totstring += get_all_commands(beetcmds) + + with open(completion_file_path, 'w') as fish_file: + fish_file.write(totstring) + + +def get_cmds_list(cmds_names): + # Make a list of all Beets core & plugin commands + substr = '' + substr += ( + "set CMDS " + " ".join(cmds_names) + ("\n" * 2) + ) + return substr + + +def get_standard_fields(fields): + # Make a list of album/track fields and append with ':' + fields = (field + ":" for field in fields) + substr = '' + substr += ( + "set FIELDS " + " ".join(fields) + ("\n" * 2) + ) + return substr + + +def get_extravalues(lib, extravalues): + # Make a list of all values from an album/track field. + # 'beet ls albumartist: ' yields completions for ABBA, Beatles, etc. + word = '' + values_set = get_set_of_values_for_field(lib, extravalues) + for fld in extravalues: + extraname = fld.upper() + 'S' + word += ( + "set " + extraname + " " + " ".join(sorted(values_set[fld])) + + ("\n" * 2) + ) + return word + + +def get_set_of_values_for_field(lib, fields): + # Get unique values from a specified album/track field + fields_dict = {} + for each in fields: + fields_dict[each] = set() + for item in lib.items(): + for field in fields: + fields_dict[field].add(wrap(item[field])) + return fields_dict + + +def get_basic_beet_options(): + word = ( + BL_NEED2.format("-l format-item", + "-f -d 'print with custom format'") + + BL_NEED2.format("-l format-album", + "-f -d 'print with custom format'") + + BL_NEED2.format("-s l -l library", + "-f -r -d 'library database file to use'") + + BL_NEED2.format("-s d -l directory", + "-f -r -d 'destination music directory'") + + BL_NEED2.format("-s v -l verbose", + "-f -d 'print debugging information'") + + + BL_NEED2.format("-s c -l config", + "-f -r -d 'path to configuration file'") + + BL_NEED2.format("-s h -l help", + "-f -d 'print this help message and exit'")) + return word + + +def get_subcommands(cmd_name_and_help, nobasicfields, extravalues): + # Formatting for Fish to complete our fields/values + word = "" + for cmdname, cmdhelp in cmd_name_and_help: + word += "\n" + "# ------ {} -------".format( + "fieldsetups for " + cmdname) + "\n" + word += ( + BL_NEED2.format( + ("-a " + cmdname), + ("-f " + "-d " + wrap(clean_whitespace(cmdhelp))))) + + if nobasicfields is False: + word += ( + BL_USE3.format( + cmdname, + ("-a " + wrap("$FIELDS")), + ("-f " + "-d " + wrap("fieldname")))) + + if extravalues: + for f in extravalues: + setvar = wrap("$" + f.upper() + "S") + word += " ".join(BL_EXTRA3.format( + (cmdname + " " + f + ":"), + ('-f ' + '-A ' + '-a ' + setvar), + ('-d ' + wrap(f))).split()) + "\n" + return word + + +def get_all_commands(beetcmds): + # Formatting for Fish to complete command options + word = "" + for cmd in beetcmds: + names = [alias for alias in cmd.aliases] + names.append(cmd.name) + for name in names: + word += "\n" + word += ("\n" * 2) + "# ====== {} =====".format( + "completions for " + name) + "\n" + + for option in cmd.parser._get_all_options()[1:]: + cmd_l = (" -l " + option._long_opts[0].replace('--', '') + )if option._long_opts else '' + cmd_s = (" -s " + option._short_opts[0].replace('-', '') + ) if option._short_opts else '' + cmd_need_arg = ' -r ' if option.nargs in [1] else '' + cmd_helpstr = (" -d " + wrap(' '.join(option.help.split())) + ) if option.help else '' + cmd_arglist = (' -a ' + wrap(" ".join(option.choices)) + ) if option.choices else '' + + word += " ".join(BL_USE3.format( + name, + (cmd_need_arg + cmd_s + cmd_l + " -f " + cmd_arglist), + cmd_helpstr).split()) + "\n" + + word = (word + " ".join(BL_USE3.format( + name, + ("-s " + "h " + "-l " + "help" + " -f "), + ('-d ' + wrap("print help") + "\n") + ).split())) + return word + + +def clean_whitespace(word): + # Remove excess whitespace and tabs in a string + return " ".join(word.split()) + + +def wrap(word): + # Need " or ' around strings but watch out if they're in the string + sptoken = '\"' + if ('"') in word and ("'") in word: + word.replace('"', sptoken) + return '"' + word + '"' + + tok = '"' if "'" in word else "'" + return tok + word + tok diff --git a/docs/changelog.rst b/docs/changelog.rst index 7ef19871b..e33cf8c12 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog New features: +* A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets * :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality`` option that controls the quality of the image output when the image is resized. @@ -217,6 +218,7 @@ For packagers: the test may no longer be necessary. * This version drops support for Python 3.4. +.. _Fish shell: https://fishshell.com/ .. _MediaFile: https://github.com/beetbox/mediafile .. _Confuse: https://github.com/beetbox/confuse .. _works: https://musicbrainz.org/doc/Work diff --git a/docs/plugins/fish.rst b/docs/plugins/fish.rst new file mode 100644 index 000000000..b2cb096ee --- /dev/null +++ b/docs/plugins/fish.rst @@ -0,0 +1,52 @@ +Fish Plugin +=========== + +The ``fish`` plugin adds a ``beet fish`` command that creates a `Fish shell`_ +tab-completion file named ``beet.fish`` in ``~/.config/fish/completions``. +This enables tab-completion of ``beet`` commands for the `Fish shell`_. + +.. _Fish shell: https://fishshell.com/ + +Configuration +------------- + +Enable the ``fish`` plugin (see :ref:`using-plugins`) on a system running the +`Fish shell`_. + +Usage +----- + +Type ``beet fish`` to generate the ``beet.fish`` completions file at: +``~/.config/fish/completions/``. If you later install or disable plugins, run +``beet fish`` again to update the completions based on the enabled plugins. + +For users not accustomed to tab completion… After you type ``beet`` followed by +a space in your shell prompt and then the ``TAB`` key, you should see a list of +the beets commands (and their abbreviated versions) that can be invoked in your +current environment. Similarly, typing ``beet -`` will show you all the +option flags available to you, which also applies to subcommands such as +``beet import -``. If you type ``beet ls`` followed by a space and then the +and the ``TAB`` key, you will see a list of all the album/track fields that can +be used in beets queries. For example, typing ``beet ls ge`` will complete +to ``genre:`` and leave you ready to type the rest of your query. + +Options +------- + +In addition to beets commands, plugin commands, and option flags, the generated +completions also include by default all the album/track fields. If you only want +the former and do not want the album/track fields included in the generated +completions, use ``beet fish -f`` to only generate completions for beets/plugin +commands and option flags. + +If you want generated completions to also contain album/track field *values* for +the items in your library, you can use the ``-e`` or ``--extravalues`` option. +For example: ``beet fish -e genre`` or ``beet fish -e genre -e albumartist`` +In the latter case, subsequently typing ``beet list genre: `` will display +a list of all the genres in your library and ``beet list albumartist: `` +will show a list of the album artists in your library. Keep in mind that all of +these values will be put into the generated completions file, so use this option +with care when specified fields contain a large number of values. Libraries with, +for example, very large numbers of genres/artists may result in higher memory +utilization, completion latency, et cetera. This option is not meant to replace +database queries altogether. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 383466a68..6c643ce61 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -78,6 +78,7 @@ following to your configuration:: export fetchart filefilter + fish freedesktop fromfilename ftintitle @@ -184,6 +185,7 @@ Interoperability * :doc:`badfiles`: Check audio file integrity. * :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes. +* :doc:`fish`: Adds `Fish shell`_ tab autocompletion to ``beet`` commands. * :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. * :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs. * :doc:`kodiupdate`: Automatically notifies `Kodi`_ whenever the beets library @@ -203,6 +205,7 @@ Interoperability .. _Emby: https://emby.media +.. _Fish shell: https://fishshell.com/ .. _Plex: https://plex.tv .. _Kodi: https://kodi.tv .. _Sonos: https://sonos.com @@ -326,4 +329,4 @@ Here are a few of the plugins written by the beets community: .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic .. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser -.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/ \ No newline at end of file +.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/