#!/usr/bin/env bash thisFile=$(realpath "$0") DOTFILES="${DOTFILES:-$HOME/dotfiles}" BOOKMARKS_FILE="${DOTFILES}/secrets/wb.txt" # Ensure sops can find the config file from any directory export SOPS_CONFIG="${DOTFILES}/.sops.yaml" # Decrypt bookmarks from sops getBookmarks() { if [[ -f ${BOOKMARKS_FILE} ]]; then # Unencrypted file (for development/testing) cat "${BOOKMARKS_FILE}" elif [[ -f "${BOOKMARKS_FILE%.txt}.enc" ]]; then # Encrypted file sops --decrypt --input-type binary "${BOOKMARKS_FILE%.txt}.enc" 2>/dev/null else echo "Error: No bookmarks file found" >&2 exit 1 fi } # Defaults isCopy=0 editMode=0 q="" noFuzzy=0 queryMode=0 if ! type fzf >/dev/null 2>&1; then noFuzzy=1 fi cleanup() { rm -f /tmp/.wb clear-pbcopy 60 & } trap cleanup EXIT bks() { getBookmarks | awk '!/^[[:space:]]*#/ && !/^[[:space:]]*$/ {print $1}' } execute() { # -------------------------------------------------------------------- # If key == $q, then it's a match, then we do the following: # a. Copy the "url" to the clipboard (with "pbcopy" in Mac or # "clip" in Linux). How do we know it's a Mac, we just simply # check if there's a ~/Music folder in your homedir. If # there is, it's likely a Mac, else it's Linux. # b. I have a "clear-pbcopy" script that basically clears the # clipboard after 20 seconds (for security reason) because # you don't want to accidentally copy-paste your personal # links in your email or your chat applications. It is as simple as # a oneline command: # "sleep 20; echo "nothing-here" | pbcopy" # c. If the first argument is "-c" then we mean we just want to # copy the string to the clipboard, so we just continue # d. Then we see if the "url" contains "http" or "www", if so # we open it with ~/bin/browser. You must have your own # "browser" script that opens your favorite web browser, e.g. # in Mac, my "~/bin/browser" contains: # open -a /Applications/Firefox.app $* # However, if the "url" does not contain http/www, # e.g. the key and url line is like this: # mytodofile ~/Documents/mytodofile.txt # then I open it "open" command line provided by Mac # d. We are done (exiting) # -------------------------------------------------------------------- echo "" # ---------------------- a echo " copying $url" echo "$url" | cb # ---------------------- b if type clear-pbcopy >/dev/null 2>&1; then clear-pbcopy 60 & fi # ---------------------- c if [ $isCopy == 1 ]; then echo " Copied to clipboard" echo "" exit fi # ---------------------- d echo " open $url" x-open $url # ---------------------- d echo "" exit } fuzzy() { # Use awk to parse bookmarks in a single pass (much faster than bash loop) formatted=$(getBookmarks | awk ' # Skip decorative headers and section dividers /^#[-=~# ]*$/ || /^# *--/ || /^##+/ { next } # Accumulate description and extract tags from comments /^#[^#]/ { sub(/^# */, "") desc = desc (desc ? " " : "") $0 # Extract @tags n = split($0, words, " ") for (i = 1; i <= n; i++) { if (words[i] ~ /^@/) { tags = tags (tags ? " " : "") words[i] } } next } # Handle bookmark lines (non-comment, non-empty) /^[^#[:space:]]/ { key = $1 # Skip @-prefixed keys if (key ~ /^@/) { desc = ""; tags = ""; next } # Get URL (everything after first field) url = $0 sub(/^[^ \t]+[ \t]+/, "", url) # Clean description gsub(/\n/, " ", desc) if (desc == "") desc = "-" if (tags == "") tags = "-" print key "\t" url "\t" desc "\t" tags desc = "" tags = "" } ') selected_line=$(echo "$formatted" | fzf -i --with-nth=1 \ --delimiter=$'\t' \ --query="$q" \ --prompt="🔍 Select bookmark: " \ --preview='bash -c "url=\$(echo {} | cut -f2); desc=\$(echo {} | cut -f3); tags=\$(echo {} | cut -f4); echo -e \"\$url\n\nDesc: \$desc\n\nTags: \$tags\""' \ --preview-window=down:12:wrap) if [[ -n $selected_line ]]; then IFS=$'\t' read -r key url _ <<<"$selected_line" execute "$url" else echo " No selection made" echo "" fi } help() { echo "" echo "Usage: wb [-c] [-e] [key2] [key3] ..." echo "" echo "Options:" echo " -c Copy to clipboard without opening" echo " -e Edit the bookmarks file" echo " -nf No Fuzzy Match" echo " -h Show help" echo "" echo "Examples:" echo " wb cnn # Open the cnn bookmark" echo " wb -c gmail # Copy Gmail URL without opening" echo " wb 230 zoom # Open bookmark matching '230.*zoom'" echo " wb ai sci zoom # Open bookmark matching 'ai.*sci.*zoom'" echo "" } # -------------------------------------------------------------------- # [ 70-char wide ] # 34567890123456789012345678901234567890123456789012345678901234567890 # 1 2 3 4 5 6 7 # -------------------------------------------------------------------- # -------------------------------------------------------------------- # 0. Setup $thisFile and # if "wb" is called with no arguments, then # just open this "wb" file with "e" --> emacs # You can change "e" to "vi" or other text editor # -------------------------------------------------------------------- # Parse arguments args=() while [ $# -gt 0 ]; do case "$1" in -c) isCopy=1 ;; -e) editMode=1 ;; -h | --help) help exit 0 ;; -nf) noFuzzy=1 ;; -q) queryMode=1 ;; -*) echo "Unknown option: $1" exit 1 ;; *) args+=("$1") ;; esac shift done # Build pattern from multiple arguments (e.g., "wb 230 si te" -> "230.*si.*te") if [ ${#args[@]} -gt 0 ]; then q="${args[0]}" for ((i = 1; i < ${#args[@]}; i++)); do q="$q.*${args[$i]}" done fi if [[ $editMode == 1 ]]; then encFile="${BOOKMARKS_FILE%.txt}.enc" if [[ -f $encFile ]]; then echo "Editing encrypted bookmarks file" # Decrypt to the .txt file (matches .sops.yaml path_regex), edit, re-encrypt trap "rm -f '${BOOKMARKS_FILE}'" EXIT sops --decrypt --input-type binary --output-type binary "$encFile" >"${BOOKMARKS_FILE}" ${EDITOR:-vim} "${BOOKMARKS_FILE}" sops --encrypt --input-type binary --output-type binary "${BOOKMARKS_FILE}" >"$encFile" rm -f "${BOOKMARKS_FILE}" echo "Saved and re-encrypted" elif [[ -f ${BOOKMARKS_FILE} ]]; then echo "Editing ${BOOKMARKS_FILE}" ${EDITOR:-vim} "${BOOKMARKS_FILE}" else echo "Error: No bookmarks file found" exit 1 fi exit fi if [[ $queryMode == 1 ]]; then if [[ -z $q ]]; then bs=$(bks) else bs=$(bks | grep -i "$q") fi num=$(echo "$bs" | wc -l | tr -d ' ') bs=$(echo "$bs" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g') echo "$num bookmarks: ($bs)" exit fi if [[ $q == "" ]]; then if [[ $noFuzzy == 1 ]]; then help else fuzzy fi exit fi ## -------------------------------------------------------------------- ## 1. Find all bookmarks matching the pattern using regex ## Store matches in /tmp/.wb (using awk for speed) ## -------------------------------------------------------------------- tempFile=/tmp/.wb getBookmarks | awk -v pattern="$q" ' !/^[[:space:]]*#/ && !/^[[:space:]]*$/ && $1 ~ pattern {print} ' >"$tempFile" hitCount=$(wc -l <"$tempFile" | tr -d ' ') ## -------------------------------------------------------------------- ## 2. Handle results based on hit count ## -------------------------------------------------------------------- # No hits found if [ $hitCount -eq 0 ]; then echo "" if [[ $noFuzzy == 1 ]]; then echo " '$q' not found" echo " Available bookmarks matching pattern:" bks | grep -iE "$q" | cat -n else fuzzy fi echo "" exit fi # Multiple hits - check for exact match first if [ $hitCount -gt 1 ]; then exactHit=0 while read -r key url misc; do if [[ $key == "$q" ]]; then exactHit=1 echo "$key $url" >"$tempFile" break fi done <"$tempFile" if [ $exactHit -eq 1 ]; then hitCount=1 fi fi # Still multiple hits - show options or use fuzzy if [ $hitCount -gt 1 ]; then if [[ $noFuzzy == 1 ]]; then echo "" echo " Multiple matches for '$q':" sort "$tempFile" | while read -r key url misc; do echo " $key" done echo "" else fuzzy fi exit fi # Single hit - execute if [ $hitCount -eq 1 ]; then read -r key url _ <"$tempFile" execute "$url" fi ## -------------------------------------------------------------------- ## 5. Bookmarks are now stored in secrets/wb.txt (or secrets/wb.enc when encrypted) ## To encrypt: sops --encrypt --input-type binary --output-type binary secrets/wb.txt > secrets/wb.enc ## Then remove secrets/wb.txt ## --------------------------------------------------------------------