lib/browse.sh

5.9 KB · 243 lines · bash

1#!/usr/bin/env bash
2
3brt_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
11brt_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
67brt_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
78brt_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 "
99import json
100items = json.load(open('${cache_file}'))
101for 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 "
114import json, shlex, sys
115item = json.loads(sys.argv[1])
116print('title=' + shlex.quote(item.get('title', '')))
117print('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
152brt_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 "
220import json
221items = json.load(open('${cache_file}'))
222idx = ${page} * 5 + int('${rank}') - 1
223print(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