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 => +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"