async function saveFileDialog (opts = {}) {
  // validate
  assert(opts && typeof opts === 'object', 'Must pass an options object')
  assert(!opts.title || typeof opts.title === 'string', '.title must be a string')
  assert(!opts.buttonLabel || typeof opts.buttonLabel === 'string', '.buttonLabel must be a string')
  assert(!opts.drive || typeof opts.drive === 'string', '.drive must be a string')
  assert(!opts.defaultPath || typeof opts.defaultPath === 'string', '.defaultPath must be a string')
  assert(!opts.defaultFilename || typeof opts.defaultFilename === 'string', '.defaultFilename must be a string')
  if (opts.filters) {
    assert(typeof opts.filters === 'object', '.filters must be an object')
    assert(!opts.filters.extensions || isStrArray(opts.filters.extensions), '.filters.extensions must be an array of strings')
    assert(!opts.filters.networked || typeof opts.filters.networked === 'boolean', '.filters.networked must be a boolean')
  }

  // initiate the modal
  opts.saveMode = true
  var res
  try {
    while (true) {
      res = await modals.create(this.sender, 'select-file', opts)
      if (res && res.gotoCreateDrive) {
        res = await modals.create(this.sender, 'create-drive').catch(e => undefined)
        if (res && res.gotoSync) {
          await modals.create(this.sender, 'folder-sync', {url: res.url, closeAfterSync: true})
        }
        if (res) opts.drive = res.url
      } else {
        break
      }
    }
  } catch (e) {
    if (e.name !== 'Error') {
      throw e // only rethrow if a specific error
    }
  }
  if (!res) throw new UserDeniedError()
  return res
}
@param {Object} [opts]
@param {string} [opts.title]
@param {string} [opts.buttonLabel]
@param {string} [opts.drive]
@param {string} [opts.defaultPath]
@param {string} [opts.defaultFilename]
@param {string} [opts.extension]
@param {Object} [opts.filters]
@param {string[]} [opts.filters.extensions]
@param {boolean} [opts.filters.networked]
@returns {Promise<string[]>}