~kris/dots

srice

c776188f504eb2e42ec1d359bdfd97e513b02add — Kris Yotam 5 months ago 2dc7c2f
add bm bookmark manager and dsnip launcher
3 files changed, 227 insertions(+), 0 deletions(-)

A .local/bin/bm
A .local/bin/dsnip
A .local/bin/dsnip-seed
A .local/bin/bm => .local/bin/bm +100 -0
@@ 0,0 1,100 @@
#!/usr/bin/env bash
###############################################################################
# bm — Bookmark manager CLI
#
# Usage:
#   bm add <url> [name] [category] [tags]   — add a bookmark
#   bm rm  <url|name>                        — remove a bookmark
#   bm ls  [category]                        — list bookmarks
#   bm cats                                  — list categories
#   bm search <query>                        — search by name/url/tags
#   bm edit <url|name>                       — 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 <url> [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 <url|name>}"
        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 <query>}"
        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 <url|name>}"
        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 <add|rm|ls|cats|search|edit> [args]"
        exit 1
        ;;
esac

A .local/bin/dsnip => .local/bin/dsnip +68 -0
@@ 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 <krisyotam@pm.me>
# 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

A .local/bin/dsnip-seed => .local/bin/dsnip-seed +59 -0
@@ 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"