~kris/dots

srice

15fdcf78915608f4681dbcd34bfaa239c0d0dcea — Kris Yotam 6 months ago 3484c9c
add 10 scripts, remove remapd/remaps

New: dllm, dlog, email, lfub, sb-systemstats, shred, sort, trash, vroid, ytcomments
Removed: remapd, remaps (banned — caused Xorg CPU spikes)
12 files changed, 1008 insertions(+), 19 deletions(-)

A .local/bin/dllm
A .local/bin/dlog
A .local/bin/email
A .local/bin/lfub
D .local/bin/remapd
D .local/bin/remaps
A .local/bin/sb-systemstats
A .local/bin/shred
A .local/bin/sort
A .local/bin/trash
A .local/bin/vroid
A .local/bin/ytcomments
A .local/bin/dllm => .local/bin/dllm +55 -0
@@ 0,0 1,55 @@
#!/usr/bin/env bash
###############################################################################
# dllm — LLM launcher via dmenu
#
# Maintainer:   Kris Yotam <krisyotam@pm.me>
# License:      MIT
# Created:      2026-02-18
###############################################################################

TERM_CMD="st"

# Top-level menu
tier1=$(printf '%s\n' \
    "  Cloud" \
    "  Local" \
| dmenu -i -l 2 -p "  LLM")

[ -z "$tier1" ] && exit 0

case "$tier1" in
    *Cloud*)
        choice=$(printf '%s\n' \
            "  Claude Code" \
            "󰊭  Gemini" \
        | dmenu -i -l 2 -p "  Cloud")

        [ -z "$choice" ] && exit 0

        case "$choice" in
            *Claude*) setsid -f $TERM_CMD -e claude &>/dev/null ;;
            *Gemini*) setsid -f $TERM_CMD -e gemini &>/dev/null ;;
        esac
        ;;
    *Local*)
        # Pull installed models from ollama
        models=$(ollama list 2>/dev/null | tail -n +2 | awk '{print $1}')

        if [ -z "$models" ]; then
            notify-send "dllm" "No local models installed. Run: ollama pull <model>"
            exit 1
        fi

        # Prefix each model with nerd font icon
        menu=$(echo "$models" | while read -r m; do echo "  $m"; done)

        choice=$(echo "$menu" | dmenu -i -l 20 -p "  Local")

        [ -z "$choice" ] && exit 0

        # Strip icon prefix to get model name
        model=$(echo "$choice" | sed 's/^.  //')

        setsid -f $TERM_CMD -e ollama run "$model" &>/dev/null
        ;;
esac

A .local/bin/dlog => .local/bin/dlog +286 -0
@@ 0,0 1,286 @@
#!/usr/bin/env bash
###############################################################################
# dlog — Reading log & completed content manager via dmenu
#
# Writes to krisyotam.com media.db (SQLite)
# Tables: reading_log, reading_books, reading_audiobooks, reading_blogs,
#         reading_essays, reading_papers, reading_verse, short_stories
#
# Maintainer:   Kris Yotam <krisyotam@pm.me>
# License:      MIT
# Created:      2026-02-18
###############################################################################

DB="$HOME/dev/krisyotam.com/public/data/media.db"

# ── Helpers ────────────────────────────────────────────────────

ask()    { printf '' | dmenu -p "$1"; }
pick()   { dmenu -i -l "${2:-10}" -p "$1"; }
esc()    { printf '%s' "$1" | sed "s/'/''/g"; }
today()  { date +%Y-%m-%d; }

# Duration: >99m displays as Xh Ym
fmt_dur() {
    local m="$1"
    if [ "$m" -gt 99 ] 2>/dev/null; then
        printf '%dh %dm' "$((m / 60))" "$((m % 60))"
    else
        printf '%dm' "$m"
    fi
}

sql() { sqlite3 "$DB" "$1"; }

# ── Log Entry (reading_log) ───────────────────────────────────

log_entry() {
    local dt title author type minutes

    dt=$(echo "$(today)" | pick "󰃭  Date:")
    [ -z "$dt" ] && return

    title=$(ask "󰗴  Title:")
    [ -z "$title" ] && return

    author=$(ask "󰏪  Author:")
    [ -z "$author" ] && return

    type=$(printf '%s\n' \
        "󰂽  Book" \
        "󰋋  Audiobook" \
        "󰖟  Blog Post" \
        "󰏫  Short Story" \
        "󰎈  Verse" \
        "󰧮  Essay" \
        "󰈙  Paper" \
    | pick "󰏗  Type:" 7)
    [ -z "$type" ] && return
    type=$(echo "$type" | sed 's/^[^ ]* *//')

    # Offer "+ Audiobook" if primary type isn't already Audiobook
    if [ "$type" != "Audiobook" ]; then
        local also
        also=$(printf '%s\n' "󰜺  No" "󰋋  + Audiobook" \
            | pick "󰋋  Also listening?" 2)
        [[ -n "$also" && "$also" == *Audiobook* ]] && type="$type + Audiobook"
    fi

    minutes=$(printf '%s\n' 5 10 15 20 30 45 60 90 120 180 \
        | pick "󱑂  Minutes:" 10)
    [ -z "$minutes" ] && return

    local dur
    dur=$(fmt_dur "$minutes")

    local ok
    ok=$(printf '%s\n' "󰄬  Confirm" "󰜺  Cancel" \
        | pick "$title · $author · $type · $dur" 2)
    [[ -z "$ok" || "$ok" == *Cancel* ]] && return

    sql "INSERT INTO reading_log (date, title, author, type, minutes)
         VALUES ('$(esc "$dt")', '$(esc "$title")', '$(esc "$author")',
                 '$(esc "$type")', $minutes);"

    notify-send "dlog" "󰎚  Logged: $title ($dur)"
}

# ── Completed: Books ──────────────────────────────────────────

add_book() {
    local title subtitle author cover link

    title=$(ask "󰂽  Title:")
    [ -z "$title" ] && return
    subtitle=$(ask "󰂽  Subtitle:")
    author=$(ask "󰏪  Author:")
    [ -z "$author" ] && return
    cover=$(ask "󰋩  Cover URL:")
    link=$(ask "󰌹  Link:")

    sql "INSERT INTO reading_books (title, subtitle, author, cover, link)
         VALUES ('$(esc "$title")', '$(esc "$subtitle")', '$(esc "$author")',
                 '$(esc "$cover")', '$(esc "$link")');"

    notify-send "dlog" "󰂽  Added: $title"
}

# ── Completed: Audiobooks ─────────────────────────────────────

add_audiobook() {
    local title subtitle author cover link

    title=$(ask "󰋋  Title:")
    [ -z "$title" ] && return
    subtitle=$(ask "󰋋  Subtitle:")
    author=$(ask "󰏪  Author:")
    [ -z "$author" ] && return
    cover=$(ask "󰋩  Cover URL:")
    link=$(ask "󰌹  Link:")

    sql "INSERT INTO reading_audiobooks (title, subtitle, author, cover, link)
         VALUES ('$(esc "$title")', '$(esc "$subtitle")', '$(esc "$author")',
                 '$(esc "$cover")', '$(esc "$link")');"

    notify-send "dlog" "󰋋  Added: $title"
}

# ── Completed: Blog Posts ─────────────────────────────────────

add_blog() {
    local title author src arc year

    title=$(ask "󰖟  Title:")
    [ -z "$title" ] && return
    author=$(ask "󰏪  Author:")
    src=$(ask "󰌹  Source URL:")
    arc=$(ask "󰀼  Archive URL:")
    year=$(ask "󰃭  Year:")

    sql "INSERT INTO reading_blogs (title, author, source_link, archive_link, publication_year)
         VALUES ('$(esc "$title")', '$(esc "$author")', '$(esc "$src")',
                 '$(esc "$arc")', ${year:-NULL});"

    notify-send "dlog" "󰖟  Added: $title"
}

# ── Completed: Short Stories ──────────────────────────────────

add_short_story() {
    local title author year

    title=$(ask "󰏫  Title:")
    [ -z "$title" ] && return
    author=$(ask "󰏪  Author:")
    [ -z "$author" ] && return
    year=$(ask "󰃭  Year:")

    sql "INSERT INTO short_stories (title, author, publication_year)
         VALUES ('$(esc "$title")', '$(esc "$author")', ${year:-NULL});"

    notify-send "dlog" "󰏫  Added: $title"
}

# ── Completed: Verse ──────────────────────────────────────────

add_verse() {
    local title author vtype src year

    title=$(ask "󰎈  Title:")
    [ -z "$title" ] && return
    author=$(ask "󰏪  Author:")
    [ -z "$author" ] && return

    vtype=$(printf '%s\n' \
        "lyric poem" "narrative poem" "ballad" "sonnet" "epigram" \
        "haiku" "ode" "elegy" "free verse" "epic" "limerick" "villanelle" \
    | pick "󰎈  Form:" 12)

    src=$(ask "󰌹  Source URL:")
    year=$(ask "󰃭  Year:")

    sql "INSERT INTO reading_verse (title, author, verse_type, source_link, publication_year)
         VALUES ('$(esc "$title")', '$(esc "$author")', '$(esc "$vtype")',
                 '$(esc "$src")', ${year:-NULL});"

    notify-send "dlog" "󰎈  Added: $title"
}

# ── Completed: Essays ─────────────────────────────────────────

add_essay() {
    local title author src arc year

    title=$(ask "󰧮  Title:")
    [ -z "$title" ] && return
    author=$(ask "󰏪  Author:")
    [ -z "$author" ] && return
    src=$(ask "󰌹  Source URL:")
    arc=$(ask "󰀼  Archive URL:")
    year=$(ask "󰃭  Year:")

    sql "INSERT INTO reading_essays (title, author, source_link, archive_link, publication_year)
         VALUES ('$(esc "$title")', '$(esc "$author")', '$(esc "$src")',
                 '$(esc "$arc")', ${year:-NULL});"

    notify-send "dlog" "󰧮  Added: $title"
}

# ── Completed: Papers ─────────────────────────────────────────

add_paper() {
    local title src arc year
    local authors=()

    title=$(ask "󰈙  Title:")
    [ -z "$title" ] && return

    # Multi-author loop
    while true; do
        local a
        a=$(printf '%s\n' "󰄬  Done adding authors" \
            | pick "󰏪  Author $((${#authors[@]} + 1)):" 1)
        [[ -z "$a" || "$a" == *Done* ]] && break
        authors+=("$a")
    done
    [ ${#authors[@]} -eq 0 ] && return

    # Build JSON array for author field
    local json='['
    for i in "${!authors[@]}"; do
        [ "$i" -gt 0 ] && json+=','
        json+="\"$(esc "${authors[$i]}")\""
    done
    json+=']'

    src=$(ask "󰌹  Source URL:")
    arc=$(ask "󰀼  Archive URL:")
    year=$(ask "󰃭  Year:")

    sql "INSERT INTO reading_papers (title, author, source_link, archive_link, publication_year)
         VALUES ('$(esc "$title")', '$(esc "$json")', '$(esc "$src")',
                 '$(esc "$arc")', ${year:-NULL});"

    notify-send "dlog" "󰈙  Added: $title (${#authors[@]} authors)"
}

# ── Completed Submenu ─────────────────────────────────────────

completed_menu() {
    local choice
    choice=$(printf '%s\n' \
        "󰂽  Books" \
        "󰋋  Audiobooks" \
        "󰖟  Blog Posts" \
        "󰏫  Short Stories" \
        "󰎈  Verse" \
        "󰧮  Essays" \
        "󰈙  Papers" \
    | pick "󰄲  Completed" 7)

    [ -z "$choice" ] && return

    case "$choice" in
        *Books*)      add_book ;;
        *Audiobooks*) add_audiobook ;;
        *Blog*)       add_blog ;;
        *Short*)      add_short_story ;;
        *Verse*)      add_verse ;;
        *Essays*)     add_essay ;;
        *Papers*)     add_paper ;;
    esac
}

# ── Main ──────────────────────────────────────────────────────

main=$(printf '%s\n' \
    "󰎚  Log" \
    "󰄲  Completed" \
| pick "󰎚  dlog" 2)

[ -z "$main" ] && exit 0

case "$main" in
    *Log*)       log_entry ;;
    *Completed*) completed_menu ;;
esac

A .local/bin/email => .local/bin/email +78 -0
@@ 0,0 1,78 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail

# email — send emails with optional attachments via msmtp
# usage: email [-a account] [-s subject] [-f file]... <recipient> [body]
# examples:
#   email kris@example.com "hello"
#   email -s "Report" -f data.csv kris@example.com
#   echo "body" | email -s "Subject" -f a.csv -f b.csv kris@example.com

die() { printf '%s: %s\n' "${0##*/}" "$*" >&2; exit 1; }

account="gmail"
subject="(no subject)"
files=()
boundary="----=_boundary_$(date +%s)_$$"

while [[ $# -gt 0 ]]; do
	case "${1}" in
		-a) account="${2}"; shift ;;
		-s) subject="${2}"; shift ;;
		-f) files+=("${2}"); shift ;;
		-h) printf 'usage: email [-a account] [-s subject] [-f file]... <recipient> [body]\n'; exit 0 ;;
		-*) die "unknown option: ${1}" ;;
		*)  break ;;
	esac
	shift
done

[[ $# -lt 1 ]] && die "no recipient specified"
to="${1}"
shift

# body from arg, stdin, or empty
if [[ $# -gt 0 ]]; then
	body="$*"
elif [[ ! -t 0 ]]; then
	body="$(cat)"
else
	body=""
fi

# build and send
{
	printf 'To: %s\n' "${to}"
	printf 'Subject: %s\n' "${subject}"
	printf 'MIME-Version: 1.0\n'

	if [[ ${#files[@]} -eq 0 ]]; then
		printf 'Content-Type: text/plain; charset=utf-8\n\n'
		printf '%s\n' "${body}"
	else
		printf 'Content-Type: multipart/mixed; boundary="%s"\n\n' "${boundary}"

		# body part
		printf -- '--%s\n' "${boundary}"
		printf 'Content-Type: text/plain; charset=utf-8\n\n'
		printf '%s\n\n' "${body}"

		# attachments
		for f in "${files[@]}"; do
			[[ -f "${f}" ]] || die "file not found: ${f}"
			fname="${f##*/}"
			printf -- '--%s\n' "${boundary}"
			printf 'Content-Type: application/octet-stream; name="%s"\n' "${fname}"
			printf 'Content-Disposition: attachment; filename="%s"\n' "${fname}"
			printf 'Content-Transfer-Encoding: base64\n\n'
			base64 "${f}"
			printf '\n'
		done

		printf -- '--%s--\n' "${boundary}"
	fi
} | msmtp -a "${account}" "${to}"

printf 'sent to %s via %s\n' "${to}" "${account}"

A .local/bin/lfub => .local/bin/lfub +24 -0
@@ 0,0 1,24 @@
#!/bin/sh

# This is a wrapper script for lf that allows it to create image previews with
# ueberzug. This works in concert with the lf configuration file and the
# lf-cleaner script.

set -e

cleanup() {
    exec 3>&-
	rm "$FIFO_UEBERZUG"
}

if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then
	lf "$@"
else
	[ ! -d "$HOME/.cache/lf" ] && mkdir -p "$HOME/.cache/lf"
	export FIFO_UEBERZUG="$HOME/.cache/lf/ueberzug-$$"
	mkfifo "$FIFO_UEBERZUG"
	ueberzug layer -s <"$FIFO_UEBERZUG" -p json &
	exec 3>"$FIFO_UEBERZUG"
	trap cleanup HUP INT QUIT TERM PWR EXIT
	lf "$@" 3>&-
fi

D .local/bin/remapd => .local/bin/remapd +0 -8
@@ 1,8 0,0 @@
#!/bin/bash

# Rerun the remaps script whenever a new input device is added.

while :; do
	remaps
	grep -qP -m1 '[^un]bind.+\/[^:]+\(usb\)' <(udevadm monitor -u -t seat -s input -s usb)
done

D .local/bin/remaps => .local/bin/remaps +0 -11
@@ 1,11 0,0 @@
#!/bin/sh

# This script is called on startup to remap keys.
# Decrease key repeat delay to 300ms and increase key repeat rate to 50 per second.
xset r rate 300 50
# Map the caps lock key to super, and map the menu key to right super.
setxkbmap -option caps:super,altwin:menu_win
# When caps lock is pressed only once, treat it as escape.
killall xcape 2>/dev/null ; xcape -e 'Super_L=Escape'
# Turn off caps lock if on since there is no longer a key for it.
xset -q | grep -q "Caps Lock:\s*on" && xdotool key Caps_Lock

A .local/bin/sb-systemstats => .local/bin/sb-systemstats +51 -0
@@ 0,0 1,51 @@
#!/usr/bin/env bash

# System stats: GPU temp, CPU temp, memory usage (BreadOnPenguins style)
# Nerd Font icons, color-coded temps, click for details

CPU_TEMP=$(sensors | awk '
    /^Package id|^Core 0|^Tdie|^CPU/ {
        gsub(/[+°C]/, "");
        for (i=1; i<=NF; i++) {
            if ($i ~ /^[0-9]+(\.[0-9]+)?$/) {
                gsub(/\..*/, "", $i);
                print $i;
                exit;
            }
        }
    }')
[ -z "$CPU_TEMP" ] && CPU_TEMP="N/A"

# AMD GPU (edge/junction)
GPU_TEMP=$(sensors | awk '/^edge|^junction/ {gsub(/\+/,""); gsub(/\..*/,"",$2); print $2; exit}')
# NVIDIA fallback
if [ -z "$GPU_TEMP" ] && command -v nvidia-smi >/dev/null 2>&1; then
    GPU_TEMP=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader)
fi
[ -z "$GPU_TEMP" ] && GPU_TEMP="N/A"

MEM_USE=$(free -m | awk '/^Mem/ {printf "%.1f", ($3)/1024}')

# CPU icon based on temp
case $CPU_TEMP in
    "N/A") CPU_ICON="󰍛 ?" ;;
    [7-9][6-9]|[8-9][0-9]|100) CPU_ICON="" ;;
    [6][6-9]|7[0-5]) CPU_ICON="󰍛" ;;
    *) CPU_ICON="󰍛" ;;
esac

# GPU icon based on temp
case $GPU_TEMP in
    "N/A") GPU_ICON="󰢮 ?" ;;
    [7-9][1-9]|[8-9][0-9]|100) GPU_ICON="" ;;
    [5][1-9]|70) GPU_ICON="󰢮" ;;
    *) GPU_ICON="󰢮" ;;
esac

echo "$GPU_ICON ${GPU_TEMP}° $CPU_ICON ${CPU_TEMP}°  ${MEM_USE}g"

case $BLOCK_BUTTON in
    1) setsid -w -f "$TERMINAL" -e htop ;;
    3) notify-send "󰍛 System Stats" "CPU: ${CPU_TEMP}°C\nGPU: ${GPU_TEMP}°C\nRAM: ${MEM_USE} GB" ;;
    6) setsid -f "$TERMINAL" -e "$EDITOR" "$0" ;;
esac

A .local/bin/shred => .local/bin/shred +94 -0
@@ 0,0 1,94 @@
#!/bin/sh

# Secure file destruction: strip metadata, overwrite, shred, remove
# Usage: nuke <file1> [file2] ...
# nuke -d <dir>       nuke an entire directory

set -e

die() { echo "nuke: $*" >&2; exit 1; }

strip_metadata() {
    file="$1"
    mime=$(file -b --mime-type "$file" 2>/dev/null || echo "unknown")
    case "$mime" in
        image/*|video/*|audio/*|application/pdf)
            if command -v exiftool >/dev/null 2>&1; then
                exiftool -all= -overwrite_original "$file" 2>/dev/null || true
            fi
            if command -v mat2 >/dev/null 2>&1; then
                mat2 --inplace "$file" 2>/dev/null || true
            fi
            ;;
    esac
}

nuke_file() {
    file="$1"
    [ -f "$file" ] || return

    # 1. strip all metadata
    strip_metadata "$file"

    # 2. get file size, overwrite with /dev/urandom
    size=$(stat -c%s "$file" 2>/dev/null || echo 0)
    if [ "$size" -gt 0 ]; then
        dd if=/dev/urandom of="$file" bs=1 count="$size" conv=notrunc 2>/dev/null || true
    fi

    # 3. shred: 7 passes random + 1 pass zeros to hide shredding
    /usr/bin/shred -vzn 7 "$file" 2>/dev/null

    # 4. truncate to zero
    truncate -s 0 "$file" 2>/dev/null || true

    # 5. rename to random name before unlinking (obscure filename from journal)
    dir=$(dirname "$file")
    rand=$(head -c 16 /dev/urandom | xxd -p)
    mv "$file" "$dir/$rand" 2>/dev/null && sync && rm -f "$dir/$rand"
    if [ -f "$file" ]; then
        sync
        rm -f "$file"
    fi

    echo "nuked: $file"
}

nuke_dir() {
    dir="$1"
    [ -d "$dir" ] || die "not a directory: $dir"

    # nuke all files recursively
    find "$dir" -type f | while read -r f; do
        nuke_file "$f"
    done

    # remove empty directory tree
    rm -rf "$dir"
    echo "nuked directory: $dir"
}

[ -z "$1" ] && die "usage: nuke [-d] <files/dirs>"

if [ "$1" = "-d" ]; then
    shift
    [ -z "$1" ] && die "usage: nuke -d <directory>"
    for d in "$@"; do
        nuke_dir "$d"
    done
else
    for f in "$@"; do
        if [ -d "$f" ]; then
            printf "'%s' is a directory. use nuke -d to nuke directories. skip? [Y/n] " "$f"
            read -r ans
            case "$ans" in
                n|N) nuke_dir "$f" ;;
                *) echo "skipped: $f" ;;
            esac
        elif [ -f "$f" ]; then
            nuke_file "$f"
        else
            echo "not found: $f"
        fi
    done
fi

A .local/bin/sort => .local/bin/sort +237 -0
@@ 0,0 1,237 @@
#!/usr/bin/env bash
# ==============================================================
#  Sort - Downloads cleaner
# --------------------------------------------------------------
#  Author   : Kris Yotam (aka. khr1st)
#  Contact  : krisyotam@protonmail.com
#  License  : GNU GPLv3
#  Date     : 2025-12-04
# --------------------------------------------------------------
#  Description:
#    Minimal, suckless-style bin script to tidy a Downloads
#    directory by moving files into categorized subfolders under
#    "$HOME/Misc" (Compressed, Installers, Packages, Other) and
#    user directories (Pictures, Music, Videos). Shows a small TUI
#    grid progress indicator and prints each move as it happens.
#    At the end the script offers to run the separate `clean`
#    utility which interactively prunes files in "$HOME/Misc".
# ==============================================================

set -eu

# Configuration
DEFAULT_SRCS=("$HOME/Downloads" "$HOME/downloads")
MISC_BASE="$HOME/Misc"
DIR_COMPRESSED="Compressed"
DIR_INSTALLERS="Installers"
DIR_PACKAGES="Packages"
DIR_OTHER="Other"

# User dirs
PICTURES_DIR="$HOME/Pictures"
MUSIC_DIR="$HOME/Music"
VIDEOS_DIR="$HOME/Videos"

# Grid config (10x10 => 100 cells mapping to percent)
GRID_COLS=10
GRID_ROWS=10
GRID_CELLS=$((GRID_COLS * GRID_ROWS))

declare -a FILES=()
declare -a LOGS=()

ext_lower() {
  # Get lowercase extension without leading dot
  fname="$1"
  ext="${fname##*.}"
  printf "%s" "${ext,,}"
}

ensure_dirs() {
  mkdir -p "$MISC_BASE/$DIR_COMPRESSED" \
           "$MISC_BASE/$DIR_INSTALLERS" \
           "$MISC_BASE/$DIR_PACKAGES" \
           "$MISC_BASE/$DIR_OTHER" \
           "$PICTURES_DIR" "$MUSIC_DIR" "$VIDEOS_DIR"
}

pick_dest() {
  filename="$1"
  ext="$(ext_lower "$filename")"
  case "$ext" in
    # Compressed
    zip|tar|gz|tgz|tbz|tbz2|bz2|xz|7z|rar|tar.gz|tar.bz2|tar.xz)
      printf "%s" "$MISC_BASE/$DIR_COMPRESSED" ;;
    # Images -> Pictures
    jpg|jpeg|png|gif|webp|svg|bmp|tif|tiff|heic)
      printf "%s" "$PICTURES_DIR" ;;
    # Audio -> Music
    mp3|wav|flac|m4a|aac|ogg|opus|wma)
      printf "%s" "$MUSIC_DIR" ;;
    # Video -> Videos
    mp4|mkv|webm|mov|avi|mpeg|mpg|flv|3gp)
      printf "%s" "$VIDEOS_DIR" ;;
    # Installers
    exe|msi|dmg|AppImage|sh|run)
      printf "%s" "$MISC_BASE/$DIR_INSTALLERS" ;;
    # Packages
    deb|rpm|pkg|apk)
      printf "%s" "$MISC_BASE/$DIR_PACKAGES" ;;
    *) printf "%s" "$MISC_BASE/$DIR_OTHER" ;;
  esac
}

unique_dest() {
  destdir="$1"
  base="$2"
  dest="$destdir/$base"
  if [ ! -e "$dest" ]; then
    printf "%s" "$dest"
    return 0
  fi
  i=1
  name="${base%.*}"
  ext=""
  if [[ "$base" == *.* ]]; then
    ext=".${base##*.}"
  fi
  while :; do
    candidate="$destdir/${name}_$i$ext"
    if [ ! -e "$candidate" ]; then
      printf "%s" "$candidate" && return 0
    fi
    i=$((i + 1))
  done
}

move_one() {
  src="$1"
  bn="$(basename -- "$src")"
  destdir="$(pick_dest "$bn")"
  mkdir -p "$destdir"
  dest="$(unique_dest "$destdir" "$bn")"
  if mv -- "$src" "$dest"; then
    LOGS+=("Moved: $bn -> $(realpath --relative-to="$HOME" "$dest")")
    echo "Moved: $bn -> $(realpath --relative-to="$HOME" "$dest")"
  else
    LOGS+=("Failed: $bn")
    echo "Failed to move: $bn" >&2
  fi
}

gather_files() {
  src="$1"
  # gather regular files only (no dirs)
  while IFS= read -r -d '' f; do
    FILES+=("$f")
  done < <(find "$src" -maxdepth 1 -type f -print0 2>/dev/null)
}

draw_grid() {
  filled_cells=$1
  columns=$GRID_COLS
  rows=$GRID_ROWS
  idx=0
  for ((r=0;r<rows;r++)); do
    line=""
    for ((c=0;c<columns;c++)); do
      idx=$((r*columns + c + 1))
      if [ $idx -le $filled_cells ]; then
        cell='[**]'
      else
        cell='[  ]'
      fi
      line+="$cell"
    done
    printf "%s\n" "$line"
  done
}

draw_tui() {
  total=$1
  done=$2
  pct=0
  if [ "$total" -gt 0 ]; then
    pct=$((done * 100 / total))
  fi
  # map done -> number of filled cells
  filled_cells=0
  if [ "$total" -gt 0 ]; then
    filled_cells=$((done * GRID_CELLS / total))
  fi
  clear
  printf "Sort: %d/%d files (%d%%)\n\n" "$done" "$total" "$pct"
  draw_grid $filled_cells
  printf "\nRecent actions:\n"
  # show last 8 logs
  start=0
  if [ ${#LOGS[@]} -gt 8 ]; then
    start=$((${#LOGS[@]} - 8))
  fi
  for ((i=start;i<${#LOGS[@]};i++)); do
    printf "  %s\n" "${LOGS[$i]}"
  done
}

main() {
  srcdir=""
  if [ $# -ge 1 ]; then
    srcdir="$1"
  else
    # pick first existing default
    for cand in "${DEFAULT_SRCS[@]}"; do
      [ -d "$cand" ] && { srcdir="$cand"; break; }
    done
  fi
  if [ -z "$srcdir" ]; then
    echo "No Downloads folder found. Specify a directory: sort /path/to/dir" >&2
    exit 1
  fi

  ensure_dirs
  gather_files "$srcdir"
  total=${#FILES[@]}
  if [ "$total" -eq 0 ]; then
    echo "No files to sort in $srcdir"
    exit 0
  fi

  moved=0
  # iterate and move files
  for f in "${FILES[@]}"; do
    move_one "$f"
    moved=$((moved + 1))
    # update tui
    draw_tui "$total" "$moved"
    # tiny pause so user sees progress
    sleep 0.08
  done

  printf "\nDone. Moved %d files into %s and user dirs.\n" "$moved" "$MISC_BASE"

  # Offer to run clean
  printf "\nRun interactive clean on %s? [y/N]: " "$MISC_BASE"
  IFS= read -r ans || ans=""
  case "$ans" in
    [yY]|[yY][eE][sS])
      # call clean script if exists else run inline
      if [ -x "$HOME/.local/bin/clean" ]; then
        "$HOME/.local/bin/clean" "$MISC_BASE"
      elif [ -x "/usr/local/bin/clean" ]; then
        "/usr/local/bin/clean" "$MISC_BASE"
      else
        # try to call script in repo
        if [ -x "$(dirname "$0")/clean" ]; then
          "$(dirname "$0")/clean" "$MISC_BASE"
        else
          echo "clean utility not found. Skipping." >&2
        fi
      fi
      ;;
    *) echo "Skipping clean." ;;
  esac
}

if [ "${BASH_SOURCE[0]}" = "$0" ]; then
  main "$@"
fi

A .local/bin/trash => .local/bin/trash +94 -0
@@ 0,0 1,94 @@
#!/bin/sh

# Simple trash bin - moves files to ~/.local/share/Trash instead of deleting
# Usage: trash <file1> [file2] ...
# trash -l            list trashed files
# trash -e            empty trash permanently
# trash -r <name>     restore file to original location

TRASH_DIR="$HOME/.local/share/Trash"
TRASH_FILES="$TRASH_DIR/files"
TRASH_INFO="$TRASH_DIR/info"

mkdir -p "$TRASH_FILES" "$TRASH_INFO"

case "$1" in
    -l|--list)
        if [ -z "$(ls -A "$TRASH_FILES" 2>/dev/null)" ]; then
            echo "trash is empty"
            exit 0
        fi
        echo "TRASHED FILES:"
        for info in "$TRASH_INFO"/*.trashinfo; do
            [ -f "$info" ] || continue
            name=$(basename "$info" .trashinfo)
            origin=$(grep "^Path=" "$info" | cut -d= -f2-)
            date=$(grep "^DeletionDate=" "$info" | cut -d= -f2-)
            printf "  %-40s  %s  %s\n" "$name" "$date" "$origin"
        done
        echo
        du -sh "$TRASH_FILES" | awk '{print "Total: " $1}'
        ;;
    -e|--empty)
        if [ -z "$(ls -A "$TRASH_FILES" 2>/dev/null)" ]; then
            echo "trash is already empty"
            exit 0
        fi
        size=$(du -sh "$TRASH_FILES" | awk '{print $1}')
        printf "permanently delete all trashed files (%s)? [y/N] " "$size"
        read -r ans
        case "$ans" in
            y|Y) rm -rf "$TRASH_FILES"/* "$TRASH_INFO"/*; echo "trash emptied" ;;
            *) echo "aborted" ;;
        esac
        ;;
    -r|--restore)
        shift
        [ -z "$1" ] && echo "usage: trash -r <name>" && exit 1
        info="$TRASH_INFO/$1.trashinfo"
        file="$TRASH_FILES/$1"
        if [ ! -f "$info" ] || [ ! -e "$file" ]; then
            echo "not found in trash: $1"
            exit 1
        fi
        origin=$(grep "^Path=" "$info" | cut -d= -f2-)
        if [ -e "$origin" ]; then
            echo "destination already exists: $origin"
            exit 1
        fi
        mkdir -p "$(dirname "$origin")"
        mv "$file" "$origin" && rm "$info"
        echo "restored: $origin"
        ;;
    -h|--help)
        echo "usage: trash <files>        move files to trash"
        echo "       trash -l             list trashed files"
        echo "       trash -e             empty trash"
        echo "       trash -r <name>      restore file"
        ;;
    *)
        [ -z "$1" ] && echo "usage: trash <files>" && exit 1
        for f in "$@"; do
            if [ ! -e "$f" ]; then
                echo "not found: $f"
                continue
            fi
            fullpath=$(readlink -f "$f")
            name=$(basename "$f")
            # handle name collisions
            dest="$name"
            n=1
            while [ -e "$TRASH_FILES/$dest" ]; do
                dest="${name}.$n"
                n=$((n + 1))
            done
            mv "$f" "$TRASH_FILES/$dest"
            cat > "$TRASH_INFO/$dest.trashinfo" <<EOF
[Trash Info]
Path=$fullpath
DeletionDate=$(date '+%Y-%m-%dT%H:%M:%S')
EOF
            echo "trashed: $f"
        done
        ;;
esac

A .local/bin/vroid => .local/bin/vroid +40 -0
@@ 0,0 1,40 @@
#!/bin/sh

PREFIX="/home/krisyotam/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata/1486350/pfx/drive_c/users/steamuser/Documents/vroid"

if [ $# -eq 0 ]; then
    echo "Usage: vroid <file.vroid> [file2.vroid ...]"
    echo ""
    echo "Copies .vroid files into the VRoid Studio Proton prefix."
    echo "Destination: C:\\users\\steamuser\\Documents\\vroid\\"
    echo ""
    echo "Current files in prefix:"
    ls "$PREFIX" 2>/dev/null || echo "  (none)"
    exit 0
fi

mkdir -p "$PREFIX"

for file in "$@"; do
    if [ ! -f "$file" ]; then
        echo "File not found: $file"
        continue
    fi

    name=$(basename "$file")

    if [ -f "$PREFIX/$name" ]; then
        printf "'%s' already exists in prefix. Overwrite? [y/N] " "$name"
    else
        printf "Copy '%s' into VRoid prefix? [Y/n] " "$name"
    fi

    read -r answer
    case "$answer" in
        [nN]) echo "Skipped." ;;
        *)
            cp "$file" "$PREFIX/$name"
            echo "Copied -> C:\\users\\steamuser\\Documents\\vroid\\$name"
            ;;
    esac
done

A .local/bin/ytcomments => .local/bin/ytcomments +49 -0
@@ 0,0 1,49 @@
#!/bin/sh

# Scrape YouTube comments into searchable .md and .json files.
# Usage: ytcomments <url> [output_dir]
#   url        - YouTube video URL
#   output_dir - where to save (default: current directory)
#
# Output files are named after the video title, sorted by like count.
# Search with: rg -i "keyword" file.md

die() { printf '%s\n' "$1" >&2; exit 1; }

[ -z "$1" ] && die "Usage: ytcomments <youtube-url> [output-dir]"

command -v yt-dlp >/dev/null || die "yt-dlp not found"
command -v jq >/dev/null || die "jq not found"

url="$1"
outdir="${2:-.}"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

printf 'Scraping comments from: %s\n' "$url"

yt-dlp --skip-download --write-comments \
	-o "$tmpdir/%(title)s.%(ext)s" "$url" 2>&1 | tail -1

infofile=$(find "$tmpdir" -name '*.info.json' | head -1)
[ -f "$infofile" ] || die "Failed to download comments"

title=$(jq -r '.title' "$infofile" | tr '/' '-' | tr -d '\n')
count=$(jq '.comments | length' "$infofile")
printf 'Found %s comments for: %s\n' "$count" "$title"

# Markdown — sorted by likes, most popular first
jq -r '
  .comments | sort_by(-.like_count) | .[] |
  "## \(.author) (\(.like_count) likes)\n\(.text)\n\n---\n"
' "$infofile" > "$outdir/$title.md"

# JSON — clean array sorted by likes
jq '[.comments | sort_by(-.like_count) | .[] | {
  author, text, likes: .like_count,
  replies: (.reply_count // 0),
  time: .timestamp
}]' "$infofile" > "$outdir/$title.json"

printf 'Saved:\n  %s/%s.md\n  %s/%s.json\n' "$outdir" "$title" "$outdir" "$title"
printf 'Search with: rg -i "keyword" "%s/%s.md"\n' "$outdir" "$title"