| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | brt_browse_link() { |
| 4 | local url="$1" |
| 5 | local text="$2" |
| 6 | url=${url//\\/\\\\} |
| 7 | url=${url//;/\\;} |
| 8 | printf '\033]8;;%s\033\\%s\033]8;;\033\\' "$url" "$text" |
| 9 | } |
| 10 | |
| 11 | brt_browse_read_key() { |
| 12 | local k k2 seq c |
| 13 | IFS= read -rsn1 k || return 1 |
| 14 | |
| 15 | if [[ $k != $'\x1b' ]]; then |
| 16 | case "$k" in |
| 17 | $'\r'|$'\n') printf 'enter' ;; |
| 18 | q|Q) printf 'quit' ;; |
| 19 | [1-5]) printf 'open:%s' "$k" ;; |
| 20 | *) printf 'other' ;; |
| 21 | esac |
| 22 | return 0 |
| 23 | fi |
| 24 | |
| 25 | if ! IFS= read -rsn1 -t 1 k2; then |
| 26 | printf 'esc' |
| 27 | return 0 |
| 28 | fi |
| 29 | |
| 30 | case "$k2" in |
| 31 | '[') |
| 32 | seq="" |
| 33 | local i |
| 34 | for (( i=0; i<3; i++ )); do |
| 35 | if IFS= read -rsn1 -t 1 c; then |
| 36 | seq+="$c" |
| 37 | [[ "$c" =~ [A-Za-z~] ]] && break |
| 38 | else |
| 39 | break |
| 40 | fi |
| 41 | done |
| 42 | case "$seq" in |
| 43 | D|'D'*) printf 'left' ;; |
| 44 | C|'C'*) printf 'right' ;; |
| 45 | A|'A'*) printf 'up' ;; |
| 46 | B|'B'*) printf 'down' ;; |
| 47 | *) printf 'esc' ;; |
| 48 | esac |
| 49 | ;; |
| 50 | O) |
| 51 | if ! IFS= read -rsn1 -t 1 c; then |
| 52 | printf 'esc' |
| 53 | return 0 |
| 54 | fi |
| 55 | case "$c" in |
| 56 | D) printf 'left' ;; |
| 57 | C) printf 'right' ;; |
| 58 | *) printf 'esc' ;; |
| 59 | esac |
| 60 | ;; |
| 61 | *) |
| 62 | printf 'esc' |
| 63 | ;; |
| 64 | esac |
| 65 | } |
| 66 | |
| 67 | brt_browse_open_url() { |
| 68 | local url="$1" |
| 69 | if command -v open &>/dev/null; then |
| 70 | open "$url" |
| 71 | elif command -v xdg-open &>/dev/null; then |
| 72 | xdg-open "$url" |
| 73 | else |
| 74 | brt_info "Open: ${url}" |
| 75 | fi |
| 76 | } |
| 77 | |
| 78 | brt_browse_draw() { |
| 79 | local query="$1" |
| 80 | local page="$2" |
| 81 | local cache_file="$3" |
| 82 | local total page_count start idx rank title url line item |
| 83 | local -a titles urls |
| 84 | |
| 85 | total=$(python3 -c "import json; print(len(json.load(open('${cache_file}'))))" 2>/dev/null || echo 0) |
| 86 | if (( total < 1 )); then |
| 87 | brt_error "No results to display" |
| 88 | return 1 |
| 89 | fi |
| 90 | |
| 91 | page_count=$(( (total + 4) / 5 )) |
| 92 | if (( page < 0 )); then page=0; fi |
| 93 | if (( page >= page_count )); then page=$(( page_count - 1 )); fi |
| 94 | |
| 95 | start=$(( page * 5 )) |
| 96 | local page_json |
| 97 | page_json=$( |
| 98 | python3 -c " |
| 99 | import json |
| 100 | items = json.load(open('${cache_file}')) |
| 101 | for item in items[${start}:${start}+5]: |
| 102 | print(json.dumps(item)) |
| 103 | " |
| 104 | ) |
| 105 | |
| 106 | printf '\033[H\033[2J\033[?25l' |
| 107 | printf '\033[1mbrt browse\033[0m — %s\n\n' "$query" |
| 108 | |
| 109 | titles=() |
| 110 | urls=() |
| 111 | while IFS= read -r item; do |
| 112 | [[ -z "$item" ]] && continue |
| 113 | eval "$(python3 -c " |
| 114 | import json, shlex, sys |
| 115 | item = json.loads(sys.argv[1]) |
| 116 | print('title=' + shlex.quote(item.get('title', ''))) |
| 117 | print('url=' + shlex.quote(item.get('url', ''))) |
| 118 | " "$item")" |
| 119 | titles+=("$title") |
| 120 | urls+=("$url") |
| 121 | done <<< "$page_json" |
| 122 | |
| 123 | for idx in "${!titles[@]}"; do |
| 124 | rank=$(( start + idx + 1 )) |
| 125 | title="${titles[$idx]}" |
| 126 | url="${urls[$idx]}" |
| 127 | [[ -z "$title" ]] && continue |
| 128 | printf ' \033[1m%d.\033[0m ' "$rank" |
| 129 | brt_browse_link "$url" "$title" |
| 130 | printf '\n \033[2m' |
| 131 | brt_browse_link "$url" "$url" |
| 132 | printf '\033[0m\n\n' |
| 133 | done |
| 134 | |
| 135 | line=$(( start + 1 )) |
| 136 | if (( start + 5 > total )); then |
| 137 | line_end=$total |
| 138 | else |
| 139 | line_end=$(( start + 5 )) |
| 140 | fi |
| 141 | |
| 142 | printf '\033[2m' |
| 143 | printf 'Showing %d-%d of %d' "$line" "$line_end" "$total" |
| 144 | if (( page_count > 1 )); then |
| 145 | printf ' · page %d/%d' "$(( page + 1 ))" "$page_count" |
| 146 | fi |
| 147 | printf '\n' |
| 148 | printf '← → change page · 1-5 open result · q quit\033[0m\n' |
| 149 | printf '\033[?25h' |
| 150 | } |
| 151 | |
| 152 | brt_browse() { |
| 153 | local query="${1:-}" |
| 154 | local cache_file page=0 key rank url total page_count |
| 155 | local old_stty="" |
| 156 | |
| 157 | if [[ -z "$query" ]]; then |
| 158 | brt_error 'Usage: brt -browse "search query"' |
| 159 | brt_info "Shows top search results with clickable links (Google when configured)" |
| 160 | brt_info "← → to paginate (1-5, 6-10, … up to 25) · 1-5 opens a result" |
| 161 | brt_info "Optional: brt -config (or BRT_GOOGLE_API_KEY + BRT_GOOGLE_CX)" |
| 162 | exit 1 |
| 163 | fi |
| 164 | |
| 165 | if ! command -v python3 &>/dev/null; then |
| 166 | brt_error "python3 is required for brt -browse" |
| 167 | exit 1 |
| 168 | fi |
| 169 | |
| 170 | if ! command -v curl &>/dev/null; then |
| 171 | brt_error "curl is required for brt -browse" |
| 172 | exit 1 |
| 173 | fi |
| 174 | |
| 175 | brt_info "Searching for: ${query}" |
| 176 | cache_file=$(mktemp "${TMPDIR:-/tmp}/brt-browse.XXXXXX") |
| 177 | err_file=$(mktemp "${TMPDIR:-/tmp}/brt-browse-err.XXXXXX") |
| 178 | if ! python3 "${BRT_LIB}/browse.py" search "$query" 25 >"$cache_file" 2>"$err_file"; then |
| 179 | [[ -s "$err_file" ]] && cat "$err_file" >&2 |
| 180 | rm -f "$cache_file" "$err_file" |
| 181 | exit 1 |
| 182 | fi |
| 183 | [[ -s "$err_file" ]] && cat "$err_file" >&2 |
| 184 | rm -f "$err_file" |
| 185 | |
| 186 | if [[ ! -s "$cache_file" ]] || ! python3 -c "import json; json.load(open('${cache_file}'))" 2>/dev/null; then |
| 187 | brt_error "Could not fetch Google search results" |
| 188 | rm -f "$cache_file" |
| 189 | exit 1 |
| 190 | fi |
| 191 | |
| 192 | if ! old_stty=$(stty -g 2>/dev/null); then |
| 193 | brt_browse_draw "$query" "$page" "$cache_file" |
| 194 | rm -f "$cache_file" |
| 195 | return 0 |
| 196 | fi |
| 197 | |
| 198 | stty -echo -icanon min 1 time 0 2>/dev/null || true |
| 199 | trap 'stty "$old_stty" 2>/dev/null; printf "\033[?25h"; rm -f "$cache_file"; trap - EXIT INT TERM' EXIT INT TERM |
| 200 | |
| 201 | while true; do |
| 202 | brt_browse_draw "$query" "$page" "$cache_file" || break |
| 203 | key=$(brt_browse_read_key || echo quit) |
| 204 | case "$key" in |
| 205 | left) |
| 206 | if (( page > 0 )); then |
| 207 | ((page--)) || true |
| 208 | fi |
| 209 | ;; |
| 210 | right) |
| 211 | total=$(python3 -c "import json; print(len(json.load(open('${cache_file}'))))") |
| 212 | page_count=$(( (total + 4) / 5 )) |
| 213 | if (( page + 1 < page_count )); then |
| 214 | ((page++)) || true |
| 215 | fi |
| 216 | ;; |
| 217 | open:*) |
| 218 | rank=${key#open:} |
| 219 | url=$(python3 -c " |
| 220 | import json |
| 221 | items = json.load(open('${cache_file}')) |
| 222 | idx = ${page} * 5 + int('${rank}') - 1 |
| 223 | print(items[idx]['url'] if 0 <= idx < len(items) else '') |
| 224 | ") |
| 225 | if [[ -n "$url" ]]; then |
| 226 | stty "$old_stty" 2>/dev/null || true |
| 227 | printf '\033[?25h\n' |
| 228 | brt_browse_open_url "$url" |
| 229 | stty -echo -icanon min 1 time 0 2>/dev/null || true |
| 230 | fi |
| 231 | ;; |
| 232 | quit|esc) |
| 233 | break |
| 234 | ;; |
| 235 | esac |
| 236 | done |
| 237 | |
| 238 | stty "$old_stty" 2>/dev/null || true |
| 239 | printf '\033[?25h\n' |
| 240 | rm -f "$cache_file" |
| 241 | trap - EXIT INT TERM |
| 242 | } |
| 243 | |