beets/beetsplug/fish.py
Serene-Arc a6e5201ff3 Apply formatting tools to all files
This is 'the big one', which touches every file so that it all conforms
to the given standard.
2023-10-22 09:53:18 +10:00

341 lines
11 KiB
Python

# This file is part of beets.
# Copyright 2015, winters jean-marie.
# Copyright 2020, Justin Mayer <https://justinmayer.com>
#
# 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
<https://fishshell.com/>, 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: <TAB>' 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