1#compdef beet
2
3# zsh completion for beets music library manager and MusicBrainz tagger: http://beets.radbox.org/
4
5# Default values for BEETS_LIBRARY & BEETS_CONFIG needed for the cache checking function.
6# They will be updated under the assumption that the config file is in the same directory as the library.
7local BEETS_LIBRARY=~/.config/beets/library.db
8local BEETS_CONFIG=~/.config/beets/config.yaml
9# Use separate caches for file locations, command completions, and query completions.
10# This allows the use of different rules for when to update each one.
11zstyle ":completion:${curcontext}:" cache-policy _beet_check_cache
12_beet_check_cache () {
13    local cachefile="$(basename ${1})"
14    if [[ ! -a "${1}" ]] || [[ "${1}" -ot =beet ]]; then
15	# always update the cache if it doesnt exist, or if the beet executable changes
16	return 0
17    fi
18    case cachefile; in
19	(beetslibrary)
20	    if [[ ! -a "${~BEETS_LIBRARY}" ]] || [[ "${1}" -ot "${~BEETS_CONFIG}" ]]; then
21		return 0
22	    fi
23	    ;;
24	(beetscmds)
25	    _retrieve_cache beetslibrary
26	    if [[ "${1}" -ot "${~BEETS_CONFIG}" ]]; then
27		return 0
28	    fi
29	    ;;
30    esac
31    return 1
32}
33
34# useful: argument to _regex_arguments for matching any word
35local matchany=/$'[^\0]##\0'/
36# arguments to _regex_arguments for completing files and directories
37local -a files dirs
38files=("$matchany" ':file:file:_files')
39dirs=("$matchany" ':dir:directory:_dirs')
40
41# Retrieve or update caches
42if ! _retrieve_cache beetslibrary || _cache_invalid beetslibrary; then
43    local BEETS_LIBRARY="${$(beet config|grep library|cut -f 2 -d ' '):-${BEETS_LIBRARY}}"
44    local BEETS_CONFIG="${$(beet config -p):-${BEETS_CONFIG}}"
45    _store_cache beetslibrary BEETS_LIBRARY BEETS_CONFIG
46fi
47
48if ! _retrieve_cache beetscmds || _cache_invalid beetscmds; then
49    local -a subcommands fields beets_regex_words_subcmds beets_regex_words_help query modify
50    local subcmd cmddesc matchquery matchmodify field fieldargs queryelem modifyelem
51    # Useful function for joining grouped lines of output into single lines (taken from _completion_helpers)
52    _join_lines() {
53	awk -v SEP="$1" -v ARG2="$2" -v START="$3" -v END2="$4" 'BEGIN {if(START==""){f=1}{f=0};
54         if(ARG2 ~ "^[0-9]+"){LINE1 = "^[[:space:]]{,"ARG2"}[^[:space:]]"}else{LINE1 = ARG2}}
55         ($0 ~ END2 && f>0 && END2!="") {exit}
56         ($0 ~ START && f<1) {f=1; if(length(START)!=0){next}}
57         ($0 ~ LINE1 && f>0) {if(f<2){f=2; printf("%s",$0)}else{printf("\n%s",$0)}; next}
58         (f>1) {gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0); printf("%s%s",SEP, $0); next}
59         END {print ""}'
60    }
61    # Variables used for completing subcommands and queries
62    subcommands=(${${(f)"$(beet help | _join_lines ' ' 3 'Commands:')"}[@]})
63    fields=($(beet fields | grep -G '^  ' | sort -u | colrm 1 2))
64    for field in "${fields[@]}"
65    do
66	fieldargs="$fieldargs '$field:::{_beet_field_values $field}'"
67    done
68    queryelem="_values -S : 'query field (add an extra : to match by regexp)' '::' $fieldargs"
69    modifyelem="_values -S = 'modify field (replace = with ! to remove field)' $(echo "'${^fields[@]}:: '")"
70    # regexps for matching query and modify terms on the command line
71    matchquery=/"(${(j/|/)fields[@]})"$':[^\0]##\0'/
72    matchmodify=/"(${(j/|/)fields[@]})"$'(=[^\0]##|!)\0'/
73    # create completion function for queries
74    _regex_arguments _beet_query "$matchany" \# \( "$matchquery" ":query:query string:$queryelem" \) \#
75    local "beets_query"="$(which _beet_query)"
76    # arguments for _regex_arguments for completing lists of queries and modifications
77    beets_query_args=( \( "$matchquery" ":query:query string:{_beet_query}" \) \# )
78    beets_modify_args=( \( "$matchmodify" ":modify:modify string:$modifyelem" \) \# )
79    # now build arguments for _beet and _beet_help completion functions
80    beets_regex_words_subcmds=('(')
81    for i in ${subcommands[@]}; do
82	subcmd="${i[(w)1]}"
83	# remove first word and parenthesised alias, replace : with -, [ with (, ] with ), and remove single quotes
84	cmddesc="${${${${${i[(w)2,-1]##\(*\) #}//:/-}//\[/(}//\]/)}//\'/}"
85	# update arguments needed for creating _beet
86	beets_regex_words_subcmds+=(/"${subcmd}"$'\0'/ ":subcmds:subcommands:((${subcmd}:${cmddesc// /\ }))")
87	beets_regex_words_subcmds+=(\( "${matchany}" ":option:option:{_beet_subcmd ${subcmd}}" \) \# \|)
88	# update arguments needed for creating _beet_help
89	beets_regex_words_help+=("${subcmd}:${cmddesc}")
90    done
91    beets_regex_words_subcmds[-1]=')'
92    _store_cache beetscmds beets_regex_words_subcmds beets_regex_words_help beets_query_args beets_modify_args beets_query
93else
94    # Evaluate the variable containing the query completer function
95    eval "${beets_query}"
96fi
97
98# Function for getting unique values for field from database (you may need to change the path to the database).
99_beet_field_values() {
100    local -a output fieldvals
101    local sqlcmd="select distinct $1 from items;"
102    _retrieve_cache beetslibrary
103    case ${1}
104    in
105        lyrics)
106            fieldvals=
107            ;;
108        *)
109	    if [[ "$(sqlite3 ${~BEETS_LIBRARY} ${sqlcmd} 2>&1)" =~ "no such column" ]]; then
110		sqlcmd="select distinct value from item_attributes where key=='$1' and value!='';"
111	    fi
112	    output="$(sqlite3 ${~BEETS_LIBRARY} ${sqlcmd} 2>/dev/null | sed -rn '/^-+$/,${{/^[- ]+$/n};p}')"
113            fieldvals=("${(f)output[@]}")
114            ;;
115    esac
116    compadd -P \" -S \" -M 'm:{[:lower:][:upper:]}={[:upper:][:lower:]}' -Q -a fieldvals
117}
118
119# This function takes a beet subcommand as its first argument, and then uses _regex_words to set ${reply[@]}
120# to an array containing arguments for the _regex_arguments function.
121_beet_subcmd_options() {
122    local shortopt optarg optdesc
123    local matchany=/$'[^\0]##\0'/
124    local -a regex_words
125    regex_words=()
126    for i in ${${(f)"$(beet help $1 | awk '/^ +-/{if(x)print x;x=$0;next}/^ *$/{if(x) exit}{if(x) x=x$0}END{print x}')"}[@]}
127    do
128        opt="${i[(w)1]/,/}"
129        optarg="${${${i## #[-a-zA-Z]# }##[- ]##*}%%[, ]*}"
130        optdesc="${${${${${i[(w)2,-1]/[A-Z, ]#--[-a-z]##[=A-Z]# #/}//:/-}//\[/(}//\]/)}//\'/}"
131        case $optarg; in
132            ("")
133                if [[ "$1" == "import" && "$opt" == "-L" ]]; then
134                    regex_words+=("$opt:$optdesc:\${beets_query_args[@]}")
135                else
136                    regex_words+=("$opt:$optdesc")
137                fi
138                ;;
139            (LOG)
140		local -a files
141		files=("$matchany" ':file:file:_files')
142		regex_words+=("$opt:$optdesc:\$files")
143                ;;
144            (CONFIG)
145                local -a configfile
146                configfile=("$matchany" ':file:config file:{_files -g *.yaml}')
147                regex_words+=("$opt:$optdesc:\$configfile")
148                ;;
149            (LIB|LIBRARY)
150                local -a libfile
151                libfile=("$matchany" ':file:database file:{_files -g *.db}')
152                regex_words+=("$opt:$optdesc:\$libfile")
153                ;;
154            (DIR|DIRECTORY)
155		local -a dirs
156		dirs=("$matchany" ':dir:directory:_dirs')
157                regex_words+=("$opt:$optdesc:\$dirs")
158                ;;
159            (SOURCE)
160                if [[ "${1}" -eq lastgenre ]]; then
161                    local -a lastgenresource
162                    lastgenresource=(/$'(artist|album|track)\0'/ ':source:genre source:(artist album track)')
163                    regex_words+=("$opt:$optdesc:\$lastgenresource")
164                else
165                    regex_words+=("$opt:$optdesc:\$matchany")
166                fi
167                ;;
168            (*)
169                regex_words+=("$opt:$optdesc:\$matchany")
170                ;;
171        esac
172    done
173    _regex_words options "$1 options" "${regex_words[@]}"
174}
175
176## Function for completing subcommands. It calls another completion function which is first created if it doesn't already exist.
177_beet_subcmd() {
178    local -a options
179    local subcmd="${1}"
180    if [[ ! $(type _beet_${subcmd} | grep function) =~ function ]]; then
181	if ! _retrieve_cache "beets${subcmd}" || _cache_invalid "beets${subcmd}"; then
182	    local matchany=/$'[^\0]##\0'/
183	    local -a files
184	    files=("$matchany" ':file:file:_files')
185	    # get arguments for completing subcommand options
186	    _beet_subcmd_options "$subcmd"
187	    options=("${reply[@]}" \#)
188	    _retrieve_cache beetscmds
189	    case ${subcmd}; in
190		(import)
191		    _regex_arguments _beet_import "${matchany}" /"${subcmd}"$'\0'/ "${options[@]}" "${files[@]}" \#
192		    ;;
193		(modify)
194		    _regex_arguments _beet_modify "${matchany}" /"${subcmd}"$'\0'/ "${options[@]}" \
195				     "${beets_query_args[@]}" "${beets_modify_args[@]}"
196		    ;;
197		(fields|migrate|version|config)
198		    _regex_arguments _beet_${subcmd} "${matchany}" /"${subcmd}"$'\0'/ "${options[@]}"
199		    ;;
200		(help)
201		    _regex_words subcmds "subcommands" "${beets_regex_words_help[@]}"
202		    _regex_arguments _beet_help "${matchany}" /$'help\0'/ "${options[@]}" "${reply[@]}"
203		    ;;
204		(*) # Other commands have options followed by a query
205		    _regex_arguments _beet_${subcmd} "${matchany}" /"${subcmd}"$'\0'/ "${options[@]}" "${beets_query_args[@]}"
206		    ;;
207	    esac
208	    # Store completion function in a cache file
209	    local "beets_${subcmd}"="$(which _beet_${subcmd})"
210	    _store_cache "beets${subcmd}" "beets_${subcmd}"
211	else
212	    # Evaluate the function which is stored in $beets_${subcmd}
213	    local var="beets_${subcmd}"
214	    eval "${(P)var}"
215	fi
216    fi
217    _beet_${subcmd}
218}
219
220# Global options
221local -a globalopts
222_regex_words options "global options" '-c:path to configuration file:$files' '-v:print debugging information' \
223	     '-l:library database file to use:$files' '-h:show this help message and exit' '-d:destination music directory:$dirs'
224globalopts=("${reply[@]}")
225
226# Create main completion function
227_regex_arguments _beet "$matchany" \( "${globalopts[@]}" \# \) "${beets_regex_words_subcmds[@]}"
228
229# Set tag-order so that options are completed separately from arguments
230zstyle ":completion:${curcontext}:" tag-order '! options'
231
232# Execute the completion function
233_beet "$@"
234
235# Local Variables:
236# mode:shell-script
237# End:
238