From c776188f504eb2e42ec1d359bdfd97e513b02add Mon Sep 17 00:00:00 2001 From: Kris Yotam <75515498+krisyotam@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:04:36 -0500 Subject: [PATCH] add bm bookmark manager and dsnip launcher --- .local/bin/bm | 100 ++++++++++++++++++++++++++++++++++++++++++ .local/bin/dsnip | 68 ++++++++++++++++++++++++++++ .local/bin/dsnip-seed | 59 +++++++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100755 .local/bin/bm create mode 100755 .local/bin/dsnip create mode 100755 .local/bin/dsnip-seed diff --git a/.local/bin/bm b/.local/bin/bm new file mode 100755 index 0000000000000000000000000000000000000000..0d4b3e17b5607ff4b134e6b619379993bea526b3 --- /dev/null +++ b/.local/bin/bm @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +############################################################################### +# bm — Bookmark manager CLI +# +# Usage: +# bm add [name] [category] [tags] — add a bookmark +# bm rm — remove a bookmark +# bm ls [category] — list bookmarks +# bm cats — list categories +# bm search — search by name/url/tags +# bm edit — edit a bookmark in $EDITOR +############################################################################### + +DB="${XDG_DATA_HOME:-$HOME/.local/share}/karbs/bookmarks.db" + +sq() { printf '%s' "${1//\'/\'\'}"; } + +cmd="${1:-ls}" +shift + +case "$cmd" in + add) + url="${1:?usage: bm add [name] [category] [tags]}" + name="${2:-$(printf '%s' "$url" | sed 's|https\?://||; s|^www\.||; s|/$||; s|/.*||')}" + category="${3:-Uncategorized}" + tags="${4:-}" + safe_url=$(sq "$url"); safe_name=$(sq "$name") + safe_cat=$(sq "$category"); safe_tags=$(sq "$tags") + sqlite3 "$DB" "INSERT OR REPLACE INTO bookmarks (name,url,category,tags) + VALUES ('${safe_name}','${safe_url}','${safe_cat}','${safe_tags}');" + echo "Added: [${category}] ${name}" + ;; + + rm) + term="${1:?usage: bm rm }" + safe=$(sq "$term") + sqlite3 "$DB" "DELETE FROM bookmarks WHERE url='${safe}' OR name='${safe}';" + echo "Removed: ${term}" + ;; + + ls) + filter="${1:-}" + if [ -n "$filter" ]; then + safe=$(sq "$filter") + sqlite3 "$DB" -column -header \ + "SELECT id,category,name,url,tags FROM bookmarks WHERE category='${safe}' ORDER BY name;" + else + sqlite3 "$DB" -column -header \ + "SELECT id,category,name,url,tags FROM bookmarks ORDER BY category,name;" + fi + ;; + + cats) + sqlite3 "$DB" "SELECT category, COUNT(*) as count FROM bookmarks GROUP BY category ORDER BY category;" + ;; + + search) + query="${1:?usage: bm search }" + safe=$(sq "$query") + sqlite3 "$DB" -column -header " + SELECT category, name, url, tags FROM bookmarks + WHERE name LIKE '%${safe}%' + OR url LIKE '%${safe}%' + OR tags LIKE '%${safe}%' + OR category LIKE '%${safe}%' + ORDER BY category, name;" + ;; + + edit) + term="${1:?usage: bm edit }" + safe=$(sq "$term") + row=$(sqlite3 "$DB" -separator $'\t' \ + "SELECT id,name,url,category,tags,description FROM bookmarks WHERE url='${safe}' OR name='${safe}' LIMIT 1;") + [ -z "$row" ] && echo "Not found: $term" && exit 1 + id=$(cut -f1 <<< "$row") + tmp=$(mktemp /tmp/bm-edit.XXXXXX) + printf '%s' "$row" | awk -F'\t' '{ + print "name: " $2 + print "url: " $3 + print "category: " $4 + print "tags: " $5 + print "description: " $6 + }' > "$tmp" + ${EDITOR:-nvim} "$tmp" + name=$(grep '^name:' "$tmp" | sed 's/^name: *//') + url=$(grep '^url:' "$tmp" | sed 's/^url: *//') + cat=$(grep '^category:' "$tmp" | sed 's/^category: *//') + tags=$(grep '^tags:' "$tmp" | sed 's/^tags: *//') + desc=$(grep '^description:' "$tmp" | sed 's/^description: *//') + rm -f "$tmp" + sn=$(sq "$name"); su=$(sq "$url"); sc=$(sq "$cat"); st=$(sq "$tags"); sd=$(sq "$desc") + sqlite3 "$DB" "UPDATE bookmarks SET name='${sn}',url='${su}',category='${sc}',tags='${st}',description='${sd}' WHERE id=${id};" + echo "Updated: [${cat}] ${name}" + ;; + + *) + echo "Usage: bm [args]" + exit 1 + ;; +esac diff --git a/.local/bin/dsnip b/.local/bin/dsnip new file mode 100755 index 0000000000000000000000000000000000000000..ea9da9d0a41d74eeb104ff707062232bc8db27e3 --- /dev/null +++ b/.local/bin/dsnip @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +############################################################################### +# dsnip — Bookmark launcher: category browse + full-text search via dmenu +# +# DB: ~/.local/share/karbs/bookmarks.db +# Usage: +# dsnip — open launcher +# dsnip-seed — import legacy snippets file (run once) +# bm add — add a bookmark from the terminal +# +# Maintainer: Kris Yotam +# License: MIT +# Created: 2026-04-05 +############################################################################### + +DB="${XDG_DATA_HOME:-$HOME/.local/share}/karbs/bookmarks.db" +CAT_ICON="󰉋 " +DM="dmenu -i -l 25 -c -bw 2" + +sqlite3 "$DB" " + CREATE TABLE IF NOT EXISTS bookmarks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + category TEXT NOT NULL DEFAULT 'Uncategorized', + tags TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '' + ); +" 2>/dev/null + +# Escape single quotes for inline SQL +sq() { printf '%s' "${1//\'/\'\'}"; } + +build_list() { + sqlite3 "$DB" "SELECT DISTINCT '${CAT_ICON}' || category FROM bookmarks ORDER BY category;" + printf '%.0s─' {1..40}; echo + sqlite3 "$DB" " + SELECT '[' || category || '] ' || name || + CASE WHEN tags != '' THEN ' # ' || tags ELSE '' END + FROM bookmarks ORDER BY category, name;" +} + +chosen=$(build_list | $DM -p " Bookmarks") +[ -z "$chosen" ] && exit 0 +[[ "$chosen" == ─* ]] && exit 0 + +if [[ "$chosen" == "${CAT_ICON}"* ]]; then + cat="${chosen#${CAT_ICON}}" + safe_cat=$(sq "$cat") + chosen2=$(sqlite3 "$DB" " + SELECT name || CASE WHEN tags != '' THEN ' # ' || tags ELSE '' END + FROM bookmarks WHERE category = '${safe_cat}' ORDER BY name;" \ + | $DM -p " ${cat}") + [ -z "$chosen2" ] && exit 0 + name=$(printf '%s' "$chosen2" | sed 's/ # .*//') + safe_name=$(sq "$name") + url=$(sqlite3 "$DB" "SELECT url FROM bookmarks WHERE category = '${safe_cat}' AND name = '${safe_name}' LIMIT 1;") +else + name_part=$(printf '%s' "$chosen" | sed 's/ # .*//') + cat=$(printf '%s' "$name_part" | sed 's/^\[\([^]]*\)\].*/\1/') + name=$(printf '%s' "$name_part" | sed 's/^\[[^]]*\] //') + safe_cat=$(sq "$cat") + safe_name=$(sq "$name") + url=$(sqlite3 "$DB" "SELECT url FROM bookmarks WHERE category = '${safe_cat}' AND name = '${safe_name}' LIMIT 1;") +fi + +[ -z "$url" ] && exit 0 +xdg-open "$url" &>/dev/null & disown diff --git a/.local/bin/dsnip-seed b/.local/bin/dsnip-seed new file mode 100755 index 0000000000000000000000000000000000000000..139ac359e16dbcae263b0cb2790fc76947c7a396 --- /dev/null +++ b/.local/bin/dsnip-seed @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +############################################################################### +# dsnip-seed — Import legacy karbs/snippets file into bookmarks.db +# +# Run once after creating the db. Safe to re-run (UNIQUE on url skips dupes). +############################################################################### + +DB="${XDG_DATA_HOME:-$HOME/.local/share}/karbs/bookmarks.db" +SNIPPETS="${XDG_DATA_HOME:-$HOME/.local/share}/karbs/snippets" + +[ -f "$SNIPPETS" ] || { echo "snippets file not found: $SNIPPETS"; exit 1; } + +category="Uncategorized" +inserted=0 +skipped=0 + +while IFS= read -r line; do + # Strip trailing whitespace + line=$(printf '%s' "$line" | sed 's/[[:space:]]*$//') + + # Skip blank lines + [ -z "$line" ] && continue + + # Category header (must check before stripping inline comments) + if [[ "$line" == \#* ]] && [[ "$line" != http* ]]; then + category=$(printf '%s' "$line" | sed 's/^# *//') + continue + fi + + # Strip inline comments (e.g. "https://... # Profile") + line=$(printf '%s' "$line" | sed 's/ *#[^/].*//') + + # Skip non-URL lines + [[ "$line" != http* ]] && continue + + url=$(printf '%s' "$line" | sed 's/[[:space:]]*$//') + # Derive a display name: strip scheme + www, truncate at 60 chars + name=$(printf '%s' "$url" | sed 's|https\?://||; s|^www\.||; s|/$||') + name="${name:0:60}" + + # Escape single quotes + safe_url="${url//\'/\'\'}" + safe_name="${name//\'/\'\'}" + safe_cat="${category//\'/\'\'}" + + result=$(sqlite3 "$DB" " + INSERT OR IGNORE INTO bookmarks (name, url, category) + VALUES ('${safe_name}', '${safe_url}', '${safe_cat}'); + SELECT changes(); + ") + + if [ "$result" -eq 1 ]; then + ((inserted++)) + else + ((skipped++)) + fi +done < "$SNIPPETS" + +echo "Done. Inserted: $inserted Skipped (already exist): $skipped"