Restore the browser, gaming, development, and Windows VM menus
This commit is contained in:
Executable
+50
@@ -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
|
||||
Executable
+196
@@ -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
|
||||
Executable
+39
@@ -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
|
||||
Executable
+72
@@ -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"
|
||||
Executable
+87
@@ -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
|
||||
Executable
+28
@@ -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
|
||||
Executable
+29
@@ -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
|
||||
Executable
+155
@@ -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
|
||||
Executable
+28
@@ -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
|
||||
Executable
+88
@@ -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
|
||||
Executable
+20
@@ -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
|
||||
Executable
+30
@@ -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[@]}"
|
||||
Executable
+12
@@ -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 &
|
||||
Executable
+23
@@ -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 &
|
||||
Executable
+85
@@ -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 &
|
||||
Executable
+15
@@ -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 &
|
||||
Executable
+12
@@ -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 &
|
||||
Executable
+39
@@ -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."
|
||||
Executable
+48
@@ -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"
|
||||
Executable
+15
@@ -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"
|
||||
Executable
+13
@@ -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}"
|
||||
Executable
+18
@@ -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
|
||||
Executable
+70
@@ -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
|
||||
Executable
+113
@@ -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!"
|
||||
Executable
+35
@@ -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
|
||||
Executable
+14
@@ -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."
|
||||
Executable
+17
@@ -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."
|
||||
Executable
+21
@@ -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."
|
||||
Executable
+17
@@ -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."
|
||||
Executable
+37
@@ -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."
|
||||
Executable
+17
@@ -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."
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# blob:summary=Remove the Xbox Cloud Gaming web app.
|
||||
|
||||
set -e
|
||||
|
||||
blob-webapp-remove "Xbox Cloud Gaming"
|
||||
Executable
+15
@@ -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."
|
||||
Executable
+15
@@ -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"
|
||||
Executable
+23
@@ -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"
|
||||
Executable
+241
@@ -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
|
||||
Executable
+61
@@ -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
|
||||
Executable
+1591
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user