summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip Sequeira <phsequei@gmail.com>2017-11-22 19:09:06 -0500
committerwm4 <1387750+wm4@users.noreply.github.com>2019-09-27 13:19:29 +0200
commit2712db8238846f7b4e24052dbe091ea87defc5ae (patch)
tree02df8e961c08931dafb7e6a9d561497ade9cd9a5
parent21a5c416d5de6ef43a5e2c08ba53cb2a8cb59ae2 (diff)
downloadmpv-2712db8238846f7b4e24052dbe091ea87defc5ae.tar.bz2
mpv-2712db8238846f7b4e24052dbe091ea87defc5ae.tar.xz
zsh completion: move generation to runtime and improve
The completion function itself now parses --list-options on the first tab press and caches the results. This does mean a slight delay on that first tab press, but it will only do this if the argument being completed looks like an option (i.e. starts with "-"), so there is never a delay when just completing a file name. I've also put some effort into making it reasonably fast; on my machine it's consistently under 100 ms, more than half of which is mpv itself. Installation of zsh completion is now done unconditionally because it's nothing more than copying a file. If you really don't want it installed, set zshdir to empty: `./waf configure --zshdir= ...` Improvements in functionality compared to the old script: * Produces the right results for mpv binaries other than the one it was installed with (like a dev build for testing changes). * Does not require running mpv at build time, so it won't cause problems with cross compilation. * Handles aliases. * Slightly nicer handling of options that take comma-separated values and/or sub-options: A space is now inserted at the end instead of a comma, allowing you to immediately start typing the next argument, but typing a comma will still remove the automatically added space, and = and : will now do that too, so you can immediately add a sub-option. * More general/flexible handling of values for options that print their possible values with --option=help. The code as is could handle quite a few more options (*scale, demuxers, decoders, ...), but nobody wants to maintain that list here so we'll just stick with what the old completion script already did.
-rwxr-xr-xTOOLS/zsh.pl283
-rw-r--r--etc/_mpv.zsh251
-rw-r--r--waftools/generators/sources.py13
-rw-r--r--wscript6
-rw-r--r--wscript_build.py7
5 files changed, 253 insertions, 307 deletions
diff --git a/TOOLS/zsh.pl b/TOOLS/zsh.pl
deleted file mode 100755
index 482aa5e125..0000000000
--- a/TOOLS/zsh.pl
+++ /dev/null
@@ -1,283 +0,0 @@
-#!/usr/bin/perl
-
-# Generate ZSH completion
-
-#
-# This file is part of mpv.
-#
-# mpv is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# mpv is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
-#
-
-use strict;
-use warnings;
-use warnings FATAL => 'uninitialized';
-
-my $mpv = $ARGV[0] || 'mpv';
-
-my @opts = parse_main_opts('--list-options', '^ (\-\-[^\s\*]*)\*?\s*(.*)');
-
-die "Couldn't find any options" unless (@opts);
-
-my @ao = parse_opts('--ao=help', '^ ([^\s\:]*)\s*(.*)');
-my @vo = parse_opts('--vo=help', '^ ([^\s\:]*)\s*(.*)');
-
-my @af = parse_opts('--af=help', '^ ([^\s\:]*)\s*(.*)');
-my @vf = parse_opts('--vf=help', '^ ([^\s\:]*)\s*(.*)');
-
-my @protos = parse_opts('--list-protocols', '^ ([^\s]*)');
-
-my ($opts_str, $ao_str, $vo_str, $af_str, $vf_str, $protos_str);
-
-$opts_str .= qq{ '$_' \\\n} foreach (@opts);
-chomp $opts_str;
-
-$ao_str .= qq{ '$_' \\\n} foreach (@ao);
-chomp $ao_str;
-
-$vo_str .= qq{ '$_' \\\n} foreach (@vo);
-chomp $vo_str;
-
-$af_str .= qq{ '$_' \\\n} foreach (@af);
-chomp $af_str;
-
-$vf_str .= qq{ '$_' \\\n} foreach (@vf);
-chomp $vf_str;
-
-$protos_str = join(' ', @protos);
-
-my $runtime_completions = <<'EOS';
- profile|show-profile)
- local -a profiles
- local current
- for current in "${(@f)$($words[1] --profile=help)}"; do
- current=${current//\*/\\\*}
- current=${current//\:/\\\:}
- current=${current//\[/\\\[}
- current=${current//\]/\\\]}
- if [[ $current =~ $'\t'([^$'\t']*)$'\t'(.*) ]]; then
- if [[ -n $match[2] ]]; then
- current="$match[1][$match[2]]"
- else
- current="$match[1]"
- fi
- profiles=($profiles $current)
- fi
- done
- if [[ $state == show-profile ]]; then
- # For --show-profile, only one allowed
- if (( ${#profiles} > 0 )); then
- _values 'profile' $profiles && rc=0
- fi
- else
- # For --profile, multiple allowed
- profiles=($profiles 'help[list profiles]')
- _values -s , 'profile(s)' $profiles && rc=0
- fi
- ;;
-
- audio-device)
- local -a audio_devices
- local current
- for current in "${(@f)$($words[1] --audio-device=help)}"; do
- current=${current//\*/\\\*}
- current=${current//\:/\\\:}
- current=${current//\[/\\\[}
- current=${current//\]/\\\]}
- if [[ $current =~ ' '\'([^\']*)\'' \('(.*)'\)' ]]; then
- audio_devices=($audio_devices "$match[1][$match[2]]")
- fi
- done
- audio_devices=($audio_devices 'help[list audio devices]')
- _values 'audio device' $audio_devices && rc=0
- ;;
-EOS
-chomp $runtime_completions;
-
-my $tmpl = <<"EOS";
-#compdef mpv
-
-# For customization, see:
-# https://github.com/mpv-player/mpv/wiki/Zsh-completion-customization
-
-local curcontext="\$curcontext" state state_descr line
-typeset -A opt_args
-
-local -a match mbegin mend
-local MATCH MBEGIN MEND
-
-# By default, don't complete URLs unless no files match
-local -a tag_order
-zstyle -a ":completion:*:*:\$service:*" tag-order tag_order || \
- zstyle ":completion:*:*:\$service:*" tag-order '!urls'
-
-local rc=1
-
-_arguments -C -S \\
-$opts_str
- '*:files:->mfiles' && rc=0
-
-case \$state in
- ao)
- _values -s , 'audio outputs' \\
-$ao_str
- && rc=0
- ;;
-
- vo)
- _values -s , 'video outputs' \\
-$vo_str
- && rc=0
- ;;
-
- af)
- _values -s , 'audio filters' \\
-$af_str
- && rc=0
- ;;
-
- vf)
- _values -s , 'video filters' \\
-$vf_str
- && rc=0
- ;;
-
-$runtime_completions
-
- files)
- compset -P '*,'
- compset -S ',*'
- _files -r ',/ \\t\\n\\-' && rc=0
- ;;
-
- mfiles)
- local expl
- _tags files urls
- while _tags; do
- _requested files expl 'media file' _files && rc=0
- if _requested urls; then
- while _next_label urls expl URL; do
- _urls "\$expl[@]" && rc=0
- compadd -S '' "\$expl[@]" $protos_str && rc=0
- done
- fi
- (( rc )) || return 0
- done
- ;;
-esac
-
-return rc
-EOS
-
-print $tmpl;
-
-sub parse_main_opts {
- my ($cmd, $regex) = @_;
-
- my @list;
- my @lines = call_mpv($cmd);
-
- foreach my $line (@lines) {
- my ($name, $desc) = ($line =~ /^$regex/) or next;
-
- next if ($desc eq 'removed' || $desc eq 'alias');
-
- if ($desc =~ /^Flag/) {
-
- push @list, $name;
-
- $name =~ /^--(.*)/;
- if ($1 !~ /^(\{|\}|v|list-options|really-quiet|no-.*)$/) {
- push @list, "--no-$1";
- }
-
- } elsif ($desc =~ /^Print/) {
-
- push @list, $name;
-
- } else {
-
- # Option takes argument
-
- my $entry = $name;
-
- $desc =~ s/\:/\\:/g;
- $entry .= "=-:$desc:";
-
- if ($desc =~ /^Choices\\: ([^(]*)/) {
- my $choices = $1;
- $choices =~ s/ +$//; # strip trailing space
- $entry .= "($choices)";
-
- # If "no" is one of the choices, it can also be
- # negated like a flag (--no-whatever).
- if ($choices =~ /\bno\b/) {
- $name =~ s/^--/--no-/;
- push @list, $name;
- }
- } elsif ($line =~ /\[file\]/) {
- $entry .= '->files';
- } elsif ($name =~ /^--(ao|vo|af|vf|profile|show-profile|audio-device)$/) {
- $entry .= "->$1";
- }
- push @list, $entry;
- }
- }
-
- # Sort longest first, because zsh won't complete an option listed
- # after one that's a prefix of it.
- @list = sort {
- $a =~ /([^=]*)/; my $ma = $1;
- $b =~ /([^=]*)/; my $mb = $1;
-
- length($mb) <=> length($ma)
- } @list;
-
- return @list;
-}
-
-sub parse_opts {
- my ($cmd, $regex) = @_;
-
- my @list;
- my @lines = call_mpv($cmd);
-
- foreach my $line (@lines) {
- if ($line !~ /^$regex/) {
- next;
- }
-
- my $entry = $1;
-
- if (defined $2) {
- my $desc = $2;
- $desc =~ s/\:/\\:/g;
- $entry .= "[$desc]";
- }
-
- push @list, $entry
- }
-
- return @list;
-}
-
-sub call_mpv {
- my ($cmd) = @_;
- my $output = `"$mpv" --no-config $cmd`;
- if ($? == -1) {
- die "Could not run mpv: $!";
- } elsif ((my $exit_code = $? >> 8) != 0) {
- die "mpv returned $exit_code with output:\n$output";
- }
- return split /\n/, $output;
-}
diff --git a/etc/_mpv.zsh b/etc/_mpv.zsh
new file mode 100644
index 0000000000..265c20acbf
--- /dev/null
+++ b/etc/_mpv.zsh
@@ -0,0 +1,251 @@
+#compdef mpv
+
+# ZSH completion for mpv
+#
+# For customization, see:
+# https://github.com/mpv-player/mpv/wiki/Zsh-completion-customization
+
+#
+# This file is part of mpv.
+#
+# mpv is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# mpv is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+#
+
+local curcontext="$curcontext" state state_descr line
+typeset -A opt_args
+
+local -a match mbegin mend
+local MATCH MBEGIN MEND
+
+# By default, don't complete URLs unless no files match
+local -a tag_order
+zstyle -a ":completion:*:*:$service:*" tag-order tag_order ||
+ zstyle ":completion:*:*:$service:*" tag-order '!urls'
+
+# Use PCRE for regular expression matching if possible. This approximately
+# halves the execution time of generate_arguments compared to the default POSIX
+# regex, which translates to a more responsive first tab press. However, we
+# can't rely on PCRE being available, so we keep all our patterns
+# POSIX-compatible.
+setopt re_match_pcre &>/dev/null
+
+typeset -ga _mpv_completion_arguments _mpv_completion_protocols
+
+function generate_arguments {
+
+ _mpv_completion_arguments=()
+
+ local -a option_aliases=()
+
+ local list_options_line
+ for list_options_line in "${(@f)$($words[1] --list-options)}"; do
+
+ [[ $list_options_line =~ '^\s+--(\S+)\s*(.*)' ]] || continue
+
+ local name=$match[1] desc=$match[2]
+
+ if [[ $desc == Flag* ]]; then
+
+ _mpv_completion_arguments+="$name"
+ if [[ $name != (\{|\}|v|list-options) ]]; then
+ # Negated version
+ _mpv_completion_arguments+="no-$name"
+ fi
+
+ elif [[ -z $desc ]]; then
+
+ # Sub-option for list option
+
+ if [[ $name == *-(clr|help) ]]; then
+ # Like a flag
+ _mpv_completion_arguments+="$name"
+ else
+ # Find the parent option and use that with this option's name
+ _mpv_completion_arguments+="${_mpv_completion_arguments[(R)${name%-*}=*]/*=/$name=}"
+ fi
+
+ elif [[ $desc == Print* ]]; then
+
+ _mpv_completion_arguments+="$name"
+
+ elif [[ $desc =~ '^alias for --(\S+)' ]]; then
+
+ # Save this for later; we might not have parsed the target option yet
+ option_aliases+="$name $match[1]"
+
+ else
+
+ # Option takes argument
+
+ local entry="$name=-:${desc//:/\\:}:"
+
+ if [[ $desc =~ '^Choices: ([^(]*)' ]]; then
+
+ local -a choices=(${(s: :)match[1]})
+ entry+="($choices)"
+ # If "no" is one of the choices, it can also be negated like a flag
+ # (--no-whatever is equivalent to --whatever=no).
+ if (( ${+choices[(r)no]} )); then
+ _mpv_completion_arguments+="no-$name"
+ fi
+
+ elif [[ $desc == *'[file]'* ]]; then
+
+ entry+='->files'
+
+ elif [[ $name == (ao|vo|af|vf|profile|audio-device|vulkan-device) ]]; then
+
+ entry+="->parse-help-$name"
+
+ elif [[ $name == show-profile ]]; then
+
+ entry+="->parse-help-profile"
+
+ fi
+
+ _mpv_completion_arguments+="$entry"
+
+ fi
+
+ done
+
+ # Process aliases
+ local to_from real_name arg_spec
+ for to_from in $option_aliases; do
+ # to_from='alias-name real-name'
+ real_name=${to_from##* }
+ for arg_spec in "$real_name" "$real_name=*" "no-$real_name"; do
+ arg_spec=${_mpv_completion_arguments[(r)$arg_spec]}
+ [[ -n $arg_spec ]] &&
+ _mpv_completion_arguments+="${arg_spec/$real_name/${to_from%% *}}"
+ done
+ done
+
+ # Older versions of zsh have a bug where they won't complete an option listed
+ # after one that's a prefix of it. To work around this, we can sort the
+ # options by length, longest first, so that any prefix of an option will be
+ # listed after it. On newer versions of zsh where the bug is fixed, we skip
+ # this to avoid slowing down the first tab press any more than we have to.
+ autoload -Uz is-at-least
+ if ! is-at-least 5.2; then
+ # If this were a real language, we wouldn't have to sort by prepending the
+ # length, sorting the whole thing numerically, and then removing it again.
+ local -a sort_tmp=()
+ for arg_spec in $_mpv_completion_arguments; do
+ sort_tmp+=${#arg_spec%%=*}_$arg_spec
+ done
+ _mpv_completion_arguments=(${${(On)sort_tmp}/#*_})
+ fi
+
+}
+
+function generate_protocols {
+ _mpv_completion_protocols=()
+ local list_protos_line
+ for list_protos_line in "${(@f)$($words[1] --list-protocols)}"; do
+ if [[ $list_protos_line =~ '^\s+(.*)' ]]; then
+ _mpv_completion_protocols+="$match[1]"
+ fi
+ done
+}
+
+function generate_if_changed {
+ # Called with $1 = 'arguments' or 'protocols'. Generates the respective list
+ # on the first run and re-generates it if the executable being completed for
+ # is different than the one we used to generate the cached list.
+ typeset -gA _mpv_completion_binary
+ local current_binary=${words[1]:c}
+ zmodload -F zsh/stat b:zstat
+ current_binary+=T$(zstat +mtime $current_binary)
+ if [[ $_mpv_completion_binary[$1] != $current_binary ]]; then
+ generate_$1
+ _mpv_completion_binary[$1]=$current_binary
+ fi
+}
+
+# Only consider generating arguments if the argument being completed looks like
+# an option. This way, the user should never see a delay when just completing a
+# filename.
+if [[ $words[$CURRENT] == -* ]]; then
+ generate_if_changed arguments
+fi
+
+local rc=1
+
+_arguments -C -S \*--$_mpv_completion_arguments '*:files:->mfiles' && rc=0
+
+case $state in
+
+ parse-help-*)
+ local option_name=${state#parse-help-}
+ # Can't do non-capturing groups without pcre, so we index the ones we want
+ local pattern name_group=1 desc_group=2
+ case $option_name in
+ audio-device|vulkan-device)
+ pattern='^\s+'\''([^'\'']*)'\''\s+\((.*)\)'
+ ;;
+ profile)
+ # The generic pattern would actually work in most cases for --profile,
+ # but would break if a profile name contained spaces. This stricter one
+ # only breaks if a profile name contains tabs.
+ pattern=$'^\t([^\t]*)\t(.*)'
+ ;;
+ *)
+ pattern='^\s+(--'${option_name}'=)?(\S+)\s*[-:]?\s*(.*)'
+ name_group=2 desc_group=3
+ ;;
+ esac
+ local -a values
+ local current
+ for current in "${(@f)$($words[1] --${option_name}=help)}"; do
+ [[ $current =~ $pattern ]] || continue;
+ local name=${match[name_group]//:/\\:} desc=${match[desc_group]}
+ if [[ -n $desc ]]; then
+ values+="${name}:${desc}"
+ else
+ values+="${name}"
+ fi
+ done
+ (( $#values )) && {
+ compset -P '*,'
+ compset -S ',*'
+ _describe "$state_descr" values -r ',=: \t\n\-' && rc=0
+ }
+ ;;
+
+ files)
+ compset -P '*,'
+ compset -S ',*'
+ _files -r ',/ \t\n\-' && rc=0
+ ;;
+
+ mfiles)
+ local expl
+ _tags files urls
+ while _tags; do
+ _requested files expl 'media file' _files && rc=0
+ if _requested urls; then
+ while _next_label urls expl URL; do
+ _urls "$expl[@]" && rc=0
+ generate_if_changed protocols
+ compadd -S '' "$expl[@]" $_mpv_completion_protocols && rc=0
+ done
+ fi
+ (( rc )) || return 0
+ done
+ ;;
+
+esac
+
+return rc
diff --git a/waftools/generators/sources.py b/waftools/generators/sources.py
index bbca38c06d..0f5b993758 100644
--- a/waftools/generators/sources.py
+++ b/waftools/generators/sources.py
@@ -5,10 +5,6 @@ from TOOLS.matroska import generate_C_header, generate_C_definitions
from TOOLS.file2string import file2string
import os
-def __zshcomp_cmd__(ctx, argument):
- return '"${{BIN_PERL}}" "{0}/TOOLS/zsh.pl" "{1}" > "${{TGT}}"' \
- .format(ctx.srcnode.abspath(), argument)
-
def __wayland_scanner_cmd__(ctx, mode, dir, src, vendored_file):
return "${{WAYSCAN}} {0} < {1} > ${{TGT}}".format(
mode,
@@ -49,14 +45,6 @@ def ebml_header(self):
def ebml_definitions(self):
execf(self, generate_C_definitions)
-def __zshcomp__(ctx, **kwargs):
- ctx(
- rule = __zshcomp_cmd__(ctx, ctx.bldnode.abspath() + '/mpv'),
- after = ("c", "cprogram",),
- name = os.path.basename(kwargs['target']),
- **kwargs
- )
-
def __wayland_protocol_code__(ctx, **kwargs):
protocol_is_vendored = kwargs.get("vendored_protocol", False)
file_name = kwargs['protocol'] + '.xml'
@@ -105,4 +93,3 @@ def handle_add_object(tgen):
BuildContext.file2string = __file2string__
BuildContext.wayland_protocol_code = __wayland_protocol_code__
BuildContext.wayland_protocol_header = __wayland_protocol_header__
-BuildContext.zshcomp = __zshcomp__
diff --git a/wscript b/wscript
index 93144f9edd..752b400dc3 100644
--- a/wscript
+++ b/wscript
@@ -104,12 +104,6 @@ build_options = [
'deps': 'libdl && !os-win32',
'func': check_cc(linkflags=['-rdynamic']),
}, {
- 'name': '--zsh-comp',
- 'desc': 'zsh completion',
- 'func': check_ctx_vars('BIN_PERL'),
- 'func': check_true,
- 'default': 'disable',
- }, {
# does nothing - left for backward and forward compatibility
'name': '--asm',
'desc': 'inline assembly (currently without effect)',
diff --git a/wscript_build.py b/wscript_build.py
index 50f8e641ea..301e8819e4 100644
--- a/wscript_build.py
+++ b/wscript_build.py
@@ -717,11 +717,8 @@ def build(ctx):
if ctx.dependency_satisfied('cplayer'):
- if ctx.dependency_satisfied('zsh-comp'):
- ctx.zshcomp(target = "etc/_mpv", source = "TOOLS/zsh.pl")
- ctx.install_files(
- ctx.env.ZSHDIR,
- ['etc/_mpv'])
+ if ctx.env.ZSHDIR:
+ ctx.install_as(ctx.env.ZSHDIR + '/_mpv', 'etc/_mpv.zsh')
ctx.install_files(
ctx.env.DATADIR + '/applications',