#!/usr/bin/env nix-shell
#! nix-shell -i oil -p coreutils ripgrep handlr gnused xxd

# A ripoff from Duckduckgo bangs.

# Examples:
# ```
# bangs hello there ~g ~aw
# ```
# will open a search result page on Google and Arch Wiki

# TODO:
# - Make the config compatible to bangs database from Duckduckgo.
proc usage() {
    cat <<HELP
bangs - a ripoff from Duckduckgo bangs, except
        you can open multiple pages in one go.

Usage:
    bangs [SEARCH_QUERY...] [BANGS...]

A bang is absolutely necessary to indicate what search pages
to open.

While there is a default config, you should create your own list
by setting a config file at '\${XDG_CONFIG_HOME}/bangs/config.json'.
The config is simply a JSON file with the bang as the key and an
object with 'url' and 'name'.

Examples:
- Google and Duckduckgo search
    bangs hello world ~g ~ddg

- Change the bangs prefix
    BANGS_PREFIX="--" bangs how to program in python --g --yt
HELP
}

# Simply prints the given string into percent-encoded equivalent.
#
# `urlencode "Hello world"` will give "Hello%20world"
#
# Stolen from https://gist.github.com/cdown/1163649 and https://gist.github.com/cdown/1163649#gistcomment-1256298.
# Just ported it in Oil script.
proc urlencode(msg) {
  for (i in 0:len(msg)) {
    var char = msg[i]

    case $char {
        [a-zA-Z0-9.~_-])
          printf '%s' $char
          ;;
        *)
          printf '%s' $char | xxd -plain -cols 1 | while read :hex { printf '%%%s' $hex }
          ;;
    }
  }
}

# `printf` except it prints in the stderr stream.
proc warnf(format, @msg) {
  >&2 printf "$format\\n" @msg
}

# The entry point of this program.
proc main {
  # Config-related variables.
  # For now, there is no system-related config.
  # This is primarily a user script, after all. :)
  const config_dir = "${XDG_CONFIG_HOME:-"$HOME/.config"}/bangs"
  const config_file = "${config_dir}/config.json"

  # Note you can configure these variables through the respective environment variables.
  const bangs_prefix = "${BANGS_PREFIX:-~}"
  const bangs_placeholder = "${BANGS_PLACEHOLDER:-{{{s}}}}"

  # These are the default bangs available.
  # Bangs are any keys that shouldn't have whitespace characters.
  # We'll use this in case there is no user configuration.
  #
  # We also made the default config to be more flexible with the placeholder.
  const default_config = {
    'aw': {
      'name': 'Arch Wiki',
      'url': 'https://wiki.archlinux.org/index.php?title=Special%3ASearch&search=' + bangs_placeholder
    },
    'gh': {
      'name': 'GitHub',
      'url': 'https://github.com/search?utf8=%E2%9C%93&q=' + bangs_placeholder
    },
    'g': {
      'name': 'Google',
      'url': 'https://www.google.com/search?q=' + bangs_placeholder
    },
    'so': {
      'name': 'Stack Overflow',
      'url': 'http://stackoverflow.com/search?q=' + bangs_placeholder
    },
    'w': {
      'name': 'Wikipedia',
      'url': 'https://en.wikipedia.org/wiki/Special:Search?search=' + bangs_placeholder
    }
  }

  # Setting up some variables.
  const bangs_format = / %start $bangs_prefix !space+ %end /
  const valid_bangs = %()
  const search_query = %()

  # Config file detection.
  # Otherwise, we'll just use the default config.
  if test -f $config_file {
    json read :bangs < $config_file
  } else {
    var bangs = default_config
  }

  # Show the usage when no arguments was given like any sane program.
  if (len(ARGV) == 0) {
    usage
    exit 0
  }

  # Filter out the bangs from the search query.
  # The bangs are just words prefixed with a certain sequence of characters.
  # We put both bangs and the search query in separate arrays for easier processing.
  # E.g., in the search query `hello ~ddg world ~g`, `~ddg~ and `~g` are the bangs.
  for i in @ARGV {
    # If the argument is not a bang, append in the search query queue.
    write -- $i | rg --quiet $bangs_format || {
      append :search_query $i
      continue
    }

    # Otherwise, put it in the bangs array.
    # Keep in mind, we do throw out bangs that are not in the bangs database.
    var bang = $(write -- $i | sed --regexp-extended --expression "s/^${bangs_prefix}//")
    if (bang in bangs) {
      append :valid_bangs $bang
      warnf "%s will be used to search." $bang
    } else {
      warnf "%s is not found in the database." $bang
    }
  }

  # Encode the query for a consistent formatting.
  # Even though this script is in Oil where it has less problems with splitting strings, we still might want to encode the query for a good measure.
  var query = join(search_query, " ")
  var encoded_query = $(urlencode $query)

  warnf "Search query is '%s'" $query
  warnf "Encoded form is '%s'" $encoded_query

  # Search the query with the given bangs.
  for bang in @valid_bangs {
    var metadata = bangs[bang]
    var url = $(write -- ${metadata['url']} | sed --expression "s/${bangs_placeholder}/${encoded_query}/")

    handlr open $url
  }
}

main @ARGV
