Created
August 26, 2026 02:37
-
-
Save eevmanu/24fabb26e5cbf1d14a92810da3e06bbd to your computer and use it in GitHub Desktop.
Interactive CLI Chrome Bookmarks fuzzy search and clipboard copy (jq + fzf + wl-copy)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================== | |
| # TOOL: Chrome Bookmarks Interactive Fuzzy Search & Clipboard Tool | |
| # ============================================================================== | |
| # | |
| # USAGE: | |
| # Run this command directly in your terminal, or bind it to a bash/zsh function. | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # PIPELINE ARCHITECTURE BREAKDOWN | |
| # ------------------------------------------------------------------------------ | |
| # 1. JQ (JSON Extraction & Table Rendering): | |
| # - Chrome's 'Bookmarks' file is plain JSON (NOT an SQLite database; commands | |
| # like 'sqlite3 .tables' will not work on it). | |
| # - Traversal: Uses the recursive descent operator '..' across all folders | |
| # inside '.roots' ('bookmark_bar', 'other', 'synced', and any nested sub-folders). | |
| # - Filter: 'select(.type == "url")' isolates leaf bookmarks from folders. | |
| # - Sorting: 'sort_by(.name // "" | ascii_downcase)' sorts A-Z case-insensitively. | |
| # - Sanitization: 'gsub("[\t\r\n]+"; " ")' replaces tabs/newlines in bookmark | |
| # titles with spaces to prevent breaking the Tab-Separated Values (TSV) stream. | |
| # - Fixed-Width Padding ('pad(45)'): | |
| # * Padded with spaces to 45 chars, or truncated with '…' if longer. | |
| # * Ensures the vertical divider '│' lines up in a straight table column. | |
| # - 3-Field TSV Output (Decoupling Search from Display): | |
| # * Field 1 (Hidden): Raw URL (for clean extraction on selection). | |
| # * Field 2 (Hidden): Raw full title (for 100% lossless search). | |
| # * Field 3 (Visible): ANSI-colored formatted table row: | |
| # [Bold Cyan Title (45c)] [Gray │ Divider] [Green URL] | |
| # | |
| # 2. FZF (Fuzzy Interactive Search): | |
| # - '--ansi': Interprets ANSI color escape codes cleanly. | |
| # - '--delimiter=\t': Splits fields by tabs. | |
| # - '--with-nth=3': Renders ONLY Field 3 (the clean aligned table) in the UI. | |
| # - '--nth=1,2': Targets BOTH raw URL (Field 1) and full Title (Field 2) for | |
| # full-text matching. Even if a title was truncated to 45 chars visually, | |
| # fzf still searches against 100% of the hidden full title and URL. | |
| # - '--layout=reverse': Places search input prompt at the TOP with top-to-bottom results. | |
| # | |
| # 3. CUT (Result Extraction): | |
| # - 'cut -f1': Extracts only Field 1 (the clean, raw URL) upon pressing <Enter>. | |
| # | |
| # 4. TEE & WL-COPY (Dual Output & Wayland Clipboard): | |
| # - 'tee /dev/stderr': Prints the chosen URL to the terminal screen. | |
| # - 'wl-copy': Native Wayland clipboard utility; copies the URL to clipboard. | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # CHAIN OF THOUGHT & HISTORICAL DESIGN DECISIONS | |
| # ------------------------------------------------------------------------------ | |
| # - Why not SQLite? | |
| # Unlike History/Cookies/Web Data, Chrome Bookmarks is purely JSON. | |
| # - Why recursive '..'? | |
| # Bookmarks can be nested arbitrarily deep inside folder hierarchies. | |
| # - Why not a simple 'name\turl' display? | |
| # Without color coding and fixed-width padding, long titles caused the title | |
| # and URL to merge into one continuous line, making visual scanning difficult. | |
| # - Why 3 decoupled TSV fields instead of 1 or 2? | |
| # Directly truncating text would break searchability for words past char 45. | |
| # Keeping raw title/URL in Fields 1 & 2 while displaying Field 3 gives the best | |
| # of both worlds: a clean table layout + 100% lossless search. | |
| # - Why 'wl-copy' over 'xclip'/'xsel'? | |
| # On Wayland sessions, 'wl-copy' is native and persistent, avoiding XWayland | |
| # emulation drops when the subshell closes. | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # KNOWN CAVEATS & TRADE-OFFS | |
| # ------------------------------------------------------------------------------ | |
| # - Cancellation on <Esc>: | |
| # Piping directly to 'wl-copy' without checking for empty strings will clear | |
| # the clipboard if you cancel fzf with <Esc> or <Ctrl+C>. | |
| # (To make it cancel-safe, switch to: '... | xargs -r -I{} sh -c "wl-copy -n \"{}\"; echo \"{}\""') | |
| # - Stderr Output: | |
| # 'tee /dev/stderr' writes to stderr. It displays on your screen, but cannot | |
| # be piped downstream (e.g., 'cb | xdg-open' will receive nothing on stdin). | |
| # - Trailing Newline: | |
| # Standard 'wl-copy' copies a trailing newline. Use 'wl-copy -n' if you want | |
| # to paste into address bars without auto-submitting. | |
| # | |
| # ------------------------------------------------------------------------------ | |
| # FUTURE ITERATION IDEAS | |
| # ------------------------------------------------------------------------------ | |
| # - Dynamic Profile Detection: Replace 'Profile 3' with a parameter or fzf selector. | |
| # - Bottom Preview Window: Add '--preview' and '--preview-window=down:3:wrap' to | |
| # read full titles of truncated rows on hover. | |
| # - Direct Browser Launcher: Append '&& google-chrome "$url"' to open on Enter. | |
| # ============================================================================== | |
| jq -r ' | |
| def sanitize: gsub("[\t\r\n]+"; " "); | |
| def pad(n): if (length > n) then (.[:n-1] + "…") else (. + (" " * (n - length))) end; | |
| [.roots | .. | objects | select(.type == "url")] | |
| | sort_by(.name // "" | ascii_downcase) | |
| | .[] | |
| | ((.name // "Untitled") | sanitize) as $name | |
| | "\(.url)\t\($name)\t\u001b[1;36m\($name | pad(45))\u001b[0m \u001b[38;5;240m│\u001b[0m \u001b[32m\(.url)\u001b[0m" | |
| ' "/home/user/.config/google-chrome/Profile X/Bookmarks" \ | |
| | fzf --ansi --delimiter='\t' --with-nth=3 --nth=1,2 --layout=reverse \ | |
| | cut -f1 \ | |
| | tee /dev/stderr \ | |
| | wl-copy |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment