Restore the browser, gaming, development, and Windows VM menus

This commit is contained in:
2026-09-20 00:30:28 -04:00
parent 3e65d176e3
commit ffa9b2ca0e
42 changed files with 3575 additions and 7 deletions
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# blob:summary=Native messaging host: copy a Chromium tab URL to the clipboard
# blob:hidden=true
set -euo pipefail
SCRIPT_PATH="${BASH_SOURCE[0]}"
export BLOB_PATH="${BLOB_PATH:-$(cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)}"
export PATH="$BLOB_PATH/bin:/usr/local/bin:/usr/bin:$PATH"
parse_url() {
jq -r '.url // empty' 2>/dev/null <<<"$1" || true
}
copy_url() {
local url="$1"
[[ -n $url ]] || return 1
printf '%s' "$url" | wl-copy --type text/plain
blob-notify-send -g 󰅍 "URL copied to clipboard"
}
reply_copied() {
if [[ $1 == "true" ]]; then
printf '\x0f\x00\x00\x00{"copied":true}'
else
printf '\x10\x00\x00\x00{"copied":false}'
fi
}
main() {
local length payload url
length=$(head -c4 | od -An -v -tu4 --endian=little | tr -d ' ')
[[ -n ${length:-} ]] && (( length > 0 )) || exit 0
payload=$(head -c "$length")
url=$(parse_url "$payload")
if copy_url "$url"; then
reply_copied true
else
reply_copied false
fi
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
+196
View File
@@ -0,0 +1,196 @@
#!/bin/bash
# blob:summary=Native messaging host: download the URL sent by the yt-dlp Chromium extension
# blob:hidden=true
set -euo pipefail
SCRIPT_PATH="${BASH_SOURCE[0]}"
# The browser launches us without Blob's environment, so locate the repo from
# our own path when BLOB_PATH isn't already set. Export it — blob-shell (used
# by blob-osd) needs it to find the running shell, and silently no-ops without it.
export BLOB_PATH="${BLOB_PATH:-$(cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)}"
# Make sure the Blob bin and yt-dlp are reachable when launched by the browser.
export PATH="$BLOB_PATH/bin:/usr/local/bin:/usr/bin:$PATH"
DOWNLOAD_DIR="${BLOB_YTDLP_DIR:-$HOME/Videos}"
parse_url() {
jq -r '.url // empty' 2>/dev/null <<<"$1" || true
}
valid_url() {
[[ $1 =~ ^https?:// ]]
}
# A printed path is only usable if it is a regular file inside DOWNLOAD_DIR.
# Forged records (leading-dash mpv options, paths with control chars, or
# anything that escaped the download directory) must not reach the click command.
resolve_download_file() {
local candidate=$1 file_real dir_real
[[ -n $candidate ]] || return 1
[[ $candidate != *$'\n'* && $candidate != *$'\r'* && $candidate != *$'\t'* ]] || return 1
[[ -f $candidate ]] || return 1
# Read to a NUL: command substitution strips trailing newlines, which would
# resolve a name ending in one to a different file that may well exist.
IFS= read -r -d '' file_real < <(realpath -ze -- "$candidate") || return 1
IFS= read -r -d '' dir_real < <(realpath -ze -- "$DOWNLOAD_DIR") || return 1
[[ $file_real != *$'\n'* && $file_real != *$'\r'* && $file_real != *$'\t'* ]] || return 1
# Trim the slash so a download directory of "/" still leaves a usable prefix.
[[ $file_real == "${dir_real%/}"/* ]] || return 1
printf '%s' "$file_real"
}
# yt-dlp prints the title JSON-encoded, so a newline or tab in page metadata is an
# escape sequence rather than a record boundary. This is toast text, never a command.
decode_title() {
local decoded
decoded=$(jq -r 'if type == "string" then . else empty end' <<<"$1" 2>/dev/null) || return 1
decoded=${decoded%%[[:cntrl:]]*} # keep what a person would read, drop the forgery
[[ -n $decoded && $decoded != -* ]] || return 1
printf '%s' "$decoded"
}
title_from_file() {
local name=${1##*/}
name=${name%.*}
name=${name//[$'\n\r\t']/}
if [[ -z $name || $name == -* ]]; then
printf '%s' "Video"
else
printf '%s' "$name"
fi
}
# Drive the Quickshell OSD — a single overlay that updates in place (like the
# volume/brightness bar), so download progress never stacks like notifications.
osd_progress() {
blob-osd -i 󰇚 -p "$1" -d 8000 >/dev/null 2>&1 || true
}
osd_close() {
blob-shell -q osd close >/dev/null 2>&1 || true
}
download_url() {
local url="$1"
mkdir -p "$DOWNLOAD_DIR"
# Don't show anything until yt-dlp confirms there's actually a video to grab.
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
blob-notify-send -u critical -g 󰅖 "No video found for download" "$url"
exit 0
fi
osd_progress 0
# Stream the download: BLOB_PROG carries the percent (drives the OSD), and
# BLOB_FILE and BLOB_TITLE (printed only after a successful move) carry the
# path and the title. The title is JSON-encoded so metadata cannot forge a record,
# and the file is named after it: yt-dlp strips control characters from a filename
# with or without --restrict-filenames, so a record is still only ever one line.
local line pct intpct last="" er nowms lastms=0 title="" filepath="" resolved
while IFS= read -r line; do
case $line in
BLOB_PROG*)
pct=${line#BLOB_PROG$'\t'}
intpct=${pct%%.*}
intpct=${intpct//[^0-9]/}
[[ -n $intpct && $intpct != "$last" ]] || continue # skip no-op repeats
# Throttle to ~4 redraws/sec so fast downloads don't spawn a flurry of processes.
er=$EPOCHREALTIME
nowms=$((${er%[.,]*} * 1000 + 10#${er##*[.,]} / 1000))
((nowms - lastms >= 250)) || continue
last=$intpct
lastms=$nowms
osd_progress "$intpct"
;;
BLOB_FILE*)
resolved=$(resolve_download_file "${line#BLOB_FILE$'\t'}") || continue
filepath=$resolved
;;
BLOB_TITLE*)
title=$(decode_title "${line#BLOB_TITLE$'\t'}") || title=""
;;
esac
done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --no-simulate \
--quiet --no-warnings --no-exec --no-exec-before-download --progress --newline \
--progress-template $'download:BLOB_PROG\t%(progress._percent_str)s' \
--paths "$DOWNLOAD_DIR" -o '%(title)s.%(ext)s' \
--print $'after_move:BLOB_FILE\t%(filepath)s' \
--print $'after_move:BLOB_TITLE\t%(title)j' \
-- "$url" 2>&1)
osd_close
# after_move only prints on a successful download+move, so a captured path == success.
if [[ -n $filepath ]]; then
[[ -n $title ]] || title=$(title_from_file "$filepath")
((${#title} > 50)) && title="${title:0:50}…" # keep the toast compact
# Square, center-cropped thumbnail so the notification preview isn't stretched.
local preview
preview="$(mktemp --suffix=.jpg)"
ffmpeg -y -i "$filepath" -ss 00:00:00.1 -vframes 1 \
-vf "crop='min(iw,ih)':'min(iw,ih)',scale=256:256" -q:v 2 \
"$preview" -loglevel quiet 2>/dev/null || true
# Best-effort: the download already succeeded, and under `set -e` a failed
# toast would exit before the thumbnail cleanup below is ever scheduled.
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
# is one discrete argument, so it never reaches a shell.
blob-notify-send -g 󰄬 "Download complete" "$title" \
-t 10000 --image "${preview:-$filepath}" \
--exec mpv -- "$filepath" || true
# The shell loads the thumbnail into memory when the toast appears and never
# re-reads the file, so the preview only has to outlive that load, not the
# toast.
(
sleep 2
rm -f "$preview"
) &
else
blob-notify-send -u critical -g 󰅖 "Download failed" "$url"
fi
exit 0
}
main() {
local length payload url
# Detached worker: this is what actually runs yt-dlp and fires notifications.
if [[ "${1:-}" == "--download" ]]; then
download_url "$2"
fi
# Native messaging frame: 4-byte little-endian length prefix, then UTF-8 JSON.
length=$(head -c4 | od -An -v -tu4 --endian=little | tr -d ' ')
[[ -n ${length:-} ]] && ((length > 0)) || exit 0
payload=$(head -c "$length")
# Ack with an empty message so the extension's sendNativeMessage callback resolves cleanly.
printf '\x02\x00\x00\x00{}'
url=$(parse_url "$payload")
[[ -n $url ]] || exit 0
valid_url "$url" || exit 0
# Detach the download so this host exits promptly and frees the browser's port.
setsid -f "$SCRIPT_PATH" --download "$url" </dev/null >/dev/null 2>&1
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# blob:summary=List installed RetroArch core names
set -e
core_dir="/usr/lib/libretro"
preferred_cores=(
"Amstrad CPC|cap32"
"Arcade FBNeo|fbneo"
"Arcade MAME|mame"
"Commodore Amiga|puae"
"Commodore C128|vice_x128"
"Commodore C64|vice_x64"
"Commodore VIC-20|vice_xvic"
"Nintendo DS|desmume"
"Nintendo Game Boy / Color|gambatte"
"Nintendo Game Boy Advance|mgba"
"Nintendo GameCube / Wii|dolphin"
"Nintendo NES / Famicom|mesen"
"Nintendo 64|parallel_n64"
"Nintendo SNES / SFC|snes9x"
"NEC PC Engine / TurboGrafx-16|mednafen_pce_fast"
"NEC PC Engine CD / TurboGrafx-CD|mednafen_pce"
"NEC PC Engine SuperGrafx|mednafen_supergrafx"
"Sega Dreamcast|flycast"
"Sega Mega Drive / Master System / Game Gear|genesis_plus_gx"
"Sega Saturn|kronos"
"Sony PlayStation|mednafen_psx_hw"
"Sony PlayStation Portable|ppsspp"
)
[[ -d $core_dir ]] || exit 0
for preferred_core in "${preferred_cores[@]}"; do
label="${preferred_core%%|*}"
core="${preferred_core#*|}"
[[ -f $core_dir/${core}_libretro.so ]] && printf '%s (%s)\n' "$label" "$core"
done
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# blob:summary=Create a desktop launcher for a RetroArch game
# blob:args=[core path-to-game]
# blob:examples=blob games retro install snes9x ~/Games/roms/snes/game.sfc | blob-games-retro-install /usr/lib/libretro/mgba_libretro.so ~/Games/roms/gba/game.gba
set -e
if (( $# == 0 )); then
mapfile -t cores < <(blob-games-retro-cores)
if (( ${#cores[@]} == 0 )); then
blob-notify-send -g 󰯉 "No RetroArch cores found" "/usr/lib/libretro"
exit 1
fi
core=$(blob-menu-select "RetroArch core" "${cores[@]}") || exit 0
[[ -n $core ]] || exit 0
core="${core##*(}"
core="${core%)}"
game_path=$(blob-menu-file "Retro game" "$HOME/Games/roms" "7z bin ccd chd cue dmg elf fds gb gba gbc iso lha m3u md n64 nds nes pbp sfc smc swc zip z64") || exit 0
[[ -n $game_path ]] || exit 0
elif (( $# == 2 )); then
core="$1"
game_path="$2"
else
echo "Usage: blob-games-retro-install [core path-to-game]"
echo "Example: blob-games-retro-install snes9x ~/Games/roms/snes/game.sfc"
exit 1
fi
if [[ ! -f $game_path ]]; then
echo "Game not found: $game_path"
exit 1
fi
if [[ $core == */* ]]; then
core_path="$core"
else
core_path="/usr/lib/libretro/${core}_libretro.so"
fi
if [[ ! -f $core_path ]]; then
echo "Core not found: $core_path"
exit 1
fi
game_name=$(printf '%s' "${game_path##*/}" | sed 's/\.[^.]*$//; s/[[:space:]]*([^)]*)//g; s/[[:space:]]\+/ /g; s/^ //; s/ $//' | perl -Mopen=locale -pe 's/(^|[[:space:]])([^[:space:]])/$1\U$2/g')
desktop_name="$game_name"
desktop_id=$(printf '%s' "$desktop_name" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '-' | sed 's/^-//; s/-$//')
desktop_dir="$HOME/.local/share/applications"
desktop_file="$desktop_dir/$desktop_id.desktop"
mkdir -p "$desktop_dir"
cat >"$desktop_file" <<EOF
[Desktop Entry]
Version=1.0
Name=$desktop_name
Comment=Play $game_name with RetroArch
Exec=retroarch -L "$core_path" "$game_path"
Terminal=false
Type=Application
Icon=retro-gaming
StartupNotify=true
Categories=Game;Emulator;
EOF
chmod +x "$desktop_file"
update-desktop-database "$desktop_dir" &>/dev/null || true
blob-notify-send -g 󰯉 "$game_name installed" "Start it with Super + Space"
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# blob:summary=Install a supported browser
# blob:args=<chrome|brave|brave-origin|edge|firefox|zen>
# blob:examples=blob install browser firefox | blob install browser brave
source "$BLOB_PATH/install/helpers/browser-policy.sh"
setup_chromium_policy_directory() {
browser_policy_setup_dir "$1"
}
announce_browser_installed() {
echo ""
echo "$1 browser installed. Make it the default via Setup > Defaults > Browser."
}
copy_chromium_flags() {
mkdir -p ~/.config
cp -f "$BLOB_PATH/config/chromium-flags.conf" "$1"
blob-install-chromium-copy-url
blob-install-chromium-ytdlp
}
setup_firefox_wayland() {
mkdir -p ~/.config/environment.d
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/blob-firefox-wayland.conf
}
case $1 in
chrome)
echo "Installing Chrome..."
blob-pkg-aur-add google-chrome || exit 1
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
copy_chromium_flags ~/.config/chrome-flags.conf
blob-theme-browser
announce_browser_installed "Chrome"
;;
edge)
echo "Installing Edge..."
blob-pkg-aur-add microsoft-edge-stable-bin || exit 1
setup_chromium_policy_directory /etc/opt/edge/policies/managed
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
blob-theme-browser
announce_browser_installed "Edge"
;;
brave)
echo "Installing Brave..."
blob-pkg-aur-add brave-bin || exit 1
setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-flags.conf
blob-theme-browser
announce_browser_installed "Brave"
;;
brave-origin)
echo "Installing Brave Origin..."
blob-pkg-aur-add brave-origin-bin || exit 1
setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-origin-flags.conf
blob-theme-browser
announce_browser_installed "Brave Origin"
;;
firefox)
echo "Installing Firefox..."
blob-pkg-add firefox || exit 1
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
setup_firefox_wayland
announce_browser_installed "Firefox"
;;
zen)
echo "Installing Zen..."
blob-pkg-aur-add zen-browser-bin || exit 1
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
setup_firefox_wayland
announce_browser_installed "Zen"
;;
*)
echo "Usage: blob-install-browser <chrome|brave|brave-origin|edge|firefox|zen>"
exit 1
;;
esac
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# blob:summary=Install the native messaging host for the Copy URL Chromium extension
set -euo pipefail
HOST_NAME="com.blob.copy_url"
HOST_PATH="$BLOB_PATH/bin/blob-chromium-copy-url-host"
TEMPLATE="$BLOB_PATH/default/chromium/native-messaging-hosts/$HOST_NAME.json"
browser_dirs=(
"$HOME/.config/chromium"
"$HOME/.config/google-chrome"
"$HOME/.config/google-chrome-beta"
"$HOME/.config/google-chrome-unstable"
"$HOME/.config/BraveSoftware/Brave-Browser"
"$HOME/.config/BraveSoftware/Brave-Browser-Beta"
"$HOME/.config/BraveSoftware/Brave-Browser-Nightly"
"$HOME/.config/microsoft-edge"
"$HOME/.config/microsoft-edge-dev"
)
manifest=$(sed "s|__HOST_PATH__|$HOST_PATH|g" "$TEMPLATE")
for dir in "${browser_dirs[@]}"; do
mkdir -p "$dir/NativeMessagingHosts"
printf '%s\n' "$manifest" >"$dir/NativeMessagingHosts/$HOST_NAME.json"
done
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# blob:summary=Install the native messaging host for the yt-dlp Chromium extension
set -euo pipefail
HOST_NAME="com.blob.ytdlp"
HOST_PATH="$BLOB_PATH/bin/blob-chromium-ytdlp-host"
TEMPLATE="$BLOB_PATH/default/chromium/native-messaging-hosts/$HOST_NAME.json"
# Chromium-based browser profile roots that use the NativeMessagingHosts layout.
browser_dirs=(
"$HOME/.config/chromium"
"$HOME/.config/google-chrome"
"$HOME/.config/google-chrome-beta"
"$HOME/.config/google-chrome-unstable"
"$HOME/.config/BraveSoftware/Brave-Browser"
"$HOME/.config/BraveSoftware/Brave-Browser-Beta"
"$HOME/.config/BraveSoftware/Brave-Browser-Nightly"
"$HOME/.config/microsoft-edge"
"$HOME/.config/microsoft-edge-dev"
)
manifest=$(sed "s|__HOST_PATH__|$HOST_PATH|g" "$TEMPLATE")
for dir in "${browser_dirs[@]}"; do
mkdir -p "$dir/NativeMessagingHosts"
printf '%s\n' "$manifest" >"$dir/NativeMessagingHosts/$HOST_NAME.json"
done
+155
View File
@@ -0,0 +1,155 @@
#!/bin/bash
# blob:summary=Install a supported development environment
# blob:name=dev-env
# blob:args=<ruby|node|bun|deno|go|laravel|symfony|php|python|elixir|phoenix|rust|java|zig|ocaml|dotnet|clojure|scala>
# blob:examples=blob install dev-env ruby | blob install dev-env node
# blob:requires-sudo=true
if [[ -z $1 ]]; then
echo "Usage: blob-install-dev-env <ruby|node|bun|deno|go|laravel|symfony|php|python|elixir|phoenix|rust|java|zig|ocaml|dotnet|clojure|scala>" >&2
exit 1
fi
install_php() {
blob-pkg-add php composer php-sqlite xdebug
# Install Path for Composer
if [[ :$PATH: != *:$HOME/.config/composer/vendor/bin:* ]]; then
echo 'export PATH="$HOME/.config/composer/vendor/bin:$PATH"' >>"$HOME/.bashrc"
source "$HOME/.bashrc"
echo "Added Composer global bin directory to PATH."
else
echo "Composer global bin directory already in PATH."
fi
# Enable some extensions
local php_ini_path="/etc/php/php.ini"
local extensions_to_enable=(
"bcmath"
"intl"
"iconv"
"openssl"
"pdo_sqlite"
"pdo_mysql"
)
# Enable Xdebug
sudo sed -i \
-e 's/^;zend_extension=xdebug.so/zend_extension=xdebug.so/' \
-e 's/^;xdebug.mode=debug/xdebug.mode=debug/' \
/etc/php/conf.d/xdebug.ini
for ext in "${extensions_to_enable[@]}"; do
sudo sed -i "s/^;extension=${ext}/extension=${ext}/" "$php_ini_path"
done
}
install_node() {
echo -e "Installing Node.js...\n"
mise use --global node
}
case "$1" in
ruby)
echo -e "Installing Ruby on Rails...\n"
blob-pkg-add libyaml
mise settings add ruby.compile false
mise settings add idiomatic_version_file_enable_tools ruby
mise use --global ruby@latest
echo "gem: --no-document" >~/.gemrc
mise x ruby -- gem install rails --no-document
echo -e "\nYou can now run: rails new myproject"
;;
node)
install_node
;;
bun)
echo -e "Installing Bun...\n"
mise use -g bun@latest
;;
deno)
echo -e "Installing Deno...\n"
mise use -g deno@latest
;;
go)
echo -e "Installing Go...\n"
mise use --global go@latest
;;
php)
echo -e "Installing PHP...\n"
install_php
;;
laravel)
echo -e "Installing PHP and Laravel...\n"
install_php
install_node
composer global require laravel/installer
echo -e "\nYou can now run: laravel new myproject"
;;
symfony)
echo -e "Installing PHP and Symfony...\n"
install_php
blob-pkg-add symfony-cli
echo -e "\nYou can now run: symfony new --webapp myproject"
;;
python)
echo -e "Installing Python...\n"
mise use --global python@latest
echo -e "\nInstalling uv...\n"
curl -fsSL https://astral.sh/uv/install.sh | sh
;;
elixir)
echo -e "Installing Elixir...\n"
mise use --global erlang@latest
mise use --global elixir@latest
mise x elixir -- mix local.hex --force
;;
phoenix)
echo -e "Installing Phoenix Framework...\n"
# Ensure Erlang/Elixir first
mise use --global erlang@latest
mise use --global elixir@latest
# Hex & Rebar
mise x elixir -- mix local.hex --force
mise x elixir -- mix local.rebar --force
# Phoenix project (phx_new)
mise x elixir -- mix archive.install hex phx_new --force
echo -e "\nYou can now run: mix phx.new my_app"
;;
rust)
echo -e "Installing Rust...\n"
bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs)" -- -y
;;
java)
echo -e "Installing Java...\n"
mise use --global java@latest
;;
zig)
echo -e "Installing Zig...\n"
mise use --global zig@latest
mise use -g zls@latest
;;
ocaml)
echo -e "Installing OCaml...\n"
bash -c "$(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
opam init --yes
eval "$(opam env)"
opam install ocaml-lsp-server odoc ocamlformat utop --yes
;;
dotnet)
echo -e "Installing .NET...\n"
mise use --global dotnet@latest
;;
clojure)
echo -e "Installing Clojure...\n"
blob-pkg-add rlwrap
mise use --global clojure@latest
;;
scala)
echo -e "Installing Scala...\n"
mise use --global java@latest
mise use --global scala@latest
mise use --global scala-cli@latest
;;
esac
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# blob:summary=Install one of the supported databases in a Docker container with the suitable development options.
# blob:requires-sudo=true
options=("MySQL" "PostgreSQL" "Redis" "MongoDB" "MariaDB" "MSSQL")
if (( $# == 0 )); then
choices=$(printf "%s\n" "${options[@]}" | gum choose --header "Select database (return to install, esc to cancel)") || main_menu
else
choices="$@"
fi
if [[ -n $choices ]]; then
for db in $choices; do
echo "Installing $db..."
case $db in
MySQL) sudo docker run -d --restart unless-stopped -p "127.0.0.1:3306:3306" --name=mysql8 -e MYSQL_ROOT_PASSWORD= -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql:8.4 ;;
PostgreSQL) sudo docker run -d --restart unless-stopped -p "127.0.0.1:5432:5432" --name=postgres18 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:18 ;;
MariaDB) sudo docker run -d --restart unless-stopped -p "127.0.0.1:3306:3306" --name=mariadb11 -e MARIADB_ROOT_PASSWORD= -e MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true mariadb:11.8 ;;
Redis) sudo docker run -d --restart unless-stopped -p "127.0.0.1:6379:6379" --name=redis redis:7 ;;
MongoDB) sudo docker run -d --restart unless-stopped -p "127.0.0.1:27017:27017" --name mongodb -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=admin123 mongo:noble ;;
MSSQL) sudo docker run -d --restart unless-stopped -p "127.0.0.1:1433:1433" --name mssql -e MSSQL_PID=Developer -e ACCEPT_EULA=Y -e "MSSQL_SA_PASSWORD=@dmin123" mcr.microsoft.com/mssql/server:2022-CU12-ubuntu-22.04 ;;
esac
done
else
echo "No databases selected for installation."
fi
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# blob:summary=Install Battle.net standalone via umu-launcher + GE-Proton (no Steam, no Lutris, no Heroic).
# blob:requires-sudo=true
set -e
PREFIX="$HOME/Games/battlenet"
LAUNCHER="$PREFIX/drive_c/Program Files (x86)/Battle.net/Battle.net Launcher.exe"
INSTALLER_URL="https://downloader.battle.net/download/getInstallerForGame?os=win&gameProgram=BATTLENET_APP&version=Live"
echo "Installing Battle.net..."
blob-pkg-add umu-launcher
blob-install-gaming-gpu-lib32
# Detect a half-finished prefix from a closed/crashed previous run and offer
# to wipe it before trying again. Battle.net's installer isn't idempotent.
if [[ -d $PREFIX && ! -f $LAUNCHER ]]; then
echo
echo "Found a partial Battle.net install at $PREFIX (no Launcher.exe)."
echo "Battle.net's installer can't resume from this state."
if gum confirm "Wipe the partial prefix and start fresh?"; then
pkill -f "$PREFIX" 2>/dev/null || true
sleep 1
rm -rf "$PREFIX"
else
echo "Aborting. Re-run when ready to wipe."
exit 1
fi
fi
mkdir -p "$PREFIX"
export WINEPREFIX="$PREFIX"
export PROTONPATH=GE-Proton
export GAMEID=umu-battlenet
export PROTON_VERB=run
if [[ -f $LAUNCHER ]]; then
echo "Battle.net is already installed at $PREFIX."
launched_installer=0
else
cache_dir="$HOME/.cache/blob"
mkdir -p "$cache_dir"
installer="$cache_dir/Battle.net-Setup.exe"
echo
echo "Downloading Battle.net installer..."
curl --fail --location --retry 3 "$INSTALLER_URL" --output "$installer"
cat <<'EOF'
Launching the Battle.net setup wizard. Click through it normally — the
default install path is fine. When it finishes, Battle.net will be in your
app launcher.
EOF
log="/tmp/blob-battlenet-installer.log"
setsid -f sh -c "umu-run '$installer' >'$log' 2>&1" </dev/null >/dev/null 2>&1
echo "Installer log: $log"
launched_installer=1
fi
mkdir -p "$HOME/.local/share/applications"
install -m 644 "$BLOB_PATH/default/applications/battlenet.desktop" \
"$HOME/.local/share/applications/battlenet.desktop"
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
if (( launched_installer )); then
cat <<EOF
The Battle.net installer is running in the background. After it finishes,
find Battle.net in your app launcher, or run:
blob-launch-battlenet
EOF
else
cat <<EOF
Battle.net is installed. Find it in your app launcher, or run:
blob-launch-battlenet
EOF
fi
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# blob:summary=Install and launch Geforce Now.
# blob:group=install
# blob:name=gaming geforce-now
set -e
echo "Installing GeForce NOW..."
blob-pkg-add flatpak
cd /tmp
# Download and run GeForce NOW
curl -LO https://international.download.nvidia.com/GFNLinux/GeForceNOWSetup.bin
chmod +x GeForceNOWSetup.bin
./GeForceNOWSetup.bin
# Ensure a separate browser process not started by GFN is available.
# If not, it seems like GFN has a tendency to hang on login.
setsid blob-launch-browser
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# blob:summary=Install lib32 graphics drivers (Vulkan + NVIDIA) for any detected GPUs.
# blob:group=install
# blob:name=gaming gpu-lib32
# blob:requires-sudo=true
set -e
echo "Installing lib32 graphics drivers..."
PACKAGES=()
declare -A VULKAN_DRIVERS=(
[Intel]=lib32-vulkan-intel
[AMD]=lib32-vulkan-radeon
)
for vendor in "${!VULKAN_DRIVERS[@]}"; do
if lspci | grep -iE "(VGA|Display).*$vendor" >/dev/null; then
PACKAGES+=("${VULKAN_DRIVERS[$vendor]}")
fi
done
if blob-hw-nvidia-gsp; then
PACKAGES+=(lib32-nvidia-utils)
elif blob-hw-nvidia-without-gsp; then
PACKAGES+=(lib32-nvidia-580xx-utils)
fi
(( ${#PACKAGES[@]} > 0 )) && blob-pkg-add "${PACKAGES[@]}"
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# blob:summary=Install Heroic Games Launcher (Epic, GOG, Amazon Prime Gaming) with graphics drivers.
# blob:requires-sudo=true
set -e
echo "Installing Heroic Games Launcher..."
blob-pkg-add heroic-games-launcher-bin
blob-install-gaming-gpu-lib32
setsid uwsm-app -- gtk-launch heroic >/dev/null 2>&1 &
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# blob:summary=Install Lutris with Wine + DXVK for running Windows games (Battle.net, EA, Ubisoft Connect, etc.)
# blob:requires-sudo=true
set -e
echo "Installing Lutris..."
blob-pkg-add lutris umu-launcher wine-staging wine-mono wine-gecko winetricks python-protobuf
blob-install-gaming-gpu-lib32
# Lutris ships with `#!/usr/bin/env python3`, which resolves to mise's Python and
# fails to import the lutris module. Pin the shebang to the system Python.
sudo sed -i '/env python3/ c\#!/bin/python3' /usr/bin/lutris
cat <<'EOF'
Lutris will open and auto-fetch its DXVK and VKD3D runtimes in the background
(watch the bottom status bar). Once that finishes, click the + to add or install games.
EOF
setsid lutris >/dev/null 2>&1 &
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# blob:summary=Install RetroArch with the full libretro core set plus FBNeo and a ~/Games ROM directory.
set -e
echo "Installing RetroArch..."
blob-pkg-add \
retroarch \
retroarch-assets-glui retroarch-assets-ozone retroarch-assets-xmb \
libretro-beetle-pce libretro-beetle-pce-fast libretro-beetle-psx libretro-beetle-psx-hw libretro-beetle-supergrafx \
libretro-blastem \
libretro-bsnes libretro-bsnes-hd \
libretro-core-info \
libretro-desmume libretro-dolphin libretro-flycast \
libretro-gambatte libretro-genesis-plus-gx \
libretro-kronos \
libretro-mame libretro-melonds libretro-mesen libretro-mesen-s libretro-mgba libretro-mupen64plus-next \
libretro-nestopia \
libretro-overlays \
libretro-parallel-n64 libretro-picodrive libretro-play libretro-ppsspp \
libretro-sameboy libretro-scummvm libretro-shaders-slang libretro-snes9x \
libretro-yabause \
libretro-cap32-git libretro-fbneo-git libretro-uae-git \
libretro-vice-x128-git libretro-vice-x64-git libretro-vice-x64dtv-git libretro-vice-x64sc-git \
libretro-vice-xcbm2-git libretro-vice-xcbm5x0-git libretro-vice-xpet-git \
libretro-vice-xplus4-git libretro-vice-xscpu64-git libretro-vice-xvic-git \
libretro-database-git \
retroarch-joypad-autoconfig-git
# Set up ~/Games for BIOS files and ROMs
mkdir -p "$HOME/Games/bios" "$HOME/Games/roms"
CFG="$HOME/.config/retroarch/retroarch.cfg"
mkdir -p "$(dirname "$CFG")"
touch "$CFG"
set_cfg() {
local key=$1 value=$2
if grep -q "^$key = " "$CFG"; then
sed -i "s|^$key = .*|$key = \"$value\"|" "$CFG"
else
echo "$key = \"$value\"" >>"$CFG"
fi
}
set_cfg rgui_browser_directory "$HOME/Games/roms"
set_cfg system_directory "$HOME/Games/bios"
# Point at the cores and assets installed by pacman
set_cfg libretro_directory "/usr/lib/libretro"
set_cfg libretro_info_path "/usr/share/libretro/info"
set_cfg overlay_directory "/usr/share/libretro/overlays"
set_cfg osk_overlay_directory "/usr/share/libretro/overlays/keyboards"
set_cfg video_shader_dir "/usr/share/libretro/shaders/shaders_slang"
set_cfg joypad_autoconfig_dir "/usr/share/libretro/autoconfig"
# Point at the database, cheats, and cursors from libretro-database-git
set_cfg content_database_path "/usr/share/libretro/database/rdb"
set_cfg cheat_database_path "/usr/share/libretro/database/cht"
set_cfg cursor_directory "/usr/share/libretro/database/cursors"
# Vulkan is required for slang shaders and unlocks hardware renderers in beetle-psx-hw, parallel-n64, dolphin
set_cfg video_driver "vulkan"
# XMB is the classic PS3-style menu (vs. ozone/rgui/glui)
set_cfg menu_driver "xmb"
# Default to crt-royale shader for that classic CRT look. The global preset is
# auto-loaded by RetroArch when auto_shaders_enable is true and no per-core/per-game
# preset takes precedence — setting video_shader alone in retroarch.cfg is not enough.
set_cfg video_shader_enable "true"
set_cfg auto_shaders_enable "true"
mkdir -p ~/.config/retroarch/config
echo '#reference "/usr/share/libretro/shaders/shaders_slang/crt/crt-royale.slangp"' \
> ~/.config/retroarch/config/global.slangp
# Hide Images and Video tabs in the main menu sidebar
set_cfg content_show_images "false"
set_cfg content_show_video "false"
echo ""
echo "Put your roms and bios files in ~/Games. Then start RetroArch from the app launcher (Super + Space)."
setsid nautilus "$HOME/Games" >/dev/null 2>&1 &
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# blob:summary=Install Steam and graphics drivers selected for this system
# blob:requires-sudo=true
set -e
echo "Installing Steam..."
blob-pkg-add steam
blob-install-gaming-gpu-lib32
echo ""
echo "Steam will start automatically now. This might take a while..."
setsid uwsm-app -- gtk-launch steam >/dev/null 2>&1 &
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# blob:summary=Install Xbox Cloud Gaming as a web app and launch it.
# blob:group=install
# blob:name=gaming xbox-cloud
set -e
echo "Installing Xbox Cloud Gaming..."
blob-webapp-install "Xbox Cloud Gaming" "https://www.xbox.com/en-US/play" "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/xbox.png"
setsid blob-launch-webapp "https://www.xbox.com/en-US/play" >/dev/null 2>&1 &
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# blob:summary=Install support for using Xbox controllers with Steam/RetroArch/etc.
# blob:group=install
# blob:name=gaming xbox-controllers
# blob:requires-sudo=true
set -e
echo "Installing Xbox controller Bluetooth support..."
# Install xpadneo to ensure controllers work out of the box
blob-pkg-add xpadneo-dkms
# Prevent xpad/xpadneo driver conflict
echo blacklist xpad | sudo tee /etc/modprobe.d/blacklist-xpad.conf >/dev/null
echo hid_xpadneo | sudo tee /etc/modules-load.d/xpadneo.conf >/dev/null
# Ensure user is in the input group (controllers need it)
needs_reboot=false
if ! id -nG "$USER" | grep -qw input; then
sudo usermod -aG input "$USER"
needs_reboot=true
fi
# Swap drivers in the running kernel so a reboot isn't needed otherwise
if lsmod | grep -q '^xpad '; then
sudo modprobe -r xpad 2>/dev/null || needs_reboot=true
fi
if $needs_reboot; then
gum confirm "Reboot needed to finish setup. Reboot now?" && sudo reboot now
exit 0
fi
sudo modprobe hid_xpadneo
echo ""
echo "Now you can pair your Xbox controller with Bluetooth using Super + Ctrl + B."
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# blob:summary=Launch the installed Battle.net client via umu-launcher + GE-Proton.
# blob:args=[--with-mangohud]
# blob:examples=blob launch battlenet | blob launch battlenet --with-mangohud
set -e
PREFIX="$HOME/Games/battlenet"
LAUNCHER="$PREFIX/drive_c/Program Files (x86)/Battle.net/Battle.net Launcher.exe"
with_mangohud=0
for arg in "$@"; do
case "$arg" in
--with-mangohud) with_mangohud=1 ;;
-h|--help)
cat <<'EOF'
Usage: blob-launch-battlenet [--with-mangohud]
Options:
--with-mangohud Enable the MangoHud FPS overlay for games launched from
Battle.net. Toggle perf logging in-game with Shift_L+F2;
CSV logs land in ~/mangohud/.
EOF
exit 0
;;
*)
echo "Unknown argument: $arg" >&2
echo "Try: blob-launch-battlenet --help" >&2
exit 1
;;
esac
done
if [[ ! -f $LAUNCHER ]]; then
echo "Battle.net is not installed. Run blob-install-gaming-battlenet first." >&2
exit 1
fi
env_args=(
WINEPREFIX="$PREFIX"
PROTONPATH=GE-Proton
GAMEID=umu-battlenet
PROTON_VERB=run
)
(( with_mangohud )) && env_args+=(MANGOHUD=1)
env "${env_args[@]}" umu-run "$LAUNCHER"
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# blob:summary=Launch or focus on a given web app identified by the window-pattern.
# blob:args=<window-pattern> <url-and-flags...>
if (($# == 0)); then
echo "Usage: blob-launch-or-focus-webapp [window-pattern] [url-and-flags...]"
exit 1
fi
WINDOW_PATTERN="$1"
shift
LAUNCH_COMMAND="blob-launch-webapp $@"
exec blob-launch-or-focus "$WINDOW_PATTERN" "$LAUNCH_COMMAND"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# blob:summary=Launch a URL as a web app in the default supported browser
# blob:args=<url>
browser=$(xdg-settings get default-web-browser)
case $browser in
google-chrome* | brave* | microsoft-edge* | opera* | vivaldi* | helium*) ;;
*) browser="chromium.desktop" ;;
esac
exec setsid uwsm-app -- $(sed -n 's/^Exec=\([^ ]*\).*/\1/p' {~/.local,~/.nix-profile,/usr}/share/applications/$browser 2>/dev/null | head -1) --app="$1" "${@:2}"
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# blob:summary=Add the named packages to the system from the AUR if they're missing. Returns false if it couldn't be done.
# blob:args=<packages...>
if blob-pkg-missing "$@"; then
yay -S --noconfirm --needed "$@" || exit 1
fi
for pkg in "$@"; do
# Secondary check to handle states where pacman doesn't actually register an error
if ! pacman -Q "$pkg" &>/dev/null; then
echo -e "\033[31mError: Package '$pkg' did not install\033[0m" >&2
exit 1
fi
done
exit 0
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# blob:summary=Remove a supported browser and clean up Blob browser defaults
# blob:args=<chrome|brave|brave-origin|edge|firefox|zen>
# blob:examples=blob remove browser firefox | blob remove browser brave
# blob:requires-sudo=true
set_fallback_default_browser() {
local current_browser
current_browser=$(env -u BROWSER xdg-settings get default-web-browser)
if [[ $current_browser != $1 ]]; then
return
fi
if blob-cmd-present chromium; then
env -u BROWSER xdg-settings set default-web-browser chromium.desktop || true
fi
}
case $1 in
chrome)
echo "Removing Chrome..."
set_fallback_default_browser google-chrome.desktop
blob-pkg-drop google-chrome
rm -f ~/.config/chrome-flags.conf
sudo rm -f /etc/opt/chrome/policies/managed/color.json
;;
edge)
echo "Removing Edge..."
set_fallback_default_browser microsoft-edge.desktop
blob-pkg-drop microsoft-edge-stable-bin
rm -f ~/.config/microsoft-edge-stable-flags.conf
sudo rm -f /etc/opt/edge/policies/managed/color.json
;;
brave)
echo "Removing Brave..."
set_fallback_default_browser brave-browser.desktop
blob-pkg-drop brave-bin
rm -f ~/.config/brave-flags.conf
if blob-pkg-missing brave-origin-bin; then
sudo rm -rf /etc/brave
fi
;;
brave-origin)
echo "Removing Brave Origin..."
set_fallback_default_browser brave-origin.desktop
blob-pkg-drop brave-origin-bin
rm -f ~/.config/brave-origin-flags.conf
if blob-pkg-missing brave-bin; then
sudo rm -rf /etc/brave
fi
;;
firefox)
echo "Removing Firefox..."
set_fallback_default_browser firefox.desktop
blob-pkg-drop firefox
;;
zen)
echo "Removing Zen..."
set_fallback_default_browser zen.desktop
blob-pkg-drop zen-browser-bin
;;
*)
echo "Usage: blob-remove-browser <chrome|brave|brave-origin|edge|firefox|zen>"
exit 1
;;
esac
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
# blob:summary=Remove a development environment that was previously installed via blob-install-dev-env.
# blob:args=<ruby|node|bun|deno|go|php|laravel|symfony|python|elixir|phoenix|zig|rust|java|dotnet|ocaml|clojure|scala>
# blob:requires-sudo=true
if [[ -z $1 ]]; then
echo "Usage: blob-remove-dev-env <ruby|node|bun|deno|go|php|laravel|symfony|python|elixir|phoenix|zig|rust|java|dotnet|ocaml|clojure|scala>" >&2
exit 1
fi
remove_php() {
sudo pacman -Rns --noconfirm php composer php-sqlite xdebug 2>/dev/null || true
}
case "$1" in
ruby)
echo -e "Removing Ruby...\n"
mise uninstall ruby --all
mise rm -g ruby
rm -f ~/.gemrc
;;
node)
echo -e "Removing Node.js...\n"
mise uninstall node --all
mise rm -g node
;;
bun)
echo -e "Removing Bun...\n"
mise uninstall bun --all
mise rm -g bun
;;
deno)
echo -e "Removing Deno...\n"
mise uninstall deno --all
mise rm -g deno
;;
go)
echo -e "Removing Go...\n"
mise uninstall go --all
mise rm -g go
;;
php)
echo -e "Removing PHP...\n"
remove_php
;;
laravel)
echo -e "Removing Laravel...\n"
composer global remove laravel/installer 2>/dev/null || true
;;
symfony)
echo -e "Removing Symfony CLI...\n"
sudo pacman -Rns --noconfirm symfony-cli 2>/dev/null || true
;;
python)
echo -e "Removing Python...\n"
mise uninstall python --all
mise rm -g python
rm -rf ~/.local/bin/uv ~/.local/bin/uvx ~/.cargo/bin/uv 2>/dev/null || true
;;
elixir|phoenix)
echo -e "Removing Elixir/Erlang...\n"
mise uninstall elixir --all
mise uninstall erlang --all
mise rm -g elixir
mise rm -g erlang
;;
zig)
echo -e "Removing Zig...\n"
mise uninstall zig --all
mise uninstall zls --all
mise rm -g zig
mise rm -g zls
;;
rust)
echo -e "Removing Rust...\n"
rustup self uninstall -y 2>/dev/null || true
;;
java)
echo -e "Removing Java...\n"
mise uninstall java --all
mise rm -g java
;;
dotnet)
echo -e "Removing .NET...\n"
mise uninstall dotnet --all
mise rm -g dotnet
;;
ocaml)
echo -e "Removing OCaml...\n"
opam switch remove default -y 2>/dev/null || true
rm -rf ~/.opam 2>/dev/null || true
sudo rm -f /usr/local/bin/opam 2>/dev/null || true
;;
clojure)
echo -e "Removing Clojure...\n"
mise uninstall clojure --all
mise rm -g clojure
;;
scala)
echo -e "Removing Scala...\n"
mise uninstall scala --all
mise uninstall scala-cli --all
mise rm -g scala
mise rm -g scala-cli
;;
*)
echo "Unknown environment: $1"
exit 1
;;
esac
echo -e "\nDone!"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# blob:summary=Remove Battle.net, its Proton prefix, installed games, and desktop entry.
set -e
PREFIX="$HOME/Games/battlenet"
# Stop any running Battle.net / wine processes tied to this prefix.
pkill -f "$PREFIX" 2>/dev/null || true
sleep 1
rm -rf "$PREFIX"
rm -f "$HOME/.local/share/applications/battlenet.desktop"
rm -f "$HOME/.cache/blob/Battle.net-Setup.exe"
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
echo
echo "Battle.net and its Proton prefix at $PREFIX have been removed."
if blob-pkg-present umu-launcher; then
echo
if gum confirm "Also remove umu-launcher? It's only used by this command."; then
blob-pkg-drop umu-launcher
fi
fi
PROTON_DIR="$HOME/.local/share/Steam/compatibilitytools.d"
if compgen -G "$PROTON_DIR/GE-Proton*" >/dev/null; then
echo
if gum confirm "Also remove GE-Proton runtimes downloaded by umu?"; then
rm -rf "$PROTON_DIR"/GE-Proton*
rm -rf "$HOME/.local/share/umu"
fi
fi
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# blob:summary=Remove the GeForce NOW Flatpak app and its data.
# blob:group=remove
# blob:name=gaming geforce-now
set -e
if blob-cmd-present flatpak && flatpak info com.nvidia.geforcenow &>/dev/null; then
flatpak uninstall -y --delete-data com.nvidia.geforcenow
fi
echo ""
echo "GeForce NOW removed."
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# blob:summary=Remove Heroic Games Launcher and its game libraries, configs, and caches.
# blob:requires-sudo=true
set -e
blob-pkg-drop heroic-games-launcher-bin
rm -rf \
"$HOME/.config/heroic" \
"$HOME/.local/share/heroic" \
"$HOME/.cache/heroic" \
"$HOME/Games/Heroic"
echo ""
echo "Heroic and its data have been removed."
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# blob:summary=Remove Lutris, Wine, umu-launcher, and all their configs and caches.
# blob:requires-sudo=true
set -e
blob-pkg-drop lutris wine-staging wine-mono wine-gecko winetricks python-protobuf umu-launcher
rm -rf \
"$HOME/.config/lutris" \
"$HOME/.local/share/lutris" \
"$HOME/.cache/lutris" \
"$HOME/.local/share/umu" \
"$HOME/.cache/umu" \
"$HOME/.wine" \
"$HOME/.cache/wine" \
"$HOME/.cache/winetricks"
echo ""
echo "Lutris, Wine, umu-launcher, and their configs have been removed."
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# blob:summary=Remove the Minecraft launcher along with its worlds, mods, and caches.
# blob:requires-sudo=true
set -e
blob-pkg-drop minecraft-launcher
rm -rf \
"$HOME/.minecraft" \
"$HOME/.config/Minecraft Launcher" \
"$HOME/.local/share/minecraft-launcher" \
"$HOME/.cache/minecraft"
echo ""
echo "Minecraft and its data have been removed."
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# blob:summary=Remove RetroArch, all libretro cores, and its config/saves. Leaves ~/Games/roms and ~/Games/bios alone.
# blob:requires-sudo=true
set -e
blob-pkg-drop \
retroarch \
retroarch-assets-glui retroarch-assets-ozone retroarch-assets-xmb \
libretro-beetle-pce libretro-beetle-pce-fast libretro-beetle-psx libretro-beetle-psx-hw libretro-beetle-supergrafx \
libretro-blastem \
libretro-bsnes libretro-bsnes-hd \
libretro-core-info \
libretro-desmume libretro-dolphin libretro-flycast \
libretro-gambatte libretro-genesis-plus-gx \
libretro-kronos \
libretro-mame libretro-melonds libretro-mesen libretro-mesen-s libretro-mgba libretro-mupen64plus-next \
libretro-nestopia \
libretro-overlays \
libretro-parallel-n64 libretro-picodrive libretro-play libretro-ppsspp \
libretro-sameboy libretro-scummvm libretro-shaders-slang libretro-snes9x \
libretro-yabause \
libretro-cap32-git libretro-fbneo-git libretro-uae-git \
libretro-vice-x128-git libretro-vice-x64-git libretro-vice-x64dtv-git libretro-vice-x64sc-git \
libretro-vice-xcbm2-git libretro-vice-xcbm5x0-git libretro-vice-xpet-git \
libretro-vice-xplus4-git libretro-vice-xscpu64-git libretro-vice-xvic-git \
retroarch-joypad-autoconfig-git
rm -rf \
"$HOME/.config/retroarch" \
"$HOME/.local/share/retroarch" \
"$HOME/.cache/retroarch"
echo ""
echo "RetroArch and its cores have been removed."
echo "ROMs and BIOS files at ~/Games/roms and ~/Games/bios were left in place."
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# blob:summary=Remove Steam and all of its game libraries, configs, and caches.
# blob:requires-sudo=true
set -e
blob-pkg-drop steam
rm -rf \
"$HOME/.steam" \
"$HOME/.local/share/Steam" \
"$HOME/.config/steam" \
"$HOME/.cache/steam"
echo ""
echo "Steam and its data have been removed."
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# blob:summary=Remove the Xbox Cloud Gaming web app.
set -e
blob-webapp-remove "Xbox Cloud Gaming"
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# blob:summary=Remove the xpadneo Xbox controller driver and undo its module/blacklist config.
# blob:group=remove
# blob:name=gaming xbox-controllers
# blob:requires-sudo=true
set -e
blob-pkg-drop xpadneo-dkms
sudo rm -f /etc/modprobe.d/blacklist-xpad.conf /etc/modules-load.d/xpadneo.conf
echo ""
echo "Xbox controller support removed. Reboot to fully unload xpadneo and restore xpad."
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# blob:summary=Open HEY webmail and translate mailto links
# blob:args=[url]
url="$1"
web_url="https://app.hey.com"
# Handle mailto: URLs
if [[ $url =~ ^mailto: ]]; then
email=$(echo "$url" | sed 's/mailto://')
web_url="https://app.hey.com/messages/new?to=$email"
fi
exec blob-launch-webapp "$web_url"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# blob:summary=Open Zoom web meetings from browser protocol links
# blob:args=[url]
url="$1"
web_url="https://app.zoom.us/wc/home"
if [[ $url =~ ^zoom(mtg|us):// ]]; then
confno=$(echo "$url" | sed -n 's/.*[?&]confno=\([^&]*\).*/\1/p')
if [[ -n $confno ]]; then
pwd=$(echo "$url" | sed -n 's/.*[?&]pwd=\([^&]*\).*/\1/p')
if [[ -n $pwd ]]; then
web_url="https://app.zoom.us/wc/join/$confno?pwd=$pwd"
else
web_url="https://app.zoom.us/wc/join/$confno"
fi
fi
fi
exec blob-launch-webapp "$web_url"
+241
View File
@@ -0,0 +1,241 @@
#!/bin/bash
# blob:summary=Create a desktop launcher for a web app
# blob:args=[name url icon-url-or-name [custom-exec] [mime-types]]
set -e
ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
safe_icon_name() {
printf '%s\n' "$1" \
| tr '[:upper:]' '[:lower:]' \
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
}
require_plain_name() {
# The name becomes a filename. A slash would turn it into directory levels, so
# the launcher lands somewhere blob-webapp-remove cannot address and the app
# is stuck in the launcher; a leading ../ leaves the applications directory
# altogether. Refuse rather than silently renaming what the user typed -- most
# often it is a URL entered in the name field.
if [[ $1 == */* ]]; then
echo "App name cannot contain '/': $1"
exit 1
fi
}
icon_name_from_ref() {
local ref="$1"
local name
name=$(basename "$ref")
if [[ $name == *.* ]]; then
safe_icon_name "${name%.*}"
else
printf '%s\n' "$name"
fi
}
install_user_icon() {
local source="$1"
local name="$2"
local ext="${source##*.}"
[[ $ext == "$source" ]] && ext="png"
mkdir -p "$ICON_DIR"
cp "$source" "$ICON_DIR/$name.$ext"
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
printf '%s\n' "$name"
}
download_icon() {
curl -fsSL --max-time 10 -o "$2" "$1" 2>/dev/null &&
[[ -s $2 && $(file -b --mime-type "$2") == image/* ]]
}
# Chromium --app= treats javascript:, file:, and data: as a document to
# run. Prefix schemeless input with https as before, then refuse anything
# that is not http(s).
normalize_webapp_url() {
local url=$1
if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
url="https://$url"
fi
printf '%s' "$url"
}
# Raw whitespace must be percent-encoded in a URL. Refuse it before serializing
# the desktop entry; before Exec argument quoting, it also split browser flags
# and additional URLs into separate arguments. Schemes are case-insensitive.
require_http_url() {
local url=$1
if [[ $url =~ [[:space:]] ]]; then
echo "Error: web app URL must not contain whitespace." >&2
exit 1
fi
if [[ ! ${url,,} =~ ^https?:// ]]; then
echo "Error: web app URL must be http or https." >&2
exit 1
fi
}
fetch_site_icon() {
local site_url="$1" dest="$2"
local origin page icon_url
origin=$(sed -E 's|^(https?://[^/]+).*|\1|' <<<"$site_url")
# Prefer the site's own high-res icon (apple-touch-icon is typically 180px+),
# then the well-known path, then Google's favicon service as a last resort.
page=$(curl -fsSL --max-time 5 "$site_url" 2>/dev/null | head -c 100000 | tr '\n' ' ')
icon_url=$(grep -oiE "<link[^>]*rel=[\"'][^\"']*apple-touch-icon[^\"']*[\"'][^>]*>" <<<"$page" |
grep -oiE "href=[\"'][^\"']+" | head -1 | sed -E "s/^href=[\"']//")
case $icon_url in
http://* | https://*) ;;
//*) icon_url="https:$icon_url" ;;
/*) icon_url="$origin$icon_url" ;;
?*) icon_url="$origin/$icon_url" ;;
esac
{ [[ -n $icon_url ]] && download_icon "$icon_url" "$dest"; } ||
download_icon "$origin/apple-touch-icon.png" "$dest" ||
download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest"
}
desktop_string_escape() {
# Desktop Entry "string" value (freedesktop Desktop Entry Spec, "Value types"):
# a raw newline would start a new key line and let a value inject a second
# Exec=. Escape backslash first, then tab/CR/LF and a leading space. Every value
# written into the .desktop file passes through here.
#
# Parameter expansion rather than sed: GNU sed's N auto-prints the pattern space
# and exits at end of input, so a `:a;N;$!ba` slurp skips every following s///
# for a value with no newline in it - which is every value except the injection
# attempt this exists to stop.
local value="$1"
value=${value//\\/\\\\}
value=${value//$'\t'/\\t}
value=${value//$'\r'/\\r}
value=${value//$'\n'/\\n}
[[ $value == " "* ]] && value="\\s${value# }"
printf '%s' "$value"
}
desktop_exec_arg() {
# One Exec argument, double-quoted per the freedesktop Exec spec: inside quotes
# " ` $ \ take a backslash and a literal % becomes %%. Only the default Exec's
# URL needs this; $CUSTOM_EXEC stays a whole command line (file-syntax only).
local escaped
escaped=$(printf '%s' "$1" \
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g')
printf '"%s"' "$escaped"
}
if (( $# < 3 )); then
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
require_plain_name "$APP_NAME"
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
APP_URL=$(normalize_webapp_url "$APP_URL")
require_http_url "$APP_URL"
# Try to fetch the site's icon automatically first.
mkdir -p "$ICON_DIR"
ICON_VALUE=$(safe_icon_name "$APP_NAME")
if fetch_site_icon "$APP_URL" "$ICON_DIR/$ICON_VALUE.png"; then
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
ICON_REF="$ICON_VALUE"
else
ICON_REF=$(gum input --prompt "Icon URL/name> " --placeholder "Could not fetch favicon automatically. Enter PNG icon URL or icon name")
fi
CUSTOM_EXEC=""
MIME_TYPES=""
INTERACTIVE_MODE=true
else
APP_NAME="$1"
APP_URL=$(normalize_webapp_url "$2")
require_http_url "$APP_URL"
ICON_REF="$3"
CUSTOM_EXEC="$4" # Optional custom exec command
MIME_TYPES="$5" # Optional mime types
INTERACTIVE_MODE=false
fi
# Ensure valid execution
if [[ -z $APP_NAME || -z $APP_URL ]]; then
echo "You must set app name and app URL!"
exit 1
fi
require_plain_name "$APP_NAME"
if [[ -z $ICON_REF ]]; then
ICON_VALUE=$(safe_icon_name "$APP_NAME")
mkdir -p "$ICON_DIR"
if ! fetch_site_icon "$APP_URL" "$ICON_DIR/$ICON_VALUE.png"; then
echo "Error: Failed to download icon."
exit 1
fi
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
elif [[ $ICON_REF =~ ^https?:// ]]; then
ICON_VALUE=$(safe_icon_name "$APP_NAME")
mkdir -p "$ICON_DIR"
if ! download_icon "$ICON_REF" "$ICON_DIR/$ICON_VALUE.png"; then
echo "Error: Failed to download icon."
exit 1
fi
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
elif [[ -f $ICON_REF ]]; then
ICON_VALUE=$(install_user_icon "$ICON_REF" "$(safe_icon_name "$APP_NAME")")
else
# Bundled Blob icons are package-owned under /usr/share/icons/hicolor.
# Accept either "HEY" or the historical "HEY.png" argument form.
ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
fi
# Default Exec quotes the URL as one Exec-spec argument; the whole line then gets
# the file-syntax escaping below (unescaped first at read time per spec, so the
# layers compose). $CUSTOM_EXEC is a full command line, so it gets file-syntax only.
if [[ -n $CUSTOM_EXEC ]]; then
EXEC_COMMAND=$CUSTOM_EXEC
else
EXEC_COMMAND="blob-launch-webapp $(desktop_exec_arg "$APP_URL")"
fi
# Create application .desktop file
DESKTOP_DIR="$HOME/.local/share/applications"
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
mkdir -p "$DESKTOP_DIR"
name_field=$(desktop_string_escape "$APP_NAME")
exec_field=$(desktop_string_escape "$EXEC_COMMAND")
icon_field=$(desktop_string_escape "$ICON_VALUE")
cat >"$DESKTOP_FILE" <<EOF
[Desktop Entry]
Version=1.0
Name=$name_field
Comment=$name_field
Exec=$exec_field
Terminal=false
Type=Application
Icon=$icon_field
StartupNotify=true
EOF
# Add mime types if provided
if [[ -n $MIME_TYPES ]]; then
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
fi
chmod +x "$DESKTOP_FILE"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo -e "You can now find $APP_NAME using the app launcher (SUPER + SPACE)\n"
fi
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# blob:summary=Remove a web app desktop launcher
# blob:args=[name]
set -e
ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
DESKTOP_DIR="$HOME/.local/share/applications/"
# Always index the launchers, so removal deletes the file that was found rather
# than a path rebuilt from the displayed name. Installs predating the name
# validation could nest the launcher inside directories, and those are exactly
# the ones a reconstructed path cannot reach.
WEB_APP_PATHS=()
while IFS= read -r -d '' file; do
if grep -q '^Exec=.*\(blob-launch-webapp\|blob-webapp-handler\).*' "$file"; then
WEB_APPS+=("$(basename "${file%.desktop}")")
WEB_APP_PATHS+=("$file")
fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
# The launcher matching a chosen name, or empty when nothing was indexed under
# it (an app removed between the scan and the pick, say).
path_for_web_app() {
local wanted="$1" i
for i in "${!WEB_APPS[@]}"; do
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
printf '%s\n' "${WEB_APP_PATHS[$i]}"
return 0
fi
done
}
if (( $# == 0 )); then
if ((${#WEB_APPS[@]})); then
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
APP_NAME=$(blob-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
else
echo "No web apps to remove."
exit 1
fi
else
APP_NAME="$*"
fi
if [[ -z $APP_NAME ]]; then
echo "You must select a web app to remove."
exit 1
fi
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
desktop_file=$(path_for_web_app "$APP_NAME")
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
if [[ ${BLOB_REMOVE_NOTIFY:-true} != "false" ]]; then
blob-notify-send -g  "Web app removed" "$APP_NAME"
fi
update-desktop-database "$DESKTOP_DIR" &>/dev/null
+1591
View File
File diff suppressed because it is too large Load Diff