mirror of
https://github.com/beetbox/beets.git
synced 2026-01-30 12:02:41 +01:00
Merge pull request #3509 from justinmayer/fish-completion
Add plugin for Fish shell tab completion
This commit is contained in:
commit
4dee523ba2
4 changed files with 334 additions and 1 deletions
276
beetsplug/fish.py
Normal file
276
beetsplug/fish.py
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 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`
|
||||
"""
|
||||
|
||||
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: <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:
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
52
docs/plugins/fish.rst
Normal file
52
docs/plugins/fish.rst
Normal file
|
|
@ -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 -<TAB>`` will show you all the
|
||||
option flags available to you, which also applies to subcommands such as
|
||||
``beet import -<TAB>``. 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<TAB>`` 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: <TAB>`` will display
|
||||
a list of all the genres in your library and ``beet list albumartist: <TAB>``
|
||||
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.
|
||||
|
|
@ -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/
|
||||
.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning/
|
||||
|
|
|
|||
Loading…
Reference in a new issue