zynaddsubfx

ZynAddSubFX open source synthesizer
Log | Files | Refs | Submodules | LICENSE

commit 88df3977083e244f55fbd3de8618a1eaa7dc18fb
parent 9c5efe26ab297211f56a3641fa23faa9440877a3
Author: Johannes Lorenz <j.git@lorenz-ho.me>
Date:   Sat,  6 Oct 2018 14:27:38 +0200

Add bash completion

Details:
* Add bash completion
* Add parameters `list-inputs` and `list-outputs`
* Choose the preferred spelling `preferred` for `prefered`

Diffstat:
Acmake/BashCompletion.cmake | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/CMakeLists.txt | 3+++
Adoc/bash-completion/CMakeLists.txt | 6++++++
Adoc/bash-completion/zynaddsubfx.in | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Nio/Nio.cpp | 4++--
Msrc/Nio/Nio.h | 4++--
Msrc/main.cpp | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
7 files changed, 497 insertions(+), 48 deletions(-)

diff --git a/cmake/BashCompletion.cmake b/cmake/BashCompletion.cmake @@ -0,0 +1,92 @@ +# A wrapper around pkg-config-provided and cmake-provided bash completion that +# will have dynamic behavior at INSTALL() time to allow both root-level +# INSTALL() as well as user-level INSTALL(). +# +# See also https://github.com/scop/bash-completion +# +# Copyright (c) 2018, Tres Finocchiaro, <tres.finocchiaro@gmail.com> +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# +# Usage: +# INCLUDE(BashCompletion) +# BASHCOMP_INSTALL(foo) +# ... where "foo" is a shell script adjacent to the CMakeLists.txt +# +# How it determines BASHCOMP_PKG_PATH, in order: +# 1. Uses BASHCOMP_PKG_PATH if already set (e.g. -DBASHCOMP_PKG_PATH=...) +# a. If not, uses pkg-config's PKG_CHECK_MODULES to determine path +# b. Fallback to cmake's FIND_PACKAGE(bash-completion) path +# c. Fallback to hard-coded /usr/share/bash-completion/completions +# 2. Final fallback to ${CMAKE_INSTALL_PREFIX}/share/bash-completion/completions if +# detected path is unwritable. + +# - Windows does not support bash completion +# - macOS support should eventually be added for Homebrew (TODO) +IF(WIN32) + MESSAGE(STATUS "Bash competion is not supported on this platform.") +ELSEIF(APPLE) + MESSAGE(STATUS "Bash completion is not yet implemented for this platform.") +ELSE() + INCLUDE(FindUnixCommands) + # Honor manual override if provided + IF(NOT BASHCOMP_PKG_PATH) + # First, use pkg-config, which is the most reliable + FIND_PACKAGE(PkgConfig QUIET) + IF(PKGCONFIG_FOUND) + PKG_CHECK_MODULES(BASH_COMPLETION bash-completion) + PKG_GET_VARIABLE(BASHCOMP_PKG_PATH bash-completion completionsdir) + ELSE() + # Second, use cmake (preferred but less common) + FIND_PACKAGE(bash-completion QUIET) + IF(BASH_COMPLETION_FOUND) + SET(BASHCOMP_PKG_PATH "${BASH_COMPLETION_COMPLETIONSDIR}") + ENDIF() + ENDIF() + + # Third, use a hard-coded fallback value + IF("${BASHCOMP_PKG_PATH}" STREQUAL "") + SET(BASHCOMP_PKG_PATH "/usr/share/bash-completion/completions") + ENDIF() + ENDIF() + + # Always provide a fallback for non-root INSTALL() + SET(BASHCOMP_USER_PATH "${CMAKE_INSTALL_PREFIX}/share/bash-completion/completions") + + # Cmake doesn't allow easy use of conditional logic at INSTALL() time + # this is a problem because ${BASHCOMP_PKG_PATH} may not be writable and we + # need sane fallback behavior for bundled INSTALL() (e.g. .AppImage, etc). + # + # The reason this can't be detected by cmake is that it's fairly common to + # run "cmake" as a one user (i.e. non-root) and "make install" as another user + # (i.e. root). + # + # - Creates a script called "install_${SCRIPT_NAME}_completion.sh" into the + # working binary directory and invokes this script at install. + # - Script handles INSTALL()-time conditional logic for sane ballback behavior + # when ${BASHCOMP_PKG_PATH} is unwritable (i.e. non-root); Something cmake + # can't handle on its own at INSTALL() time) + MACRO(BASHCOMP_INSTALL SCRIPT_NAME) + # A shell script for wrapping conditionl logic + SET(BASHCOMP_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_${SCRIPT_NAME}_completion.sh") + + FILE(WRITE ${BASHCOMP_SCRIPT} "\ +#!${BASH}\n\ +set -e\n\ +BASHCOMP_PKG_PATH=\"${BASHCOMP_USER_PATH}\"\n\ +if [ -w \"${BASHCOMP_PKG_PATH}\" ]; then\n\ + BASHCOMP_PKG_PATH=\"${BASHCOMP_PKG_PATH}\"\n\ +fi\n\ +echo -e \"\\nInstalling bash completion...\\n\"\n\ +mkdir -p \"\$BASHCOMP_PKG_PATH\"\n\ +cp \"${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT_NAME}\" \"\$BASHCOMP_PKG_PATH\"\n\ +chmod a+r \"\$BASHCOMP_PKG_PATH/${SCRIPT_NAME}\"\n\ +echo -e \"Bash completion for ${SCRIPT_NAME} has been installed to \$BASHCOMP_PKG_PATH/${SCRIPT_NAME}\"\n\ +") + INSTALL(CODE "EXECUTE_PROCESS(COMMAND chmod u+x \"install_${SCRIPT_NAME}_completion.sh\" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} )") + INSTALL(CODE "EXECUTE_PROCESS(COMMAND \"./install_${SCRIPT_NAME}_completion.sh\" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} )") + + MESSAGE(STATUS "Bash completion script for ${SCRIPT_NAME} will be installed to ${BASHCOMP_PKG_PATH} or fallback to ${BASHCOMP_USER_PATH} if unwritable.") + ENDMACRO() +ENDIF() + diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt @@ -13,3 +13,6 @@ if(DOXYGEN_FOUND) COMMENT "Generating API documentation with Doxygen" VERBATIM) add_custom_target(doc DEPENDS html) endif() + +add_subdirectory(bash-completion) + diff --git a/doc/bash-completion/CMakeLists.txt b/doc/bash-completion/CMakeLists.txt @@ -0,0 +1,6 @@ +INCLUDE(BashCompletion) +IF(COMMAND BASHCOMP_INSTALL) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/zynaddsubfx.in + ${CMAKE_CURRENT_BINARY_DIR}/zynaddsubfx @ONLY) + BASHCOMP_INSTALL(zynaddsubfx) +ENDIF() diff --git a/doc/bash-completion/zynaddsubfx.in b/doc/bash-completion/zynaddsubfx.in @@ -0,0 +1,296 @@ +# zynaddsubfx(1) completion -*- shell-script -*- + +_zynaddsubfx_array_contains () +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +_zynaddsubfx_long_param_of() +{ + case "$1" in + -l) + echo "load" + ;; + -L) + echo "load-instrument" + ;; + -M) + echo "midi-learn" + ;; + -r) + echo "sample-rate" + ;; + -b) + echo "buffer-size" + ;; + -o) + echo "oscil-size" + ;; + -S) + echo "swap" + ;; + -U) + echo "no-gui" + ;; + -N) + echo "named" + ;; + -a) + echo "auto-connect" + ;; + -A) + echo "auto-save" + ;; + -p) + echo "pid-in-client" + ;; + -P) + echo "preferred-port" + ;; + -O) + echo "output" + ;; + -I) + echo "input" + ;; + -e) + echo "exec-after-init" + ;; + -d) + echo "dump-oscdoc" + ;; + -D) + echo "dump-json-schema" + ;; + *) + echo "" + ;; + esac +} + +_zynaddsubfx() +{ + local cword=$COMP_CWORD + local cur="${COMP_WORDS[COMP_CWORD]}" + local params filetypes pars shortargs + + # call routine provided by bash-completion + _init_completion || return + + # pars without args + pars=(--help --version --auto-connect --no-gui --swap) + pars+=(--pid-in-client-name) + # pars with args + pars+=(--load --load-instrument --midi-learn) + pars+=(--sample-rate --buffer-size --oscil-size) + pars+=(--named --auto-save) + pars+=(--preferred-port --output --input) + pars+=(--exec-after-init --dump-oscdoc --dump-json-schema) + + shortargs=(-h -v -l -L -M -r -b -o -S -U -N -a -A -p -P -O -I -e -d -D) + + local prev= + if [ "$cword" -gt 1 ] + then + prev=${COMP_WORDS[cword-1]} + fi + + # don't show shortargs, but complete them when entered + if [[ $cur =~ ^-[^-]$ ]] + then + if _zynaddsubfx_array_contains "$cur" "${shortargs[@]}" + then + COMPREPLY=( "$cur" ) + fi + return + fi + + + local filemode + + # + # please keep those in order like def_pars_args above + # + case $prev in + --load|-l) + filetypes=xmz + filemode=existing_files + ;; + --load-instrument|-L) + filetypes=xiz + filemode=existing_files + ;; + --midi-learn|-M) + filetypes=xlz + filemode=existing_files + ;; + --sample-rate|-r) + params="8000 16000 44100 48000 96000 192000" + ;; + --buffer-size|-b) + # less than 32 sounds insane, more than 4096 observed buggy + params="32 64 128 256 512 1024 2048 4096" + ;; + --oscil-size|-o) + params="128 256 512 1024 2048 4096" + ;; + --named|-N) + ;; + --auto-save|-A) + params="30 60 120 180 300 450 600 900 1200 1800 2400 3600 " + params+="5400 7200 10800 14400 21600 43200 86400" + ;; + --preferred-port|-P) + params= + ;; + --input|--output|-I|-O) + local jack_enable_cmake=@JackEnable@ + local pa_enable_cmake=@PaEnable@ + local alsa_enable_cmake=@AlsaEnable@ + local oss_enable_cmake=@OssEnable@ + + local jack_enable pa_enable alsa_enable oss_enable + [[ $jack_enable_cmake =~ FALSE|0|OFF|^$ ]] || jack_enable=1 + [[ $pa_enable_cmake =~ FALSE|0|OFF|^$ ]] || pa_enable=1 + [[ $alsa_enable_cmake =~ FALSE|0|OFF|^$ ]] || alsa_enable=1 + [[ $oss_enable_cmake =~ FALSE|0|OFF|^$ ]] || oss_enable=1 + + params="null" + [ "$jack_enable" ] && params+=" jack" + [ "$pa_enable" ] && params+=" pa" + [ "$alsa_enable" ] && params+=" alsa" + [ "$oss_enable" ] && params+=" oss" + if [[ $prev =~ --output|-O ]] + then + [ "$jack_enable" ] && params+=" jack-multi" + [ "$oss_enable" ] && params+=" oss-multi" + fi + + if [[ $prev =~ --output|-O ]] + then + for i in "${COMPREPLY[@]}" + do + if [ "$i" == '-a' ] || [ "$i" == '--autoconnect' ] + then + params="jack jack-multi" + break; + fi + done + fi + ;; + --exec-after-init|-e) + filemode=executables + ;; + --dump-oscdoc|-d) + filemode=files + filetypes=xml + ;; + --dump-json-schema|-D) + filemode=files + filetypes=json + ;; + *) + if [[ $prev =~ --help|-h|-version|-v ]] + then + # the -e flag (from --import) and help/version + # always mark the end of arguments + return + fi + + # add pars to params, but also check the history of comp words + local param + for param in "${pars[@]}" + do + local do_append=1 + for i in "${!COMP_WORDS[@]}" + do + if [ "$i" -ne 0 ] && [ "$i" -ne "$cword" ] + then + # disallow double long parameters + if [ "${COMP_WORDS[$i]}" == "$param" ] + then + do_append= + # disallow double short parameters + elif [ "${COMP_WORDS[$i]:0:1}" == '-' ] && ! [ "${COMP_WORDS[$i]:1:2}" == '-' ] && + [ "--$(_zynaddsubfx_long_param_of "${COMP_WORDS[$i]}")" == "$param" ] + then + do_append= + # --help or --version must be the first parameters + elif [ "$cword" -gt 1 ] && [[ $param =~ --help|--version ]] + then + do_append= + fi + fi + done + if [ "$do_append" ] + then + params+="$param " + fi + done + + ;; + esac + + + case $filemode in + + # use completion routine provided by bash-completion + # to fill $COMPREPLY + + existing_files) + _filedir "@($filetypes)" + ;; + + existing_directories) + _filedir -d + ;; + + executables) + + _filedir + local tmp=("${COMPREPLY[@]}") + COMPREPLY=( ) + for i in "${!tmp[@]}" + do + if [ -f "${tmp[i]}" ] && ! [ -x "${tmp[i]}" ] + then + # if it's a file that can't be executed, omit from completion + true + else + COMPREPLY+=( "${tmp[i]}" ) + fi + done + ;; + + files) + + # non existing files complete like directories... + _filedir -d + + # ...except for non-completing files with the right file type + if [ ${#COMPREPLY[@]} -eq 0 ] + then + if ! [[ "$cur" =~ /$ ]] && [ "$filetypes" ] && [[ "$cur" =~ \.($filetypes)$ ]] + then + # file ending fits, we seem to be done + COMPREPLY=( "$cur" ) + fi + fi + ;; + + esac + + if [ "$params" ] + then + # none of our parameters contain spaces, so deactivate shellcheck's warning + # shellcheck disable=SC2207 + COMPREPLY+=( $(compgen -W "${params}" -- "${cur}") ) + fi + +} + + +complete -F _zynaddsubfx zynaddsubfx diff --git a/src/Nio/Nio.cpp b/src/Nio/Nio.cpp @@ -134,7 +134,7 @@ string Nio::getSink() #if JACK #include <jack/jack.h> -void Nio::preferedSampleRate(unsigned &rate) +void Nio::preferredSampleRate(unsigned &rate) { #if __linux__ //avoid checking in with jack if it's off @@ -157,7 +157,7 @@ void Nio::preferedSampleRate(unsigned &rate) } } #else -void Nio::preferedSampleRate(unsigned &) +void Nio::preferredSampleRate(unsigned &) {} #endif diff --git a/src/Nio/Nio.h b/src/Nio/Nio.h @@ -45,8 +45,8 @@ namespace Nio std::string getSource(void); std::string getSink(void); - //Get the prefered sample rate from jack (if running) - void preferedSampleRate(unsigned &rate); + //Get the preferred sample rate from jack (if running) + void preferredSampleRate(unsigned &rate); //Complete Master Swaps to ONLY BE CALLED FROM RT CONTEXT void masterSwap(Master *master); diff --git a/src/main.cpp b/src/main.cpp @@ -103,9 +103,9 @@ void sigterm_exit(int /*sig*/) /* * Program initialisation */ -void initprogram(SYNTH_T synth, Config* config, int prefered_port) +void initprogram(SYNTH_T synth, Config* config, int preferred_port) { - middleware = new MiddleWare(std::move(synth), config, prefered_port); + middleware = new MiddleWare(std::move(synth), config, preferred_port); master = middleware->spawnMaster(); master->swaplr = swaplr; @@ -241,14 +241,19 @@ int main(int argc, char *argv[]) synth.oscilsize = config.cfg.OscilSize; swaplr = config.cfg.SwapStereo; - Nio::preferedSampleRate(synth.samplerate); + Nio::preferredSampleRate(synth.samplerate); synth.alias(); //build aliases sprng(time(NULL)); + // for option entrys with the 3rd member (flag) pointing here, + // getopt_long*() will return 0 and set this flag to the 4th member (val) + int getopt_flag; + /* Parse command-line options */ struct option opts[] = { + // options with single char equivalents { "load", 2, NULL, 'l' }, @@ -295,7 +300,7 @@ int main(int argc, char *argv[]) "pid-in-client-name", 0, NULL, 'p' }, { - "prefered-port", 1, NULL, 'P', + "preferred-port", 1, NULL, 'P', }, { "output", 1, NULL, 'O' @@ -312,15 +317,31 @@ int main(int argc, char *argv[]) { "dump-json-schema", 2, NULL, 'D' }, + // options without single char equivalents ("getopt_flag" compulsory) + { + "list-inputs", no_argument, &getopt_flag, 'i' + }, + { + "list-outputs", no_argument, &getopt_flag, 'o' + }, { 0, 0, 0, 0 } }; opterr = 0; - int option_index = 0, opt, exitwithhelp = 0, exitwithversion = 0; - int prefered_port = -1; + int option_index = 0, opt; + enum class exit_with_t + { + dont_exit, + help, + version, + list_inputs, + list_outputs + }; + exit_with_t exit_with = exit_with_t::dont_exit; + int preferred_port = -1; int auto_save_interval = 0; -int wmidi = -1; + int wmidi = -1; string loadfile, loadinstrument, execAfterInit, loadmidilearn; @@ -346,10 +367,10 @@ int wmidi = -1; switch(opt) { case 'h': - exitwithhelp = 1; + exit_with = exit_with_t::help; break; case 'v': - exitwithversion = 1; + exit_with = exit_with_t::version; break; case 'Y': /* this command a dummy command (has NO effect) and is used because I need for NSIS installer @@ -422,7 +443,7 @@ int wmidi = -1; break; case 'P': if(optarguments) - prefered_port = atoi(optarguments); + preferred_port = atoi(optarguments); break; case 'A': if(optarguments) @@ -456,49 +477,80 @@ int wmidi = -1; if(optarguments) wmidi = atoi(optarguments); break; + case 0: // catch options without single char equivalent + switch(getopt_flag) + { + case 'i': + exit_with = exit_with_t::list_inputs; + break; + case 'o': + exit_with = exit_with_t::list_outputs; + break; + } + break; case '?': cerr << "ERROR:Bad option or parameter.\n" << endl; - exitwithhelp = 1; + exit_with = exit_with_t::help; break; } } synth.alias(); - if(exitwithversion) { - cout << "Version: " << version << endl; - return 0; + switch (exit_with) + { + case exit_with_t::version: + cout << "Version: " << version << endl; + break; + case exit_with_t::help: + cout << "Usage: zynaddsubfx [OPTION]\n\n" + << " -h , --help \t\t\t\t Display command-line help and exit\n" + << " -v , --version \t\t\t Display version and exit\n" + << " -l file, --load=FILE\t\t\t Loads a .xmz file\n" + << " -L file, --load-instrument=FILE\t Loads a .xiz file\n" + << " -M file, --midi-learn=FILE\t\t Loads a .xlz file\n" + << " -r SR, --sample-rate=SR\t\t Set the sample rate SR\n" + << + " -b BS, --buffer-size=SR\t\t Set the buffer size (granularity)\n" + << " -o OS, --oscil-size=OS\t\t Set the ADsynth oscil. size\n" + << " -S , --swap\t\t\t\t Swap Left <--> Right\n" + << + " -U , --no-gui\t\t\t\t Run ZynAddSubFX without user interface\n" + << " -N , --named\t\t\t\t Postfix IO Name when possible\n" + << " -a , --auto-connect\t\t\t AutoConnect when using JACK\n" + << " -A , --auto-save=INTERVAL\t\t Automatically save at interval\n" + << "\t\t\t\t\t (disabled with 0 interval)\n" + << " -p , --pid-in-client-name\t\t Append PID to (JACK) " + "client name\n" + << " -P , --preferred-port\t\t\t Preferred OSC Port\n" + << " -O , --output\t\t\t\t Set Output Engine\n" + << " -I , --input\t\t\t\t Set Input Engine\n" + << " -e , --exec-after-init\t\t Run post-initialization script\n" + << " -d , --dump-oscdoc=FILE\t\t Dump oscdoc xml to file\n" + << " -D , --dump-json-schema=FILE\t\t Dump osc schema (.json) to file\n" + << endl; + break; + case exit_with_t::list_inputs: + case exit_with_t::list_outputs: + { + Nio::init(synth, config.cfg.oss_devs, nullptr); + auto get_func = (getopt_flag == 'i') + ? &Nio::getSources + : &Nio::getSinks; + std::set<std::string> engines = (*get_func)(); + for(std::string engine : engines) + { + std::transform(engine.begin(), engine.end(), engine.begin(), + ::tolower); + cout << engine << endl; + } + break; + } + default: + break; } - if(exitwithhelp != 0) { - cout << "Usage: zynaddsubfx [OPTION]\n\n" - << " -h , --help \t\t\t\t Display command-line help and exit\n" - << " -v , --version \t\t\t Display version and exit\n" - << " -l file, --load=FILE\t\t\t Loads a .xmz file\n" - << " -L file, --load-instrument=FILE\t Loads a .xiz file\n" - << " -M file, --midi-learn=FILE\t\t Loads a .xlz file\n" - << " -r SR, --sample-rate=SR\t\t Set the sample rate SR\n" - << - " -b BS, --buffer-size=SR\t\t Set the buffer size (granularity)\n" - << " -o OS, --oscil-size=OS\t\t Set the ADsynth oscil. size\n" - << " -S , --swap\t\t\t\t Swap Left <--> Right\n" - << - " -U , --no-gui\t\t\t\t Run ZynAddSubFX without user interface\n" - << " -N , --named\t\t\t\t Postfix IO Name when possible\n" - << " -a , --auto-connect\t\t\t AutoConnect when using JACK\n" - << " -A , --auto-save=INTERVAL\t\t Automatically save at interval\n" - << "\t\t\t\t\t (disabled with 0 interval)\n" - << " -p , --pid-in-client-name\t\t Append PID to (JACK) " - "client name\n" - << " -P , --preferred-port\t\t\t Preferred OSC Port\n" - << " -O , --output\t\t\t\t Set Output Engine\n" - << " -I , --input\t\t\t\t Set Input Engine\n" - << " -e , --exec-after-init\t\t Run post-initialization script\n" - << " -d , --dump-oscdoc=FILE\t\t Dump oscdoc xml to file\n" - << " -D , --dump-json-schema=FILE\t\t Dump osc schema (.json) to file\n" - << endl; - + if(exit_with != exit_with_t::dont_exit) return 0; - } cerr.precision(1); cerr << std::fixed; @@ -507,7 +559,7 @@ int wmidi = -1; cerr << "Internal latency = \t" << synth.dt() * 1000.0f << " ms" << endl; cerr << "ADsynth Oscil.Size = \t" << synth.oscilsize << " samples" << endl; - initprogram(std::move(synth), &config, prefered_port); + initprogram(std::move(synth), &config, preferred_port); bool altered_master = false; if(!loadfile.empty()) {