浏览代码

osx: refactor plugin and add a few features (#9026)

Co-authored-by: Capybara <git.capybara@gmail.com>
Co-authored-by: drootang <drootang@users.noreply.github.com>
Co-authored-by: Augusto Souza <augustorsouza@gmail.com>
Co-authored-by: PatrBal <44707588+PatrBal@users.noreply.github.com>
Marc Cornellà 3 年之前
父节点
当前提交
d47447a5e6
共有 4 个文件被更改,包括 353 次插入201 次删除
  1. 6 3
      plugins/osx/README.md
  2. 90 0
      plugins/osx/_security
  3. 170 0
      plugins/osx/music
  4. 87 198
      plugins/osx/osx.plugin.zsh

+ 6 - 3
plugins/osx/README.md

@@ -22,14 +22,17 @@ Original author: [Sorin Ionescu](https://github.com/sorin-ionescu)
 | `pfs`           | Return the current Finder selection                   |
 | `cdf`           | `cd` to the current Finder directory                  |
 | `pushdf`        | `pushd` to the current Finder directory               |
+| `pxd`           | Return the current Xcode project directory            |
+| `cdx`           | `cd` to the current Xcode project directory           |
 | `quick-look`    | Quick-Look a specified file                           |
 | `man-preview`   | Open a specified man page in Preview app              |
-| `showfiles`     | Show hidden files                                     |
-| `hidefiles`     | Hide the hidden files                                 |
-| `itunes`        | DEPRECATED. Use `music` from macOS Catalina on        |
+| `showfiles`     | Show hidden files in Finder                           |
+| `hidefiles`     | Hide the hidden files in Finder                       |
+| `itunes`        | _DEPRECATED_. Use `music` from macOS Catalina on      |
 | `music`         | Control Apple Music. Use `music -h` for usage details |
 | `spotify`       | Control Spotify and search by artist, album, track…   |
 | `rmdsstore`     | Remove .DS\_Store files recursively in a directory    |
+| `btrestart`     | Restart the Bluetooth daemon                          |
 
 ## Acknowledgements
 

+ 90 - 0
plugins/osx/_security

@@ -0,0 +1,90 @@
+#compdef security
+
+local -a _1st_arguments
+_1st_arguments=(
+  'help:Show all commands, or show usage for a command'
+  'list-keychains:Display or manipulate the keychain search list'
+  'default-keychain:Display or set the default keychain'
+  'login-keychain:Display or set the login keychain'
+  'create-keychain:Create keychains and add them to the search list'
+  'delete-keychain:Delete keychains and remove them from the search list'
+  'lock-keychain:Lock the specified keychain'
+  'lock-keychain:Unlock the specified keychain'
+  'set-keychain-settings:Set settings for a keychain'
+  'set-keychain-password:Set password for a keychain'
+  'show-keychain-info:Show the settings for keychain'
+  'dump-keychain:Dump the contents of one or more keychains'
+  'create-keypair:Create an asymmetric key pair'
+  'add-generic-password:Add a generic password item'
+  'add-internet-password:Add an internet password item'
+  'add-certificates:Add certificates to a keychain'
+  'find-generic-password:Find a generic password item'
+  'delete-generic-password:Delete a generic password item'
+  'find-internet-password:Find an internet password item'
+  'delete-internet-password:Delete an internet password item'
+  'find-certificate:Find a certificate item'
+  'find-identity:Find an identity certificate + private key'
+  'delete-certificate:Delete a certificate from a keychain'
+  'set-identity-preference:Set the preferred identity to use for a service'
+  'get-identity-preference:Get the preferred identity to use for a service'
+  'create-db:Create a db using the DL'
+  'export:Export items from a keychain'
+  'import:Import items into a keychain'
+  'cms:Encode or decode CMS messages'
+  'install-mds:MDS database'
+  'add-trusted-cert:Add trusted certificates:'
+  'remove-trusted-cert:Remove trusted certificates:'
+  'dump-trust-settings:Display contents of trust settings'
+  'user-trust-settings-enable:Display or manipulate user-level trust settings'
+  'trust-settings-export:Export trust settings'
+  'trust-settings-import:Import trust settings'
+  'verify-cert:Verify certificates:'
+  'authorize:Perform authorization operations'
+  'authorizationdb:Make changes to the authorization policy database'
+  'execute-with-privileges:Execute tool with privileges'
+  'leaks:Run /usr/bin/leaks on this process'
+  'error:Display a descriptive message for the given error codes:'
+  'create-filevaultmaster-keychain:"Create a keychain containing a key pair for FileVault recovery use'
+)
+_arguments '*:: :->command'
+
+if (( CURRENT == 1 )); then
+  _describe -t commands "security command" _1st_arguments
+  return
+fi
+
+case "$words[1]" in
+  find-(generic|internet)-password)
+    _values \
+      'Usage: find-[internet/generic]-password [-a account] [-s server] [options...] [-g] [keychain...]' \
+      '-a[Match "account" string]' \
+      '-c[Match "creator" (four-character code)]' \
+      '-C[Match "type" (four-character code)]' \
+      '-D[Match "kind" string]' \
+      '-G[Match "value" string (generic attribute)]' \
+      '-j[Match "comment" string]' \
+      '-l[Match "label" string]' \
+      '-s[Match "service" string]' \
+      '-g[Display the password for the item found]' \
+      '-w[Display only the password on stdout]' ;;
+  add-(generic|internet)-password)
+    _values \
+      'Usage: add-[internet/generic]-password [-a account] [-s server] [-w password] [options...] [-A|-T appPath] [keychain]]' \
+      '-a[Specify account name (required)]' \
+      '-c[Specify item creator (optional four-character code)]' \
+      '-C[Specify item type (optional four-character code)]' \
+      '-d[Specify security domain string (optional)]' \
+      '-D[Specify kind (default is "Internet password")]' \
+      '-j[Specify comment string (optional)]' \
+      '-l[Specify label (if omitted, server name is used as default label)]' \
+      '-p[Specify path string (optional)]' \
+      '-P[Specify port number (optional)]' \
+      '-r[Specify protocol (optional four-character SecProtocolType, e.g. "http", "ftp ")]' \
+      '-s[Specify server name (required)]' \
+      '-t[Specify authentication type (as a four-character SecAuthenticationType, default is "dflt")]' \
+      '-w[Specify password to be added]' \
+      '-A[Allow any application to access this item without warning (insecure, not recommended!)]' \
+      '-T[Specify an application which may access this item (multiple -T options are allowed)]' \
+      '-U[Update item if it already exists (if omitted, the item cannot already exist) ]' \
+      'utils)]' ;;
+esac

+ 170 - 0
plugins/osx/music

@@ -0,0 +1,170 @@
+#!/usr/bin/env zsh
+
+function music itunes() {
+  local APP_NAME=Music sw_vers=$(sw_vers -productVersion 2>/dev/null)
+
+  autoload is-at-least
+  if [[ -z "$sw_vers" ]] || is-at-least 10.15 $sw_vers; then
+    if [[ $0 = itunes ]]; then
+      echo >&2 The itunes function name is deprecated. Use \'music\' instead.
+      return 1
+    fi
+  else
+    APP_NAME=iTunes
+  fi
+
+  local opt=$1 playlist=$2
+  (( $# > 0 )) && shift
+  case "$opt" in
+    launch|play|pause|stop|rewind|resume|quit)
+      ;;
+    mute)
+      opt="set mute to true"
+      ;;
+    unmute)
+      opt="set mute to false"
+      ;;
+    next|previous)
+      opt="$opt track"
+      ;;
+    vol)
+      local new_volume volume=$(osascript -e "tell application \"$APP_NAME\" to get sound volume")
+      if [[ $# -eq 0 ]]; then
+        echo "Current volume is ${volume}."
+        return 0
+      fi
+      case $1 in
+        up) new_volume=$((volume + 10 < 100 ? volume + 10 : 100)) ;;
+        down) new_volume=$((volume - 10 > 0 ? volume - 10 : 0)) ;;
+        <0-100>) new_volume=$1 ;;
+        *) echo "'$1' is not valid. Expected <0-100>, up or down."
+           return 1 ;;
+      esac
+      opt="set sound volume to ${new_volume}"
+      ;;
+    playlist)
+      # Inspired by: https://gist.github.com/nakajijapan/ac8b45371064ae98ea7f
+      if [[ -n "$playlist" ]]; then
+        osascript 2>/dev/null <<EOF
+          tell application "$APP_NAME"
+            set new_playlist to "$playlist" as string
+            play playlist new_playlist
+          end tell
+EOF
+        if [[ $? -eq 0 ]]; then
+          opt="play"
+        else
+          opt="stop"
+        fi
+      else
+        opt="set allPlaylists to (get name of every playlist)"
+      fi
+      ;;
+    playing|status)
+      local currenttrack currentartist state=$(osascript -e "tell application \"$APP_NAME\" to player state as string")
+      if [[ "$state" = "playing" ]]; then
+        currenttrack=$(osascript -e "tell application \"$APP_NAME\" to name of current track as string")
+        currentartist=$(osascript -e "tell application \"$APP_NAME\" to artist of current track as string")
+        echo -E "Listening to ${fg[yellow]}${currenttrack}${reset_color} by ${fg[yellow]}${currentartist}${reset_color}"
+      else
+        echo "$APP_NAME is $state"
+      fi
+      return 0
+      ;;
+    shuf|shuff|shuffle)
+      # The shuffle property of current playlist can't be changed in iTunes 12,
+      # so this workaround uses AppleScript to simulate user input instead.
+      # Defaults to toggling when no options are given.
+      # The toggle option depends on the shuffle button being visible in the Now playing area.
+      # On and off use the menu bar items.
+      local state=$1
+
+      if [[ -n "$state" && "$state" != (on|off|toggle) ]]; then
+        print "Usage: $0 shuffle [on|off|toggle]. Invalid option."
+        return 1
+      fi
+
+      case "$state" in
+        on|off)
+          # Inspired by: https://stackoverflow.com/a/14675583
+          osascript >/dev/null 2>&1 <<EOF
+            tell application "System Events" to perform action "AXPress" of (menu item "${state}" of menu "Shuffle" of menu item "Shuffle" of menu "Controls" of menu bar item "Controls" of menu bar 1 of application process "iTunes" )
+EOF
+          return 0
+          ;;
+        toggle|*)
+          osascript >/dev/null 2>&1 <<EOF
+            tell application "System Events" to perform action "AXPress" of (button 2 of process "iTunes"'s window "iTunes"'s scroll area 1)
+EOF
+          return 0
+          ;;
+      esac
+      ;;
+    ""|-h|--help)
+      echo "Usage: $0 <option>"
+      echo "option:"
+      echo "\t-h|--help\tShow this message and exit"
+      echo "\tlaunch|play|pause|stop|rewind|resume|quit"
+      echo "\tmute|unmute\tMute or unmute $APP_NAME"
+      echo "\tnext|previous\tPlay next or previous track"
+      echo "\tshuf|shuffle [on|off|toggle]\tSet shuffled playback. Default: toggle. Note: toggle doesn't support the MiniPlayer."
+      echo "\tvol [0-100|up|down]\tGet or set the volume. 0 to 100 sets the volume. 'up' / 'down' increases / decreases by 10 points. No argument displays current volume."
+      echo "\tplaying|status\tShow what song is currently playing in Music."
+      echo "\tplaylist [playlist name]\t Play specific playlist"
+      return 0
+      ;;
+    *)
+      print "Unknown option: $opt"
+      return 1
+      ;;
+  esac
+  osascript -e "tell application \"$APP_NAME\" to $opt"
+}
+
+function _music() {
+  local app_name
+  case "$words[1]" in
+    itunes) app_name="iTunes" ;;
+    music|*) app_name="Music" ;;
+  esac
+
+  local -a cmds subcmds
+  cmds=(
+    "launch:Launch the ${app_name} app"
+    "play:Play ${app_name}"
+    "pause:Pause ${app_name}"
+    "stop:Stop ${app_name}"
+    "rewind:Rewind ${app_name}"
+    "resume:Resume ${app_name}"
+    "quit:Quit ${app_name}"
+    "mute:Mute the ${app_name} app"
+    "unmute:Unmute the ${app_name} app"
+    "next:Skip to the next song"
+    "previous:Skip to the previous song"
+    "vol:Change the volume"
+    "playlist:Play a specific playlist"
+    {playing,status}":Show what song is currently playing"
+    {shuf,shuff,shuffle}":Set shuffle mode"
+    {-h,--help}":Show usage"
+  )
+
+  if (( CURRENT == 2 )); then
+    _describe 'command' cmds
+  elif (( CURRENT == 3 )); then
+    case "$words[2]" in
+      vol) subcmds=( 'up:Raise the volume' 'down:Lower the volume' )
+        _describe 'command' subcmds ;;
+      shuf|shuff|shuffle) subcmds=('on:Switch on shuffle mode' 'off:Switch off shuffle mode' 'toggle:Toggle shuffle mode (default)')
+        _describe 'command' subcmds ;;
+    esac
+  elif (( CURRENT == 4 )); then
+    case "$words[2]" in
+      playlist) subcmds=('play:Play the playlist (default)' 'stop:Stop the playlist')
+        _describe 'command' subcmds ;;
+    esac
+  fi
+
+  return 0
+}
+
+compdef _music music itunes

+ 87 - 198
plugins/osx/osx.plugin.zsh

@@ -1,15 +1,22 @@
 # Open the current directory in a Finder window
 alias ofd='open_command $PWD'
 
+# Show/hide hidden files in the Finder
+alias showfiles="defaults write com.apple.finder AppleShowAllFiles -bool true && killall Finder"
+alias hidefiles="defaults write com.apple.finder AppleShowAllFiles -bool false && killall Finder"
+
+# Bluetooth restart
+function btrestart() {
+  sudo kextunload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport
+  sudo kextload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport
+}
+
 function _omz_osx_get_frontmost_app() {
-  local the_app=$(
-    osascript 2>/dev/null <<EOF
-      tell application "System Events"
-        name of first item of (every process whose frontmost is true)
-      end tell
+  osascript 2>/dev/null <<EOF
+    tell application "System Events"
+      name of first item of (every process whose frontmost is true)
+    end tell
 EOF
-  )
-  echo "$the_app"
 }
 
 function tab() {
@@ -27,7 +34,6 @@ function tab() {
       end tell
       tell application "Terminal" to do script "${command}" in front window
 EOF
-
   elif [[ "$the_app" == 'iTerm' ]]; then
     osascript <<EOF
       tell application "iTerm"
@@ -41,32 +47,29 @@ EOF
         end tell
       end tell
 EOF
-
   elif [[ "$the_app" == 'iTerm2' ]]; then
-      osascript <<EOF
-        tell application "iTerm2"
-          tell current window
-            create tab with default profile
-            tell current session to write text "${command}"
-          end tell
+    osascript <<EOF
+      tell application "iTerm2"
+        tell current window
+          create tab with default profile
+          tell current session to write text "${command}"
         end tell
+      end tell
 EOF
   elif [[ "$the_app" == 'Hyper' ]]; then
     osascript >/dev/null <<EOF
-          tell application "System Events"
-            tell process "Hyper" to keystroke "t" using command down
-          end tell
-          delay 1
-          tell application "System Events"
-              keystroke "${command}"
-              key code 36  #(presses enter)
-            end tell
+      tell application "System Events"
+        tell process "Hyper" to keystroke "t" using command down
+      end tell
+      delay 1
+      tell application "System Events"
+        keystroke "${command}"
+        key code 36  #(presses enter)
+      end tell
 EOF
-
   else
-    echo "tab: unsupported terminal app: $the_app"
-    false
-
+    echo "$0: unsupported terminal app: $the_app" >&2
+    return 1
   fi
 }
 
@@ -79,7 +82,6 @@ function vsplit_tab() {
   if [[ "$the_app" == 'iTerm' ]]; then
     osascript <<EOF
       -- tell application "iTerm" to activate
-
       tell application "System Events"
         tell process "iTerm"
           tell menu item "Split Vertically With Current Profile" of menu "Shell" of menu bar item "Shell" of menu bar 1
@@ -89,37 +91,33 @@ function vsplit_tab() {
         keystroke "${command} \n"
       end tell
 EOF
-
   elif [[ "$the_app" == 'iTerm2' ]]; then
-      osascript <<EOF
-        tell application "iTerm2"
-          tell current session of first window
-            set newSession to (split vertically with same profile)
-            tell newSession
-              write text "${command}"
-              select
-            end tell
+    osascript <<EOF
+      tell application "iTerm2"
+        tell current session of first window
+          set newSession to (split vertically with same profile)
+          tell newSession
+            write text "${command}"
+            select
           end tell
         end tell
+      end tell
 EOF
-  
   elif [[ "$the_app" == 'Hyper' ]]; then
-      osascript >/dev/null <<EOF
-      tell application "System Events"
-        tell process "Hyper"
-          tell menu item "Split Vertically" of menu "Shell" of menu bar 1
-            click
-          end tell
+    osascript >/dev/null <<EOF
+    tell application "System Events"
+      tell process "Hyper"
+        tell menu item "Split Vertically" of menu "Shell" of menu bar 1
+          click
         end tell
-        delay 1
-        keystroke "${command} \n"
       end tell
+      delay 1
+      keystroke "${command} \n"
+    end tell
 EOF
-
   else
     echo "$0: unsupported terminal app: $the_app" >&2
-    false
-
+    return 1
   fi
 }
 
@@ -142,44 +140,40 @@ function split_tab() {
         keystroke "${command} \n"
       end tell
 EOF
-
   elif [[ "$the_app" == 'iTerm2' ]]; then
-      osascript <<EOF
-        tell application "iTerm2"
-          tell current session of first window
-            set newSession to (split horizontally with same profile)
-            tell newSession
-              write text "${command}"
-              select
-            end tell
+    osascript <<EOF
+      tell application "iTerm2"
+        tell current session of first window
+          set newSession to (split horizontally with same profile)
+          tell newSession
+            write text "${command}"
+            select
           end tell
         end tell
+      end tell
 EOF
-
   elif [[ "$the_app" == 'Hyper' ]]; then
-      osascript >/dev/null <<EOF
-      tell application "System Events"
-        tell process "Hyper"
-          tell menu item "Split Horizontally" of menu "Shell" of menu bar 1
-            click
-          end tell
+    osascript >/dev/null <<EOF
+    tell application "System Events"
+      tell process "Hyper"
+        tell menu item "Split Horizontally" of menu "Shell" of menu bar 1
+          click
         end tell
-        delay 1
-        keystroke "${command} \n"
       end tell
+      delay 1
+      keystroke "${command} \n"
+    end tell
 EOF
-
   else
     echo "$0: unsupported terminal app: $the_app" >&2
-    false
-
+    return 1
   fi
 }
 
 function pfd() {
   osascript 2>/dev/null <<EOF
     tell application "Finder"
-      return POSIX path of (target of window 1 as alias)
+      return POSIX path of (insertion location as alias)
     end tell
 EOF
 }
@@ -205,6 +199,21 @@ function pushdf() {
   pushd "$(pfd)"
 }
 
+function pxd() {
+  dirname $(osascript 2>/dev/null <<EOF
+    if application "Xcode" is running then
+      tell application "Xcode"
+        return path of active workspace document
+      end tell
+    end if
+EOF
+)
+}
+
+function cdx() {
+  cd "$(pxd)"
+}
+
 function quick-look() {
   (( $# > 0 )) && qlmanage -p $* &>/dev/null &
 }
@@ -218,133 +227,13 @@ function vncviewer() {
   open vnc://$@
 }
 
-# iTunes control function
-function itunes music() {
-	local APP_NAME=Music
-
-	autoload is-at-least
-	if is-at-least 10.15 $(sw_vers -productVersion); then
-		if [[ $0 = itunes ]]; then
-			echo >&2 The itunes function name is deprecated. Use \`music\' instead.
-			return 1
-		fi
-	else
-		APP_NAME=iTunes
-	fi
-
-	local opt=$1
-	local playlist=$2
-	shift
-	case "$opt" in
-		launch|play|pause|stop|rewind|resume|quit)
-			;;
-		mute)
-			opt="set mute to true"
-			;;
-		unmute)
-			opt="set mute to false"
-			;;
-		next|previous)
-			opt="$opt track"
-			;;
-		vol)
-			local new_volume volume=$(osascript -e "tell application \"$APP_NAME\" to get sound volume")
-			if [[ $# -eq 0 ]]; then
-				echo "Current volume is ${volume}."
-				return 0
-			fi
-			case $1 in
-				up) new_volume=$((volume + 10 < 100 ? volume + 10 : 100)) ;;
-				down) new_volume=$((volume - 10 > 0 ? volume - 10 : 0)) ;;
-				<0-100>) new_volume=$1 ;;
-				*) echo "'$1' is not valid. Expected <0-100>, up or down."
-				   return 1 ;;
-			esac
-			opt="set sound volume to ${new_volume}"
-			;;
-		playlist)
-			# Inspired by: https://gist.github.com/nakajijapan/ac8b45371064ae98ea7f
-			if [[ ! -z "$playlist" ]]; then
-				osascript -e "tell application \"$APP_NAME\"" -e "set new_playlist to \"$playlist\" as string" -e "play playlist new_playlist" -e "end tell" 2>/dev/null;
-				if [[ $? -eq 0 ]]; then
-					opt="play"
-				else
-					opt="stop"
-				fi
-			else
-				opt="set allPlaylists to (get name of every playlist)"
-			fi
-			;;
-		playing|status)
-			local state=`osascript -e "tell application \"$APP_NAME\" to player state as string"`
-			if [[ "$state" = "playing" ]]; then
-				currenttrack=`osascript -e "tell application \"$APP_NAME\" to name of current track as string"`
-				currentartist=`osascript -e "tell application \"$APP_NAME\" to artist of current track as string"`
-				echo -E "Listening to $fg[yellow]$currenttrack$reset_color by $fg[yellow]$currentartist$reset_color";
-			else
-				echo "$APP_NAME is" $state;
-			fi
-			return 0
-			;;
-		shuf|shuff|shuffle)
-			# The shuffle property of current playlist can't be changed in iTunes 12,
-			# so this workaround uses AppleScript to simulate user input instead.
-			# Defaults to toggling when no options are given.
-			# The toggle option depends on the shuffle button being visible in the Now playing area.
-			# On and off use the menu bar items.
-			local state=$1
-
-			if [[ -n "$state" && ! "$state" =~ "^(on|off|toggle)$" ]]
-			then
-				print "Usage: $0 shuffle [on|off|toggle]. Invalid option."
-				return 1
-			fi
-
-			case "$state" in
-				on|off)
-					# Inspired by: https://stackoverflow.com/a/14675583
-					osascript 1>/dev/null 2>&1 <<-EOF
-					tell application "System Events" to perform action "AXPress" of (menu item "${state}" of menu "Shuffle" of menu item "Shuffle" of menu "Controls" of menu bar item "Controls" of menu bar 1 of application process "iTunes" )
-EOF
-					return 0
-					;;
-				toggle|*)
-					osascript 1>/dev/null 2>&1 <<-EOF
-					tell application "System Events" to perform action "AXPress" of (button 2 of process "iTunes"'s window "iTunes"'s scroll area 1)
-EOF
-					return 0
-					;;
-			esac
-			;;
-		""|-h|--help)
-			echo "Usage: $0 <option>"
-			echo "option:"
-			echo "\tlaunch|play|pause|stop|rewind|resume|quit"
-			echo "\tmute|unmute\tcontrol volume set"
-			echo "\tnext|previous\tplay next or previous track"
-			echo "\tshuf|shuffle [on|off|toggle]\tSet shuffled playback. Default: toggle. Note: toggle doesn't support the MiniPlayer."
-			echo "\tvol [0-100|up|down]\tGet or set the volume. 0 to 100 sets the volume. 'up' / 'down' increases / decreases by 10 points. No argument displays current volume."
-			echo "\tplaying|status\tShow what song is currently playing in Music."
-			echo "\tplaylist [playlist name]\t Play specific playlist"
-			echo "\thelp\tshow this message and exit"
-			return 0
-			;;
-		*)
-			print "Unknown option: $opt"
-			return 1
-			;;
-	esac
-	osascript -e "tell application \"$APP_NAME\" to $opt"
-}
-
-# Spotify control function
-source ${ZSH}/plugins/osx/spotify
-
-# Show/hide hidden files in the Finder
-alias showfiles="defaults write com.apple.finder AppleShowAllFiles -bool true && killall Finder"
-alias hidefiles="defaults write com.apple.finder AppleShowAllFiles -bool false && killall Finder"
-
 # Remove .DS_Store files recursively in a directory, default .
 function rmdsstore() {
-	find "${@:-.}" -type f -name .DS_Store -delete
+  find "${@:-.}" -type f -name .DS_Store -delete
 }
+
+# Music / iTunes control function
+source "${0:h:A}/music"
+
+# Spotify control function
+source "${0:h:A}/spotify"