From 15fdcf78915608f4681dbcd34bfaa239c0d0dcea Mon Sep 17 00:00:00 2001 From: Kris Yotam Date: Thu, 12 Mar 2026 22:46:32 -0500 Subject: [PATCH] add 10 scripts, remove remapd/remaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New: dllm, dlog, email, lfub, sb-systemstats, shred, sort, trash, vroid, ytcomments Removed: remapd, remaps (banned — caused Xorg CPU spikes) --- .local/bin/dllm | 55 ++++++++ .local/bin/dlog | 286 ++++++++++++++++++++++++++++++++++++++ .local/bin/email | 78 +++++++++++ .local/bin/lfub | 24 ++++ .local/bin/remapd | 8 -- .local/bin/remaps | 11 -- .local/bin/sb-systemstats | 51 +++++++ .local/bin/shred | 94 +++++++++++++ .local/bin/sort | 237 +++++++++++++++++++++++++++++++ .local/bin/trash | 94 +++++++++++++ .local/bin/vroid | 40 ++++++ .local/bin/ytcomments | 49 +++++++ 12 files changed, 1008 insertions(+), 19 deletions(-) create mode 100755 .local/bin/dllm create mode 100755 .local/bin/dlog create mode 100755 .local/bin/email create mode 100755 .local/bin/lfub delete mode 100755 .local/bin/remapd delete mode 100755 .local/bin/remaps create mode 100755 .local/bin/sb-systemstats create mode 100755 .local/bin/shred create mode 100644 .local/bin/sort create mode 100755 .local/bin/trash create mode 100755 .local/bin/vroid create mode 100755 .local/bin/ytcomments diff --git a/.local/bin/dllm b/.local/bin/dllm new file mode 100755 index 0000000000000000000000000000000000000000..6525db5c439fa91d85f61a135f089706a16fa9b3 --- /dev/null +++ b/.local/bin/dllm @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +############################################################################### +# dllm — LLM launcher via dmenu +# +# Maintainer: Kris Yotam +# 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 " + 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 diff --git a/.local/bin/dlog b/.local/bin/dlog new file mode 100755 index 0000000000000000000000000000000000000000..0540686429f92f0e2d662132a2df2f8db4e89cbd --- /dev/null +++ b/.local/bin/dlog @@ -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 +# 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 diff --git a/.local/bin/email b/.local/bin/email new file mode 100755 index 0000000000000000000000000000000000000000..80c4d08a9a459a7cad949b80e968d80a1527e76e --- /dev/null +++ b/.local/bin/email @@ -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]... [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]... [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}" diff --git a/.local/bin/lfub b/.local/bin/lfub new file mode 100755 index 0000000000000000000000000000000000000000..f9bb2df94106b150c0582dfb7cc32400d8c02a99 --- /dev/null +++ b/.local/bin/lfub @@ -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 diff --git a/.local/bin/remapd b/.local/bin/remapd deleted file mode 100755 index ee4cf396e70fc9bc16c865477cd40ee780dbfde7..0000000000000000000000000000000000000000 --- a/.local/bin/remapd +++ /dev/null @@ -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 diff --git a/.local/bin/remaps b/.local/bin/remaps deleted file mode 100755 index 6d7d54e989016942ac12db675c7fb9603fd2aa8c..0000000000000000000000000000000000000000 --- a/.local/bin/remaps +++ /dev/null @@ -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 diff --git a/.local/bin/sb-systemstats b/.local/bin/sb-systemstats new file mode 100755 index 0000000000000000000000000000000000000000..3abb78c08aeb7e9d00b3fc58f1deb26bf6dc9771 --- /dev/null +++ b/.local/bin/sb-systemstats @@ -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 diff --git a/.local/bin/shred b/.local/bin/shred new file mode 100755 index 0000000000000000000000000000000000000000..0f4d0b508da01c1c28938c6d8142908a004c3a90 --- /dev/null +++ b/.local/bin/shred @@ -0,0 +1,94 @@ +#!/bin/sh + +# Secure file destruction: strip metadata, overwrite, shred, remove +# Usage: nuke [file2] ... +# nuke -d 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] " + +if [ "$1" = "-d" ]; then + shift + [ -z "$1" ] && die "usage: nuke -d " + 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 diff --git a/.local/bin/sort b/.local/bin/sort new file mode 100644 index 0000000000000000000000000000000000000000..c75584ba23c8f5650fb34cda30cd10445c7bdd2b --- /dev/null +++ b/.local/bin/sort @@ -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 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 diff --git a/.local/bin/trash b/.local/bin/trash new file mode 100755 index 0000000000000000000000000000000000000000..5c6ad2b5580ecd48ca234e9b559c21b64859988e --- /dev/null +++ b/.local/bin/trash @@ -0,0 +1,94 @@ +#!/bin/sh + +# Simple trash bin - moves files to ~/.local/share/Trash instead of deleting +# Usage: trash [file2] ... +# trash -l list trashed files +# trash -e empty trash permanently +# trash -r 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 " && 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 move files to trash" + echo " trash -l list trashed files" + echo " trash -e empty trash" + echo " trash -r restore file" + ;; + *) + [ -z "$1" ] && echo "usage: trash " && 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" < [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 diff --git a/.local/bin/ytcomments b/.local/bin/ytcomments new file mode 100755 index 0000000000000000000000000000000000000000..6f8ffe380582a4b80098a4a61f8ecb55cc8ff5b7 --- /dev/null +++ b/.local/bin/ytcomments @@ -0,0 +1,49 @@ +#!/bin/sh + +# Scrape YouTube comments into searchable .md and .json files. +# Usage: ytcomments [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 [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"