| 1 | #!/bin/sh |
| 2 | # Requires: rofi, a browser |
| 3 | # Web search: pick an engine, type a query, land in the browser. Typing a |
| 4 | # query straight into the engine list also works — anything that is not one |
| 5 | # of the listed engines is treated as a query for the first one. |
| 6 | # Add engines by adding lines to ENGINES: "<icon> <name>\t<url with %s>". |
| 7 | set -u |
| 8 | |
| 9 | BROWSER_CMD=${BROWSER:-} |
| 10 | if [ -z "$BROWSER_CMD" ]; then |
| 11 | for candidate in helium.AppImage chromium firefox qutebrowser xdg-open; do |
| 12 | if command -v "$candidate" >/dev/null 2>&1; then |
| 13 | BROWSER_CMD=$candidate |
| 14 | break |
| 15 | fi |
| 16 | done |
| 17 | fi |
| 18 | [ -n "$BROWSER_CMD" ] || { notify-send "Web search" "No browser found"; exit 1; } |
| 19 | |
| 20 | # The AppImage refuses to start under this user without it, the same way the |
| 21 | # Mod+O keybinding launches it. |
| 22 | BROWSER_ARGS="" |
| 23 | case "$BROWSER_CMD" in |
| 24 | *helium*) BROWSER_ARGS="--no-sandbox" ;; |
| 25 | esac |
| 26 | |
| 27 | ENGINES=$(printf '%s\n' \ |
| 28 | " kagi https://kagi.com/search?q=%s" \ |
| 29 | " github https://github.com/search?q=%s" \ |
| 30 | " npm https://www.npmjs.com/search?q=%s" \ |
| 31 | " crates.io https://crates.io/search?q=%s" \ |
| 32 | " mdn https://developer.mozilla.org/en-US/search?q=%s" \ |
| 33 | " alpine packages https://pkgs.alpinelinux.org/packages?name=%s" \ |
| 34 | " wikipedia https://en.wikipedia.org/w/index.php?search=%s" \ |
| 35 | " youtube https://www.youtube.com/results?search_query=%s" \ |
| 36 | " stack overflow https://stackoverflow.com/search?q=%s") |
| 37 | |
| 38 | # Percent-encode by byte, so a query with an accent or an emoji survives the |
| 39 | # trip. od gives the bytes; awk decides which ones may travel as themselves. |
| 40 | urlencode() { |
| 41 | printf '%s' "$1" | od -An -tx1 -v | tr -s ' ' '\n' | awk ' |
| 42 | function h2d(h, i, c, d) { |
| 43 | d = 0 |
| 44 | for (i = 1; i <= length(h); i++) { |
| 45 | c = index("0123456789abcdef", substr(tolower(h), i, 1)) - 1 |
| 46 | d = d * 16 + c |
| 47 | } |
| 48 | return d |
| 49 | } |
| 50 | NF { |
| 51 | d = h2d($1) |
| 52 | ch = sprintf("%c", d) |
| 53 | if (ch ~ /^[A-Za-z0-9._~-]$/) printf "%s", ch |
| 54 | else printf "%%%s", toupper($1) |
| 55 | } |
| 56 | ' |
| 57 | } |
| 58 | |
| 59 | CHOICE=$(printf '%s\n' "$ENGINES" | cut -f1 | rofi -dmenu -i -l 9 -p "search") || exit 0 |
| 60 | [ -n "$CHOICE" ] || exit 0 |
| 61 | |
| 62 | # rofi hands back whatever was typed when nothing matched: that is a query |
| 63 | # for the default engine, not an engine name. |
| 64 | URL=$(printf '%s\n' "$ENGINES" | awk -F'\t' -v c="$CHOICE" '$1 == c { print $2; exit }') |
| 65 | if [ -n "$URL" ]; then |
| 66 | QUERY=$(rofi -dmenu -p "${CHOICE##* }" -l 0 </dev/null) || exit 0 |
| 67 | else |
| 68 | QUERY=$CHOICE |
| 69 | URL=$(printf '%s\n' "$ENGINES" | awk -F'\t' 'NR == 1 { print $2; exit }') |
| 70 | fi |
| 71 | [ -n "$QUERY" ] || exit 0 |
| 72 | |
| 73 | # A pasted or typed URL is a destination, not something to search for. |
| 74 | case "$QUERY" in |
| 75 | http://*|https://*) TARGET=$QUERY ;; |
| 76 | *) TARGET=$(printf "$URL" "$(urlencode "$QUERY")") ;; |
| 77 | esac |
| 78 | |
| 79 | exec "$BROWSER_CMD" ${BROWSER_ARGS:+$BROWSER_ARGS} "$TARGET" |