# 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` """ import os from operator import attrgetter from beets import library, ui from beets.plugins import BeetsPlugin from beets.ui import commands 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", ) cmd.parser.add_option( "-o", "--output", default="~/.config/fish/completions/beet.fish", help="where to save the script. default: " "~/.config/fish/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. completion_file_path = os.path.expanduser(opts.output) completion_dir = os.path.dirname(completion_file_path) if completion_dir != "": os.makedirs(completion_dir, exist_ok=True) 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 = list(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 _escape(name): # Escape ? in fish if name == "?": name = "\\" + name return name 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: cmdname = _escape(cmdname) 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 = list(cmd.aliases) names.append(cmd.name) for name in names: name = _escape(name) 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