Compare commits

..
26 Commits
Author SHA1 Message Date
SirBlob 747b7ebdb7 Install the boot splash theme and set up GRUB without Limine 2026-09-21 02:10:47 +00:00
SirBlob 5858eec50d Draw the About logo and bar icon as a cup of chai 2026-09-21 02:10:40 +00:00
SirBlob 8d8dee2342 Theme fastfetch from the active palette 2026-09-21 02:10:39 +00:00
SirBlob 5372c6ea29 Centre the quick settings panel under the clock 2026-09-21 02:10:29 +00:00
SirBlob 740586611a Stop alerting when a theme ships no backgrounds 2026-09-21 02:10:29 +00:00
SirBlob 3211350435 Stop the theme menu drawing theme names in the icon cell 2026-09-21 02:10:28 +00:00
SirBlob 933f3a9efd Redesign the flats palette around a proper surface ramp 2026-09-21 02:10:19 +00:00
SirBlob 06446f36f8 Make cd jump to frequently used directories with zoxide 2026-09-21 02:10:18 +00:00
SirBlob 49d2f0e41e Put the prompt directory and the input on one line 2026-09-21 02:10:17 +00:00
SirBlob c0c4eadfe1 Let the screensaver effects draw in color 2026-09-21 02:10:16 +00:00
SirBlob 90c22b4e0c Run every terminal launch through one dispatcher with fallbacks 2026-09-21 02:10:01 +00:00
SirBlob d316308bf5 Install the lock screen PAM stack and replace the missing screensaver effects 2026-09-21 01:07:34 +00:00
SirBlob 64afc07250 Start the starship prompt and build it from the active theme 2026-09-21 01:07:27 +00:00
SirBlob e74c9c5bc1 Stop btop painting the meter track in the selection color 2026-09-21 01:07:27 +00:00
SirBlob a9b9ba6ba4 Bind monospace to a Nerd Font and keep panels clear of the bar 2026-09-21 01:07:16 +00:00
SirBlob deeacbe580 Theme GTK apps from the active palette 2026-09-21 00:49:40 +00:00
SirBlob bfba908ef4 List wallpapers through the symlinked wallpapers folder 2026-09-21 00:49:35 +00:00
SirBlob ef02250a8f Show the update icon only when updates are pending 2026-09-21 00:49:35 +00:00
SirBlob e5c20e0a46 Add the missing terminal launcher behind Super and Enter 2026-09-21 00:49:35 +00:00
SirBlob 737d601bca Pick a renderer from the GPU and fall back to software rendering 2026-09-21 00:49:29 +00:00
SirBlob 00803e59a4 Drop the boot splash and mask plymouth so the console stays reachable 2026-09-20 23:18:50 +00:00
SirBlob 3fbdd754c1 Find systemd units on disk and report the packages the desktop needs 2026-09-20 23:13:32 +00:00
SirBlob 06204a357a Put the blob command on PATH and reapply the default theme when it is missing 2026-09-20 23:07:25 +00:00
SirBlob 2f29cb8bae Install the maintained pywal fork and skip packages already present 2026-09-20 23:02:17 +00:00
SirBlob 52e747fe3e Connect the generated theme files to btop, Neovim, and kitty 2026-09-20 13:31:43 -04:00
SirBlob f00d09eb02 Fix the black screen on first boot and the installer's missing directories 2026-09-20 13:18:03 -04:00
65 changed files with 1435 additions and 356 deletions
+15 -13
View File
@@ -74,24 +74,26 @@ It runs these steps, in this order, and every one of them is idempotent:
| Step | What it does | | Step | What it does |
| --- | --- | | --- | --- |
| 1. `BLOB_PATH` | symlinks `~/.local/share/blob` to this checkout | | 1. Directories | creates the 19 directories the shell and scripts expect, before anything writes into them |
| 2. Directories | creates the 19 directories the shell and scripts expect | | 2. `BLOB_PATH` | symlinks `~/.local/share/blob` to this checkout |
| 3. Packages | installs what is missing from `packages/`, bootstrapping `yay` first on a machine that has no AUR helper | | 3. Packages | installs what is missing from `packages/`, bootstrapping `yay` first on a machine that has no AUR helper |
| 4. Config | copies `hypr/`, the themed app configs, `blob/shell.json`, the uwsm environment, and the icon font into `~/.config` | | 4. Config | copies `hypr/`, the themed app configs, `blob/shell.json`, the uwsm environment, and the icon font into `~/.config`, and writes `/etc/profile.d/blob.sh` so a login shell finds the `blob` command |
| 5. User units | installs the session services into `~/.config/systemd/user` and enables them | | 5. First theme | links `~/wallpapers` to the checkout, applies `flats` headless, picks a background, and links the btop and Neovim theme files, so the first login is not a blank desktop |
| 6. systemd drop-ins | writes the shutdown timeouts, oomd thresholds, plocate prune paths, and sleep hooks under `/etc` | | 6. User units | installs the session services into `~/.config/systemd/user` and enables them |
| 7. System services | enables NetworkManager, resolved, bluetooth, cups, avahi, docker, power-profiles-daemon, and oomd, and masks `NetworkManager-wait-online` | | 7. systemd drop-ins | writes the shutdown timeouts, oomd thresholds, plocate prune paths, and sleep hooks under `/etc` |
| 8. Login screen | installs the session entry and the SDDM config, enables `sddm.service`, and sets the default target to `graphical.target` | | 8. System services | enables NetworkManager, resolved, bluetooth, cups, avahi, docker, power-profiles-daemon, and oomd, and masks `NetworkManager-wait-online` |
| 9. Launch | starts the login screen, so the desktop comes up without a reboot | | 9. Login screen | installs the session entry and the SDDM config, enables `sddm.service`, and sets the default target to `graphical.target` |
| 10. Launch | starts the login screen, so the desktop comes up without a reboot |
Step 8 is the one a plain Arch install cannot do without: SDDM's stock Wayland Step 9 is the one a plain Arch install cannot do without: SDDM's stock Wayland
greeter runs on `weston`, which Blob does not install, so the greeter is pointed greeter runs on `weston`, which Blob does not install, so the greeter is pointed
at Hyprland instead. It also reports any leftover drop-in that autologs into a at Hyprland with a Lua config, the only format Hyprland still reads. It also
session that is not installed, which is what a black screen with no login prompt reports any leftover drop-in that autologs into a session that is not installed,
usually is. [install.md](docs/install.md) covers each step in full. which is the other thing a black screen with no login prompt usually is.
[install.md](docs/install.md) covers each step in full.
To back out, `./uninstall.sh` removes the session entry, the config, the user To back out, `./uninstall.sh` removes the session entry, the config, the user
services, and the symlink, restoring any `.bak` the installer made. The services, and the symlinks, restoring any `.bak` the installer made. The
checkout, the wallpapers, and the themes stay where they are. checkout, the wallpapers, and the themes stay where they are.
## Regenerating docs ## Regenerating docs
+128 -10
View File
@@ -3,7 +3,9 @@
# blob-boot - boot splash and bootloader management. # blob-boot - boot splash and bootloader management.
# #
# blob-boot [image] Apply a Plymouth boot splash image (default action) # blob-boot [image] Apply a Plymouth boot splash image (default action)
# blob-boot grub Migrate from Limine back to GRUB, detecting every OS # blob-boot grub Install GRUB and detect every other OS on the machine.
# Migrates off Limine when it is there; on a machine
# that never had it, only GRUB is installed
# --purge removes Limine in the same run instead of # --purge removes Limine in the same run instead of
# keeping it for one fallback boot # keeping it for one fallback boot
# blob-boot detect Rescan for other operating systems, rebuild the menu # blob-boot detect Rescan for other operating systems, rebuild the menu
@@ -16,8 +18,12 @@
set -euo pipefail set -euo pipefail
DEFAULT_IMAGE="$HOME/Documents/dotfiles/branding/boot_flash.png" BLOB_PATH="${BLOB_PATH:-$HOME/.local/share/blob}"
PLYMOUTH_LOGO="/usr/share/plymouth/themes/blob/logo.png" DEFAULT_IMAGE="$BLOB_PATH/branding/boot_flash.png"
THEME_SOURCE="$BLOB_PATH/default/plymouth"
THEME_DIR="/usr/share/plymouth/themes/blob"
PLYMOUTH_LOGO="$THEME_DIR/logo.png"
MKINITCPIO_DROPIN="/etc/mkinitcpio.conf.d/blob-plymouth.conf"
log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
info() { printf ' %s\n' "$*"; } info() { printf ' %s\n' "$*"; }
@@ -43,17 +49,24 @@ apply_splash() {
fi fi
[ -f "$image_path" ] || die "File '$image_path' does not exist." [ -f "$image_path" ] || die "File '$image_path' does not exist."
[ -d "$(dirname "$PLYMOUTH_LOGO")" ] || die "Plymouth blob theme is not installed." command -v plymouth-set-default-theme >/dev/null \
|| die "The 'plymouth' package is not installed. Install it with: sudo pacman -S plymouth"
log "Applying boot splash image: $image_path" log "Applying boot splash image: $image_path"
require_sudo require_sudo
sudo cp "$image_path" "$PLYMOUTH_LOGO" install_theme "$image_path"
sudo chmod 644 "$PLYMOUTH_LOGO" install_initramfs_hook
enable_plymouth_units
log "Setting the default Plymouth theme"
sudo plymouth-set-default-theme blob
log "Rebuilding initramfs" log "Rebuilding initramfs"
rebuild_initramfs rebuild_initramfs
report_kernel_cmdline
# Under GRUB the menu references the initramfs by path, so it only needs # Under GRUB the menu references the initramfs by path, so it only needs
# regenerating if the file set changed - but it is cheap and keeps the menu # regenerating if the file set changed - but it is cheap and keeps the menu
# honest after a kernel or os-prober change. # honest after a kernel or os-prober change.
@@ -65,6 +78,80 @@ apply_splash() {
ok "Boot splash successfully updated!" ok "Boot splash successfully updated!"
} }
install_theme() {
local image_path=$1
[ -d "$THEME_SOURCE" ] || die "Theme source '$THEME_SOURCE' is missing from the checkout."
sudo install -d -m 0755 "$THEME_DIR"
sudo install -m 0644 "$THEME_SOURCE/blob.plymouth" "$THEME_DIR/blob.plymouth"
sudo install -m 0644 "$THEME_SOURCE/blob.script" "$THEME_DIR/blob.script"
sudo install -m 0644 "$image_path" "$PLYMOUTH_LOGO"
info "installed the blob theme to $THEME_DIR"
}
# A drop-in rather than an edit of HOOKS: mkinitcpio.conf.d is sourced after
# the main config, so the hook can be inserted without rewriting a line the
# user or another tool owns. plymouth has to come after udev, which starts the
# devices it draws on.
install_initramfs_hook() {
sudo install -d -m 0755 "$(dirname "$MKINITCPIO_DROPIN")"
sudo tee "$MKINITCPIO_DROPIN" >/dev/null <<'DROPIN'
# Written by blob-boot. Adds the plymouth hook after udev, once.
if [[ " ${HOOKS[*]} " != *" plymouth "* ]]; then
_hooks=()
for _hook in "${HOOKS[@]}"; do
_hooks+=("$_hook")
[[ $_hook == "udev" || $_hook == "systemd" ]] && _hooks+=("plymouth")
done
HOOKS=("${_hooks[@]}")
unset _hooks _hook
fi
DROPIN
info "wrote $MKINITCPIO_DROPIN"
}
# install.sh masks these so an unconfigured plymouth cannot hold the console and
# leave the machine with no greeter and no TTY. A configured one needs them: if
# plymouth-quit-wait stays masked the splash never hands the display back.
enable_plymouth_units() {
local unit
for unit in plymouth-start.service plymouth-quit-wait.service; do
if [ "$(systemctl is-enabled "$unit" 2>/dev/null)" = "masked" ]; then
sudo systemctl unmask "$unit" >/dev/null
info "unmasked $unit"
fi
done
}
# plymouth only draws when the kernel was told to be quiet and to splash. The
# bootloader owns that line, so report rather than rewrite anything but GRUB's.
report_kernel_cmdline() {
local params="quiet splash vt.global_cursor_default=0"
if grep -qw splash /proc/cmdline; then
info "kernel command line already carries 'splash'"
return 0
fi
log "One step left: the kernel command line"
if using_grub; then
warn "GRUB is in charge. Add to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub:"
warn " $params"
warn "then: sudo grub-mkconfig -o /boot/grub/grub.cfg"
elif [ -f /boot/limine.conf ]; then
warn "Limine is in charge. Add to the cmdline in /boot/limine.conf:"
warn " $params"
elif [ -d /boot/loader/entries ]; then
warn "systemd-boot is in charge. Add to the options line in /boot/loader/entries/*.conf:"
warn " $params"
else
warn "Add these to your bootloader's kernel command line:"
warn " $params"
fi
warn "Without them the splash is drawn over by kernel messages."
}
# Call mkinitcpio directly: limine-mkinitcpio-hook installs a wrapper at # Call mkinitcpio directly: limine-mkinitcpio-hook installs a wrapper at
# /usr/local/bin/mkinitcpio that stops to ask whether to rebuild Limine # /usr/local/bin/mkinitcpio that stops to ask whether to rebuild Limine
# entries, which is useless in a script. While Limine is still the active # entries, which is useless in a script. While Limine is still the active
@@ -82,6 +169,26 @@ using_grub() {
[ -f /boot/grub/grub.cfg ] && [ -f /boot/EFI/GRUB/grubx64.efi ] [ -f /boot/grub/grub.cfg ] && [ -f /boot/EFI/GRUB/grubx64.efi ]
} }
# Everything Limine-specific below is skipped on a machine that never had it:
# reclaiming its space, removing its hooks, and rewriting the mkinitcpio preset
# its hooks needed. Rewriting that preset elsewhere would drop the fallback
# initramfs for no reason.
limine_present() {
pacman -Qq limine &>/dev/null || [ -f /boot/limine.conf ] || [ -d /boot/EFI/limine ]
}
install_grub_packages() {
local missing=()
command -v grub-install >/dev/null || missing+=(grub)
command -v os-prober >/dev/null || missing+=(os-prober)
(( ${#missing[@]} == 0 )) && return 0
log "Installing ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}" \
|| die "Could not install: ${missing[*]}"
}
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Limine -> GRUB migration # Limine -> GRUB migration
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@@ -92,10 +199,9 @@ migrate_to_grub() {
[ -d /sys/firmware/efi ] || die "Not booted in UEFI mode; this script assumes UEFI." [ -d /sys/firmware/efi ] || die "Not booted in UEFI mode; this script assumes UEFI."
mountpoint -q /boot || die "/boot is not mounted." mountpoint -q /boot || die "/boot is not mounted."
command -v grub-install >/dev/null || die "The 'grub' package is not installed."
command -v os-prober >/dev/null || die "The 'os-prober' package is not installed."
require_sudo require_sudo
install_grub_packages
local machine_id esp_uuid backup local machine_id esp_uuid backup
machine_id=$(</etc/machine-id) machine_id=$(</etc/machine-id)
@@ -111,8 +217,14 @@ migrate_to_grub() {
sudo efibootmgr -v | sudo tee "$backup/efibootmgr-before.txt" >/dev/null 2>&1 || true sudo efibootmgr -v | sudo tee "$backup/efibootmgr-before.txt" >/dev/null 2>&1 || true
info "saved." info "saved."
reclaim_esp_space "$machine_id" "$backup" if limine_present; then
restore_standard_initramfs reclaim_esp_space "$machine_id" "$backup"
restore_standard_initramfs
else
info "Limine is not installed; leaving the initramfs layout alone"
log "Building the initramfs"
rebuild_initramfs
fi
configure_grub_scripts configure_grub_scripts
install_grub "$esp_uuid" install_grub "$esp_uuid"
fix_failing_boot_units fix_failing_boot_units
@@ -529,6 +641,12 @@ set_boot_order() {
# Delete every trace of Limine and hand its EFI fallback path to GRUB. # Delete every trace of Limine and hand its EFI fallback path to GRUB.
purge_limine() { purge_limine() {
if ! limine_present; then
log "Claiming the removable EFI fallback path for GRUB"
sudo grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable --recheck
return 0
fi
log "Removing Limine" log "Removing Limine"
drop_limine_pkgs limine limine-mkinitcpio-hook limine-snapper-sync drop_limine_pkgs limine limine-mkinitcpio-hook limine-snapper-sync
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# blob:summary=Detect whether a DRM device with an accelerated driver is present.
drm_path="${BLOB_DRM_PATH:-/sys/class/drm}"
accelerated_drivers=(
amdgpu
i915
nouveau
nvidia
nvidia-drm
radeon
xe
)
shopt -s nullglob
for card in "$drm_path"/card[0-9]*; do
[[ -L $card/device/driver ]] || continue
driver=$(basename "$(readlink -f "$card/device/driver")")
for accelerated in "${accelerated_drivers[@]}"; do
[[ $driver == "$accelerated" ]] && exit 0
done
done
exit 1
+4 -2
View File
@@ -12,9 +12,11 @@ LOGO_FILE="$HOME/.config/blob/branding/about.txt"
FIT_FILE="$HOME/.local/state/blob/windows/about.fit" FIT_FILE="$HOME/.local/state/blob/windows/about.fit"
# A user-level fastfetch config can relocate or restyle the logo in ways this # A user-level fastfetch config can relocate or restyle the logo in ways this
# measurement cannot see, so leave sizing to the float rule in that case. # measurement cannot see, so leave sizing to the float rule in that case. Blob's
# own config is the symlink the installer makes into the current theme, and the
# measurements below are written against it.
custom_fastfetch_config() { custom_fastfetch_config() {
[[ -f $HOME/.config/fastfetch/config.jsonc ]] [[ -f $HOME/.config/fastfetch/config.jsonc && ! -L $HOME/.config/fastfetch/config.jsonc ]]
} }
# wc -L counts display columns only in a UTF-8 locale. A session that never set # wc -L counts display columns only in a UTF-8 locale. A session that never set
+1 -1
View File
@@ -10,4 +10,4 @@ source blob-restart-gum
cmd="$*" cmd="$*"
presentation_script="blob-show-logo; $cmd; if (( \$? != 130 )); then blob-show-done; fi" presentation_script="blob-show-logo; $cmd; if (( \$? != 130 )); then blob-show-done; fi"
exec setsid uwsm-app -- xdg-terminal-exec --app-id=org.blob.terminal --title=Blob -e bash -c "$presentation_script" exec setsid uwsm-app -- blob-terminal-exec --app-id=org.blob.terminal --title=Blob -e bash -c "$presentation_script"
+3 -2
View File
@@ -2,7 +2,8 @@
# blob:summary=Launch the Blob screensaver in the default terminal on the system with the correct font configuration. # blob:summary=Launch the Blob screensaver in the default terminal on the system with the correct font configuration.
if blob-cmd-missing ttfx; then if ! blob-screensaver-effect >/dev/null; then
blob-notify-send -g ✋ "Screensaver needs ttfx or tte" "Install python-terminaltexteffects."
exit 1 exit 1
fi fi
@@ -15,7 +16,7 @@ if blob-toggle-enabled screensaver-off && [[ $1 != "force" ]]; then
fi fi
focused=$(blob-hypr-monitor-focused) focused=$(blob-hypr-monitor-focused)
terminal=$(xdg-terminal-exec --print-id) terminal=$(blob-terminal-exec --print-id)
case $terminal in case $terminal in
*Alacritty* | *ghostty* | *foot* | *kitty*) ;; *Alacritty* | *ghostty* | *foot* | *kitty*) ;;
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# blob:summary=Launch the default terminal
# blob:args=[command [args...]]
exec setsid uwsm-app -- blob-terminal-exec "$@"
+1 -1
View File
@@ -10,4 +10,4 @@ else
APP_ID="org.blob.$(basename $1)" APP_ID="org.blob.$(basename $1)"
fi fi
exec setsid uwsm-app -- xdg-terminal-exec --app-id=$APP_ID -e "$1" "${@:2}" exec setsid uwsm-app -- blob-terminal-exec --app-id=$APP_ID -e "$1" "${@:2}"
+1 -1
View File
@@ -67,7 +67,7 @@ desktop_name="${desktop_file##*/}"
desktop_name="${desktop_name%.desktop}" desktop_name="${desktop_name%.desktop}"
exec_line="$(sed -n 's/^Exec=//p' "$desktop_file" | head -1)" exec_line="$(sed -n 's/^Exec=//p' "$desktop_file" | head -1)"
if [[ $exec_line =~ (^|[[:space:]])(\$TERMINAL|xdg-terminal-exec)[[:space:]].*-e([[:space:]]|$) ]]; then if [[ $exec_line =~ (^|[[:space:]])(\$TERMINAL|xdg-terminal-exec|blob-terminal-exec)[[:space:]].*-e([[:space:]]|$) ]]; then
BLOB_REMOVE_NOTIFY=false blob-tui-remove "$desktop_name" BLOB_REMOVE_NOTIFY=false blob-tui-remove "$desktop_name"
exit 0 exit 0
fi fi
+28 -5
View File
@@ -2,13 +2,37 @@
# blob:summary=Run the Blob screensaver using random effects from TTE. # blob:summary=Run the Blob screensaver using random effects from TTE.
effect_command=$(blob-screensaver-effect) || exit 1
effect_args() {
local input="$HOME/.config/blob/branding/screensaver.txt"
if [[ $effect_command == "ttfx" ]]; then
printf '%s\n' -i "$input" --frame-rate 120 --canvas-width 0 --canvas-height 0 \
--reuse-canvas --anchor-canvas c --anchor-text c --random-effect --no-eol \
--no-restore-cursor
return 0
fi
local effects=(
beams binarypath blackhole bouncyballs bubbles burn colorshift crumble
decrypt errorcorrect expand fireworks middleout orbittingvolley overflow
pour print rain randomsequence rings scattered slice slide spotlights
spray swarm sweep synthgrid unstable vhstape waves wipe
)
printf '%s\n' -i "$input" --frame-rate 120 --canvas-width 0 --canvas-height 0 \
--anchor-canvas c --anchor-text c \
"${effects[RANDOM % ${#effects[@]}]}"
}
screensaver_in_focus() { screensaver_in_focus() {
hyprctl activewindow -j | jq -e '.class == "org.blob.screensaver"' >/dev/null 2>&1 hyprctl activewindow -j | jq -e '.class == "org.blob.screensaver"' >/dev/null 2>&1
} }
exit_screensaver() { exit_screensaver() {
hyprctl eval 'hl.config({ cursor = { invisible = false } })' &>/dev/null || hyprctl keyword cursor:invisible false &>/dev/null || true hyprctl eval 'hl.config({ cursor = { invisible = false } })' &>/dev/null || hyprctl keyword cursor:invisible false &>/dev/null || true
pkill -x ttfx 2>/dev/null pkill -x "$effect_command" 2>/dev/null
pkill -f '[o]rg.blob.screensaver' 2>/dev/null pkill -f '[o]rg.blob.screensaver' 2>/dev/null
exit 0 exit 0
} }
@@ -36,11 +60,10 @@ wait_for_terminal_resize() {
wait_for_terminal_resize wait_for_terminal_resize
while true; do while true; do
ttfx -i ~/.config/blob/branding/screensaver.txt \ mapfile -t effect_arguments < <(effect_args)
--frame-rate 120 --canvas-width 0 --canvas-height 0 --reuse-canvas --anchor-canvas c --anchor-text c\ "$effect_command" "${effect_arguments[@]}" &
--random-effect --no-eol --no-restore-cursor &
while pgrep -t "${tty#/dev/}" -x ttfx >/dev/null; do while pgrep -t "${tty#/dev/}" -x "$effect_command" >/dev/null; do
if read -n1 -t 1 || ! screensaver_in_focus; then if read -n1 -t 1 || ! screensaver_in_focus; then
exit_screensaver exit_screensaver
fi fi
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# blob:summary=Print the terminal text effects command the screensaver can use
# blob:hidden=true
for candidate in ttfx tte; do
if command -v "$candidate" &>/dev/null; then
printf '%s\n' "$candidate"
exit 0
fi
done
exit 1
+4 -3
View File
@@ -20,7 +20,8 @@ if pgrep -x "1password" >/dev/null && blob-cmd-present 1password; then
fi fi
# Avoid running screensaver when locked # Avoid running screensaver when locked
pkill -x ttfx 2>/dev/null || true effect_command=$(blob-screensaver-effect 2>/dev/null) && {
# ttfx handles SIGTERM asynchronously, so keep its terminal alive until it exits. pkill -x "$effect_command" 2>/dev/null || true
timeout 1s pidwait -x ttfx 2>/dev/null || true timeout 1s pidwait -x "$effect_command" 2>/dev/null || true
}
pkill -f '[o]rg.blob.screensaver' 2>/dev/null || true pkill -f '[o]rg.blob.screensaver' 2>/dev/null || true
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# blob:summary=Run a command in the default terminal
# blob:args=[--print-id] [--app-id=<app-id>] [--title=<title>] [-e] <command> [args...]
# blob:hidden=true
terminal=""
for candidate in xdg-terminal-exec foot kitty; do
if command -v "$candidate" &>/dev/null; then
terminal=$candidate
break
fi
done
if [[ -z $terminal ]]; then
blob-notify-send "No terminal installed" "Install xdg-terminal-exec, foot or kitty."
exit 1
fi
if [[ $terminal == "xdg-terminal-exec" ]]; then
exec xdg-terminal-exec "$@"
fi
app_id=""
title=""
command_args=()
while (( $# > 0 )); do
case "$1" in
--print-id)
printf '%s\n' "$terminal"
exit 0
;;
--app-id=*)
app_id=${1#--app-id=}
shift
;;
--title=*)
title=${1#--title=}
shift
;;
-e)
shift
command_args+=("$@")
break
;;
*)
command_args+=("$1")
shift
;;
esac
done
launch=("$terminal")
case "$terminal" in
foot)
[[ -n $app_id ]] && launch+=("--app-id=$app_id")
;;
kitty)
[[ -n $app_id ]] && launch+=("--class=$app_id")
;;
esac
[[ -n $title ]] && launch+=("--title=$title")
exec "${launch[@]}" "${command_args[@]}"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# blob:summary=Apply the current Blob theme's light or dark preference to GTK apps
# blob:hidden=true
blob-cmd-present gsettings || exit 0
colors_toml=$HOME/.local/state/blob/current/theme/colors.toml
[[ -f $colors_toml ]] || exit 0
mode=$(blob-theme-color --file "$colors_toml" mode)
if [[ $mode == "light" ]]; then
color_scheme="prefer-light"
gtk_theme="Adwaita"
else
color_scheme="prefer-dark"
gtk_theme="Adwaita-dark"
fi
gsettings set org.gnome.desktop.interface color-scheme "$color_scheme" 2>/dev/null || true
gsettings set org.gnome.desktop.interface gtk-theme "$gtk_theme" 2>/dev/null || true
+3 -3
View File
@@ -21,7 +21,7 @@ current_mode() {
} }
list_theme_rows() { list_theme_rows() {
printf 'Dynamic (from wallpaper)\tblob-dynamic\n' printf '\tDynamic (from wallpaper)\tblob-dynamic\n'
{ {
find "$user_themes_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null find "$user_themes_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null
@@ -30,8 +30,8 @@ list_theme_rows() {
[ -f "$theme_dir/colors.toml" ] || continue [ -f "$theme_dir/colors.toml" ] || continue
slug=$(basename "$theme_dir") slug=$(basename "$theme_dir")
[ "$slug" = "blob-dynamic" ] && continue [ "$slug" = "blob-dynamic" ] && continue
printf '%s\t%s\n' "$(prettify "$slug")" "$slug" printf '\t%s\t%s\n' "$(prettify "$slug")" "$slug"
done | sort -u -t$'\t' -k2,2 done | sort -u -t$'\t' -k3,3
} }
print_current_colors() { print_current_colors() {
+1 -1
View File
@@ -108,7 +108,6 @@ set_theme_background() {
local new_background new_background_snapshot local new_background new_background_snapshot
if ! choose_theme_background; then if ! choose_theme_background; then
blob-notify-send "No background was found for theme" -t 2000
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
return return
fi fi
@@ -321,6 +320,7 @@ post_theme_commands=(
blob-restart-btop blob-restart-btop
blob-theme-foot blob-theme-foot
blob-theme-browser blob-theme-browser
blob-theme-gtk
) )
if [[ $THEME_HEADLESS != "1" ]]; then if [[ $THEME_HEADLESS != "1" ]]; then
+1
View File
@@ -203,6 +203,7 @@ add_template_value() {
if [[ $value =~ ^#[0-9A-Fa-f]{6}$ ]]; then if [[ $value =~ ^#[0-9A-Fa-f]{6}$ ]]; then
rgb=$(hex_to_rgb "$value") rgb=$(hex_to_rgb "$value")
printf 's|{{ %s_rgb }}|%s|g\n' "$key" "$rgb" >>"$sed_script" printf 's|{{ %s_rgb }}|%s|g\n' "$key" "$rgb" >>"$sed_script"
printf 's|{{ %s_ansi }}|38;2;%s|g\n' "$key" "${rgb//,/;}" >>"$sed_script"
fi fi
} }
+1 -1
View File
@@ -84,7 +84,7 @@ cat >"$DESKTOP_FILE" <<EOF
Version=1.0 Version=1.0
Name=$APP_NAME Name=$APP_NAME
Comment=$APP_NAME Comment=$APP_NAME
Exec=xdg-terminal-exec --app-id=$APP_CLASS -e $APP_EXEC Exec=blob-terminal-exec --app-id=$APP_CLASS -e $APP_EXEC
Terminal=false Terminal=false
Type=Application Type=Application
Icon=$ICON_VALUE Icon=$ICON_VALUE
+1 -1
View File
@@ -12,7 +12,7 @@ DESKTOP_DIR="$HOME/.local/share/applications/"
if (( $# == 0 )); then if (( $# == 0 )); then
# Find all TUIs # Find all TUIs
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
if grep -qE '^Exec=.*(\$TERMINAL|xdg-terminal-exec).*-e' "$file"; then if grep -qE '^Exec=.*(\$TERMINAL|xdg-terminal-exec|blob-terminal-exec).*-e' "$file"; then
TUIS+=("$(basename "${file%.desktop}")") TUIS+=("$(basename "${file%.desktop}")")
fi fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0) done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
+1 -1
View File
@@ -12,7 +12,7 @@ echo "Scanning for TUIs in $APP_DIR..."
tui_desktop_files=() tui_desktop_files=()
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
if grep -q "Exec=xdg-terminal-exec --app-id=TUI\." "$file" 2>/dev/null; then if grep -qE "Exec=(xdg|blob)-terminal-exec --app-id=TUI\." "$file" 2>/dev/null; then
tui_desktop_files+=("$file") tui_desktop_files+=("$file")
fi fi
done < <(find "$APP_DIR" -maxdepth 1 -name "*.desktop" -print0 2>/dev/null) done < <(find "$APP_DIR" -maxdepth 1 -name "*.desktop" -print0 2>/dev/null)
+5
View File
@@ -37,6 +37,11 @@ else
fi fi
fi fi
if ! command -v wal >/dev/null 2>&1; then
echo "Error: 'wal' is not installed. Install it with: yay -S python-pywal16"
exit 1
fi
echo "Extracting colors using Pywal..." echo "Extracting colors using Pywal..."
wal -i "$IMAGE_PATH" -n -q 2> >(grep -v "deprecated in IMv7" >&2) wal -i "$IMAGE_PATH" -n -q 2> >(grep -v "deprecated in IMv7" >&2)
+25 -26
View File
@@ -1,26 +1,25 @@
@@@@@@@@@@@@ @@ @@ @@
@@@@ @@@@ @@ @@ @@
@@@ @@@ @@ @@ @@
@@@ @@@ @@ @@ @@
@@@ @@ @@ @@ @@
@@ @@ @@ @@
@ @@ @@ @@ @@ @@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ @@ ##### @ @ @ @@ @@ @@
@ @ ##### @ @ ##### @ @@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@ @ ### @ @ ###### @ @@ @@ @@@@@@
@ @ @ @ ######@@ @@ @@ @@ @@
@@ @@@ @@@ @@ @@@ @ @@ @@ @@
@@ @@ @@ @@ @@
@@ @@ @@ @@ @@
@@ @ @@ @@@@@@@
@ @@ @@ @@
@@ @@ @@ @@
@@ @@ @@ @@
@ @@ @@ @@
@@ @@ @@@@@@@@@@@@@@@@
@@ @@
@@@ @@
@@@ @@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@ @@@ @@ @@
@@@@ @@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<rect x="8.6" y="1.6" width="1.3" height="4.6" rx="0.65" fill="#000000" transform="rotate(-12 9.25 3.9)"/>
<rect x="12.4" y="2.4" width="1.3" height="4.6" rx="0.65" fill="#000000" transform="rotate(12 13.05 4.7)"/>
<path d="M17.2 11 a3.4 3.4 0 0 1 0 6 v-1.7 a1.7 1.7 0 0 0 0 -2.6 z" fill="#000000"/>
<path d="M4 9 h14 l-2.5 9.2 a1.6 1.6 0 0 1 -1.55 1.2 h-5.9 a1.6 1.6 0 0 1 -1.55 -1.2 z" fill="#000000"/>
<rect x="3" y="20.4" width="18" height="2" rx="1" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<test name="family" qual="any">
<string>monospace</string>
</test>
<edit name="family" mode="prepend_first" binding="strong">
<string>JetBrainsMono Nerd Font</string>
</edit>
</match>
</fontconfig>
-83
View File
@@ -1,83 +0,0 @@
# vim:ft=kitty
## name: Flats
## upstream: themes/flats/colors.toml
## blurb: Dark theme with a soft blue accent. Hand-translated from the
## Omarchy colors.toml, since macOS has no omarchy-theme-set.
# The basic colors
foreground #cbd2d9
background #0f1012
selection_foreground #0f1012
selection_background #7babe3
# Cursor colors
cursor #cbd2d9
cursor_text_color #0f1012
# URL underline color when hovering with mouse
url_color #7babe3
# Kitty window border colors
active_border_color #7babe3
inactive_border_color #949494
bell_border_color #d58638
# OS Window titlebar colors
wayland_titlebar_color system
macos_titlebar_color system
# Tab bar colors
active_tab_foreground #0f1012
active_tab_background #7babe3
inactive_tab_foreground #cbd2d9
inactive_tab_background #0f1012
tab_bar_background #000000
# Colors for marks (marked text in the terminal)
mark1_foreground #0f1012
mark1_background #7babe3
mark2_foreground #0f1012
mark2_background #38d586
mark3_foreground #0f1012
mark3_background #d58638
# The 16 terminal colors
#
# Note: flats maps the ANSI slots unconventionally — color4 (blue) holds a
# magenta and color6 (cyan) holds the blue, among others. Kept faithful to
# colors.toml so both machines render identically; the comments below name
# the ANSI slot, not the resulting hue.
# black
color0 #000000
color8 #949494
# red
color1 #d53838
color9 #d53838
# green
color2 #38d4d5
color10 #38d4d5
# yellow
color3 #d58638
color11 #d58638
# blue
color4 #d53886
color12 #d53886
# magenta
color5 #38d586
color13 #38d586
# cyan
color6 #3886d5
color14 #3886d5
# white
color7 #d4d4d4
color15 #d4d4d4
+4 -5
View File
@@ -1,11 +1,10 @@
# vim:ft=kitty # vim:ft=kitty
# #
# kitty config for macOS. The Arch side of these dotfiles gets its terminal # Colors come from the active theme, the same way foot.ini takes them: the
# colors from omarchy-theme-set rewriting the active theme; there is no # theme pipeline renders default/themed/kitty.conf.tpl into the current-theme
# omarchy here, so current-theme.conf is checked in alongside this file and # directory, and blob-restart-terminal signals kitty to reload it.
# generated by hand from themes/<name>/colors.toml.
include current-theme.conf include ~/.local/state/blob/current/theme/kitty.conf
# Fonts # Fonts
-32
View File
@@ -1,32 +0,0 @@
add_newline = true
command_timeout = 200
format = "[$directory$git_branch$git_status]($style)$character"
[character]
error_symbol = "[✗](bold cyan)"
success_symbol = "[](bold cyan)"
[directory]
truncation_length = 2
truncation_symbol = "…/"
repo_root_style = "bold cyan"
repo_root_format = "[$repo_root]($repo_root_style)[$path]($style)[$read_only]($read_only_style) "
[git_branch]
format = "[$branch]($style) "
style = "italic cyan"
[git_status]
format = '[$all_status]($style)'
style = "cyan"
ahead = "⇡${count} "
diverged = "⇕⇡${ahead_count}⇣${behind_count} "
behind = "⇣${count} "
conflicted = " "
up_to_date = " "
untracked = "? "
modified = " "
stashed = ""
staged = ""
renamed = ""
deleted = ""
+4 -4
View File
@@ -254,8 +254,8 @@
"setup.direct-boot": {"icon":"","label":"Direct Boot","action":"blob-launch-floating blob-setup-direct-boot"}, "setup.direct-boot": {"icon":"","label":"Direct Boot","action":"blob-launch-floating blob-setup-direct-boot"},
// Install // Install
"install.package": {"icon":"󰣇","label":"Package","action":"xdg-terminal-exec --app-id=org.blob.terminal blob-pkg-install"}, "install.package": {"icon":"󰣇","label":"Package","action":"blob-terminal-exec --app-id=org.blob.terminal blob-pkg-install"},
"install.aur": {"icon":"󰣇","label":"AUR","action":"xdg-terminal-exec --app-id=org.blob.terminal blob-pkg-aur-install"}, "install.aur": {"icon":"󰣇","label":"AUR","action":"blob-terminal-exec --app-id=org.blob.terminal blob-pkg-aur-install"},
"install.tui": {"icon":"","label":"TUI","action":"blob-launch-floating blob-tui-install"}, "install.tui": {"icon":"","label":"TUI","action":"blob-launch-floating blob-tui-install"},
"install.style": {"icon":"","label":"Style"}, "install.style": {"icon":"","label":"Style"},
"install.style.theme": {"icon":"󰸌","label":"Theme","action":"blob-launch-floating blob-theme-install"}, "install.style.theme": {"icon":"󰸌","label":"Theme","action":"blob-launch-floating blob-theme-install"},
@@ -269,8 +269,8 @@
"install.style.font.iosevka": {"icon":"","label":"Iosevka","action":"blob-install-font Iosevka ttf-iosevka-nerd 'Iosevka Nerd Font Mono'"}, "install.style.font.iosevka": {"icon":"","label":"Iosevka","action":"blob-install-font Iosevka ttf-iosevka-nerd 'Iosevka Nerd Font Mono'"},
// Remove // Remove
"remove.package": {"icon":"󰣇","label":"Package","action":"xdg-terminal-exec --app-id=org.blob.terminal blob-pkg-remove"}, "remove.package": {"icon":"󰣇","label":"Package","action":"blob-terminal-exec --app-id=org.blob.terminal blob-pkg-remove"},
"remove.tui": {"icon":"","label":"TUI","when":"grep -qE '^Exec=.*(\\$TERMINAL|xdg-terminal-exec).*-e' $HOME/.local/share/applications/*.desktop","action":"blob-tui-remove"}, "remove.tui": {"icon":"","label":"TUI","when":"grep -qE '^Exec=.*(\\$TERMINAL|xdg-terminal-exec|blob-terminal-exec).*-e' $HOME/.local/share/applications/*.desktop","action":"blob-tui-remove"},
"remove.theme": {"icon":"󰸌","label":"Theme","action":"blob-theme-remove"}, "remove.theme": {"icon":"󰸌","label":"Theme","action":"blob-theme-remove"},
// Update // Update
+1
View File
@@ -37,6 +37,7 @@ hl.env("PATH", table.concat(kept, ":"))
-- Hardware-specific environment. -- Hardware-specific environment.
require("default.hypr.nvidia") require("default.hypr.nvidia")
require("default.hypr.renderer")
hl.config({ hl.config({
xwayland = { xwayland = {
+14
View File
@@ -0,0 +1,14 @@
local paths = require("default.hypr.paths")
local accelerated = paths.blob_path .. "/bin/blob-hw-gpu-accelerated"
if not o.shell_succeeds(o.shell_quote(accelerated)) then
hl.env("LIBGL_ALWAYS_SOFTWARE", "1")
hl.env("WLR_NO_HARDWARE_CURSORS", "1")
hl.config({
cursor = {
no_hardware_cursors = true,
},
})
end
+2
View File
@@ -0,0 +1,2 @@
#%PAM-1.0
auth include system-auth
+8
View File
@@ -0,0 +1,8 @@
[Plymouth Theme]
Name=Blob
Description=Blob boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/blob
ScriptFile=/usr/share/plymouth/themes/blob/blob.script
+70
View File
@@ -0,0 +1,70 @@
Window.SetBackgroundTopColor(0.059, 0.063, 0.071);
Window.SetBackgroundBottomColor(0.059, 0.063, 0.071);
screen.width = Window.GetWidth();
screen.height = Window.GetHeight();
logo.image = Image("logo.png");
logo.width = logo.image.GetWidth();
logo.height = logo.image.GetHeight();
scale_x = screen.width / logo.width;
scale_y = screen.height / logo.height;
scale = scale_x;
if (scale_y < scale_x) {
scale = scale_y;
}
if (scale != 1) {
logo.image = logo.image.Scale(logo.width * scale, logo.height * scale);
logo.width = logo.image.GetWidth();
logo.height = logo.image.GetHeight();
}
logo.sprite = Sprite(logo.image);
logo.sprite.SetX((screen.width - logo.width) / 2);
logo.sprite.SetY((screen.height - logo.height) / 2);
logo.sprite.SetZ(0);
prompt.sprite = Sprite();
prompt.sprite.SetZ(10);
message.sprite = Sprite();
message.sprite.SetZ(10);
fun draw_prompt(text) {
if (text == "") {
prompt.sprite.SetImage(Image.Text(" ", 1, 1, 1));
return;
}
prompt.image = Image.Text(text, 0.8, 0.82, 0.85);
prompt.sprite.SetImage(prompt.image);
prompt.sprite.SetX((screen.width - prompt.image.GetWidth()) / 2);
prompt.sprite.SetY(screen.height * 0.82);
}
fun display_password_callback(prompt_text, bullets) {
dots = "";
index = 0;
while (index < bullets) {
dots += "*";
index++;
}
draw_prompt(prompt_text + " " + dots);
}
fun display_normal_callback() {
draw_prompt("");
}
fun message_callback(text) {
message.image = Image.Text(text, 0.8, 0.82, 0.85);
message.sprite.SetImage(message.image);
message.sprite.SetX((screen.width - message.image.GetWidth()) / 2);
message.sprite.SetY(screen.height * 0.9);
}
Plymouth.SetDisplayPasswordFunction(display_password_callback);
Plymouth.SetDisplayNormalFunction(display_normal_callback);
Plymouth.SetMessageFunction(message_callback);
-9
View File
@@ -1,9 +0,0 @@
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
force_default_wallpaper = 0
}
animations {
enabled = false
}
+38
View File
@@ -0,0 +1,38 @@
-- Hyprland config for the SDDM Wayland greeter. SDDM starts the greeter itself
-- once the compositor is up, so nothing here launches anything.
-- The greeter runs as SDDM's own user, with no BLOB_PATH and no read access to
-- the checkout, so it calls the copy install.sh puts in /usr/local/bin rather
-- than loading the Lua modules the session uses. A detector that is not there
-- yet reads as no acceleration, which is the fallback that always draws.
local function gpu_is_accelerated()
local pipe = io.popen("/usr/local/bin/blob-hw-gpu-accelerated >/dev/null 2>&1 && echo OK")
if not pipe then
return false
end
local output = pipe:read("*a") or ""
pipe:close()
return output:match("OK") ~= nil
end
if not gpu_is_accelerated() then
hl.env("LIBGL_ALWAYS_SOFTWARE", "1")
hl.env("WLR_NO_HARDWARE_CURSORS", "1")
end
hl.config({
cursor = {
no_hardware_cursors = true,
},
misc = {
disable_hyprland_logo = true,
disable_splash_rendering = true,
force_default_wallpaper = 0,
},
animations = {
enabled = false,
},
})
+1 -1
View File
@@ -2,7 +2,7 @@
DisplayServer=wayland DisplayServer=wayland
[Wayland] [Wayland]
CompositorCommand=start-hyprland -- --config /etc/sddm/hyprland-greeter.conf CompositorCommand=start-hyprland -- --config /etc/sddm/hyprland-greeter.lua
[Theme] [Theme]
Current= Current=
+2 -2
View File
@@ -14,7 +14,7 @@ theme[hi_fg]="{{ accent }}"
theme[selected_bg]="{{ selection }}" theme[selected_bg]="{{ selection }}"
# Foreground color of selected item in processes box # Foreground color of selected item in processes box
theme[selected_fg]="{{ accent }}" theme[selected_fg]="{{ selection_foreground }}"
# Color of inactive/disabled text # Color of inactive/disabled text
theme[inactive_fg]="{{ muted }}" theme[inactive_fg]="{{ muted }}"
@@ -23,7 +23,7 @@ theme[inactive_fg]="{{ muted }}"
theme[graph_text]="{{ light_foreground }}" theme[graph_text]="{{ light_foreground }}"
# Background color of the percentage meters # Background color of the percentage meters
theme[meter_bg]="{{ selection }}" theme[meter_bg]="{{ lighter_background }}"
# Misc colors for processes box including mini cpu graphs, details memory graph and details status text # Misc colors for processes box including mini cpu graphs, details memory graph and details status text
theme[proc_misc]="{{ light_foreground }}" theme[proc_misc]="{{ light_foreground }}"
+50
View File
@@ -0,0 +1,50 @@
{
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
"logo": {
"type": "file",
"source": "~/.config/blob/branding/about.txt",
"padding": {
"top": 2,
"left": 2,
"right": 6
}
},
"display": {
"separator": " ",
"key": {
"width": 9
},
"color": {
"title": "{{ accent_ansi }}",
"keys": "{{ accent_ansi }}",
"output": "{{ foreground_ansi }}"
}
},
"modules": [
{
"type": "title",
"format": "{user-name}@{host-name}"
},
"break",
{ "type": "os", "key": "os" },
{ "type": "kernel", "key": "kernel" },
{ "type": "uptime", "key": "uptime" },
{ "type": "packages", "key": "packages" },
{ "type": "shell", "key": "shell" },
"break",
{ "type": "wm", "key": "wm" },
{ "type": "terminal", "key": "term" },
{ "type": "display", "key": "display" },
"break",
{ "type": "cpu", "key": "cpu" },
{ "type": "gpu", "key": "gpu" },
{ "type": "memory", "key": "memory" },
{ "type": "disk", "key": "disk" },
"break",
{
"type": "colors",
"symbol": "circle",
"paddingLeft": 9
}
]
}
+41
View File
@@ -0,0 +1,41 @@
@define-color window_bg_color {{ background }};
@define-color window_fg_color {{ foreground }};
@define-color view_bg_color {{ background }};
@define-color view_fg_color {{ foreground }};
@define-color headerbar_bg_color {{ lighter_background }};
@define-color headerbar_fg_color {{ foreground }};
@define-color headerbar_border_color {{ dark_background }};
@define-color headerbar_backdrop_color {{ background }};
@define-color sidebar_bg_color {{ lighter_background }};
@define-color sidebar_fg_color {{ foreground }};
@define-color sidebar_backdrop_color {{ background }};
@define-color sidebar_border_color {{ dark_background }};
@define-color secondary_sidebar_bg_color {{ lighter_background }};
@define-color secondary_sidebar_fg_color {{ foreground }};
@define-color secondary_sidebar_backdrop_color {{ background }};
@define-color secondary_sidebar_border_color {{ dark_background }};
@define-color card_bg_color {{ lighter_background }};
@define-color card_fg_color {{ foreground }};
@define-color dialog_bg_color {{ background }};
@define-color dialog_fg_color {{ foreground }};
@define-color popover_bg_color {{ lighter_background }};
@define-color popover_fg_color {{ foreground }};
@define-color accent_bg_color {{ accent }};
@define-color accent_fg_color {{ background }};
@define-color accent_color {{ accent }};
@define-color destructive_bg_color {{ red }};
@define-color destructive_fg_color {{ background }};
@define-color destructive_color {{ red }};
@define-color success_color {{ green }};
@define-color warning_color {{ yellow }};
@define-color error_color {{ red }};
@define-color theme_bg_color {{ background }};
@define-color theme_fg_color {{ foreground }};
@define-color theme_base_color {{ background }};
@define-color theme_text_color {{ foreground }};
@define-color theme_selected_bg_color {{ accent }};
@define-color theme_selected_fg_color {{ background }};
@define-color insensitive_bg_color {{ dark_background }};
@define-color insensitive_fg_color {{ muted }};
@define-color borders {{ dark_background }};
+42
View File
@@ -0,0 +1,42 @@
add_newline = true
command_timeout = 200
format = "$directory$git_branch$git_status$character"
palette = "blob"
[palettes.blob]
accent = "{{ accent }}"
foreground = "{{ foreground }}"
muted = "{{ muted }}"
success = "{{ green }}"
warning = "{{ yellow }}"
error = "{{ red }}"
[character]
error_symbol = "[✗](bold error)"
success_symbol = "[](bold accent)"
[directory]
style = "foreground"
truncation_length = 2
truncation_symbol = "…/"
repo_root_style = "bold accent"
repo_root_format = "[$repo_root]($repo_root_style)[$path]($style)[$read_only]($read_only_style) "
[git_branch]
format = "[$branch]($style) "
style = "italic accent"
[git_status]
format = '[$all_status ]($style)'
style = "warning"
ahead = "⇡${count} "
diverged = "⇕⇡${ahead_count}⇣${behind_count} "
behind = "⇣${count} "
conflicted = " "
up_to_date = " "
untracked = "? "
modified = " "
stashed = ""
staged = ""
renamed = ""
deleted = ""
+3
View File
@@ -206,6 +206,7 @@ of the `blob` listing.
| `blob-hw-display` | Print the most likely display backlight device. | - | | `blob-hw-display` | Print the most likely display backlight device. | - |
| `blob-hw-external` | Returns true when an external monitor is physically connected. | - | | `blob-hw-external` | Returns true when an external monitor is physically connected. | - |
| `blob-hw-fingerprint` | Returns true when a fingerprint reader is present (hidden) | - | | `blob-hw-fingerprint` | Returns true when a fingerprint reader is present (hidden) | - |
| `blob-hw-gpu-accelerated` | Detect whether a DRM device with an accelerated driver is present. | - |
| `blob-hw-hybrid-gpu` | Detect whether the system has an active hybrid GPU configuration | - | | `blob-hw-hybrid-gpu` | Detect whether the system has an active hybrid GPU configuration | - |
| `blob-hw-laptop` | Returns true when running on a laptop (has a lid or laptop chassis). | - | | `blob-hw-laptop` | Returns true when running on a laptop (has a lid or laptop chassis). | - |
| `blob-hw-laptop-closed` | Returns true when the laptop lid is closed (hidden) | - | | `blob-hw-laptop-closed` | Returns true when the laptop lid is closed (hidden) | - |
@@ -295,6 +296,7 @@ of the `blob` listing.
| `blob-launch-or-focus-webapp` | Launch or focus on a given web app identified by the window-pattern. | <window-pattern> <url-and-flags...> | | `blob-launch-or-focus-webapp` | Launch or focus on a given web app identified by the window-pattern. | <window-pattern> <url-and-flags...> |
| `blob-launch-screensaver` | Launch the Blob screensaver in the default terminal on the system with the correct font configuration. | - | | `blob-launch-screensaver` | Launch the Blob screensaver in the default terminal on the system with the correct font configuration. | - |
| `blob-launch-shell` | Launch the Blob shell with its log kept in the journal (hidden) | - | | `blob-launch-shell` | Launch the Blob shell with its log kept in the journal (hidden) | - |
| `blob-launch-terminal` | Launch the default terminal | [command [args...]] |
| `blob-launch-tui` | Launch a TUI command in the default terminal with Blob styling | [--app-id=<app-id>] <command> [args...] | | `blob-launch-tui` | Launch a TUI command in the default terminal with Blob styling | [--app-id=<app-id>] <command> [args...] |
| `blob-launch-webapp` | Launch a URL as a web app in the default supported browser | <url> | | `blob-launch-webapp` | Launch a URL as a web app in the default supported browser | <url> |
@@ -531,6 +533,7 @@ of the `blob` listing.
| Command | Does | Arguments | | Command | Does | Arguments |
| --- | --- | --- | | --- | --- | --- |
| `blob-terminal-exec` | Run a command in the default terminal (hidden) | [--print-id] [--app-id=<app-id>] [--title=<title>] [-e] <command> [args...] |
| `blob-theme-browser` | Apply the current theme color to Chromium, Chrome, Edge, and Brave (hidden) | - | | `blob-theme-browser` | Apply the current theme color to Chromium, Chrome, Edge, and Brave (hidden) | - |
| `blob-theme-browser-policy` | Write the current theme color into the browser policy directories (hidden) | <rrggbb> | | `blob-theme-browser-policy` | Write the current theme color into the browser policy directories (hidden) | <rrggbb> |
| `blob-theme-color` | Resolve semantic colors from an Blob theme colors.toml (hidden) | [--file <colors.toml>] (--all \| --raw \| <key> [fallback]) | | `blob-theme-color` | Resolve semantic colors from an Blob theme colors.toml (hidden) | [--file <colors.toml>] (--all \| --raw \| <key> [fallback]) |
+163 -23
View File
@@ -13,44 +13,101 @@ with `--check`.
| `install/helpers/as-root.sh` | `as_root`, a no-op when already root | | `install/helpers/as-root.sh` | `as_root`, a no-op when already root |
| `install/steps/directories.sh` | the directories the shell and scripts expect | | `install/steps/directories.sh` | the directories the shell and scripts expect |
| `install/steps/packages.sh` | repo and AUR packages, and the yay bootstrap | | `install/steps/packages.sh` | repo and AUR packages, and the yay bootstrap |
| `install/steps/config.sh` | the `BLOB_PATH` symlink and everything under `~/.config` | | `install/steps/config.sh` | the `BLOB_PATH` symlink, everything under `~/.config`, and the login shell profile |
| `install/steps/services.sh` | system services, systemd drop-ins, user units | | `install/steps/services.sh` | system services, systemd drop-ins, user units |
| `install/steps/theme.sh` | the wallpaper link, the first theme, the first background |
| `install/steps/login.sh` | SDDM, the session entry, and the first launch | | `install/steps/login.sh` | SDDM, the session entry, and the first launch |
## Steps ## Steps
1. **`BLOB_PATH`** is symlinked to the checkout at `~/.local/share/blob`. Nothing 1. **Directories** are created first, because nothing below can write into a
under `bin/`, `shell/`, `themes/` or `default/` is ever copied. directory that is not there: `~/.config/blob`, `~/.config/systemd/user/app.slice.d`,
2. **Directories** are created, including `~/.config/systemd/user/app.slice.d` `~/.local/state/blob`, `~/.local/share` and `~/.cache/blob`. A fresh Arch
and `~/wallpapers`. install has none of them.
2. **`BLOB_PATH`** is symlinked to the checkout at `~/.local/share/blob`. Nothing
under `bin/`, `shell/`, `themes/` or `default/` is ever copied. `ln` does not
create the parent of a link, so this step makes `~/.local/share` itself rather
than assuming step 1 ran.
3. **Packages** come from `packages/blob.packages` and 3. **Packages** come from `packages/blob.packages` and
`packages/blob-aur.packages`. A machine with no AUR helper gets `yay-bin` `packages/blob-aur.packages`. A machine with no AUR helper gets `yay-bin`
built once with `makepkg`. See [packages.md](packages.md). built once with `makepkg`. A failed batch refreshes the databases and retries,
then falls back to one package at a time, so a single name that no longer
resolves cannot cost you the other hundred and the config, service and login
steps below. See [packages.md](packages.md).
4. **Config** is copied into `~/.config`: `hypr/`, the themed app configs, 4. **Config** is copied into `~/.config`: `hypr/`, the themed app configs,
`blob/shell.json`, the uwsm environment, and the icon font. `blob/shell.json`, the uwsm environment, `fontconfig/fonts.conf`, and the
5. **User units** land in `~/.config/systemd/user` and are enabled, not started: icon font, and the font cache is refreshed. The shell draws every icon as a
Nerd Font glyph in the `monospace` family, and on a fresh Arch install
fontconfig resolves that to a font with no such glyphs, so the bar and its
panels come up full of blanks. `fonts.conf` binds `monospace` to
`JetBrainsMono Nerd Font`; `blob font set` rewrites the same file, and the
installer then reports it as a local change and keeps your choice.
`~/.bashrc` gets one line sourcing `session/bashrc`. The line is added once,
a `.bak` is kept, and a `~/.bashrc` that already has it is left alone.
`session/profile.sh` also goes to `/etc/profile.d/blob.sh`, which sources
`session/env-bootstrap` and is what puts `blob` on `PATH` in a login shell.
Without it `BLOB_PATH` and `$BLOB_PATH/bin` are only set by
`~/.config/uwsm/env.d/10-blob`, which uwsm reads when the graphical session
starts, so `blob` is not found from a TTY. A shell that was already open when
the installer ran keeps its old `PATH`; open a new one, or
`source ~/.local/share/blob/session/env-bootstrap`.
5. **The first theme** is applied. `~/wallpapers` is symlinked to the checkout's
`wallpapers/`, `flats` is applied with `BLOB_THEME_HEADLESS=1` (no shell or
session bus exists yet), and the background symlink is pointed at the first
image in `~/wallpapers`. Without this the first login has no palette and no
wallpaper, which draws as a black desktop. It then links the per-app theme
files the pipeline renders but no app would otherwise find:
`~/.config/btop/themes/current.theme` (btop's `color_theme = "current"`) and,
only when `~/.config/nvim/lua/plugins` already exists, `theme.lua` there.
Every step is skipped when a theme, a background, a wallpaper directory or a
theme file of your own is already in place. A theme counts as in place only
when the staged theme under `~/.local/state/blob/current/theme` is there too,
so a recorded theme name left without its palette is applied again rather
than skipped.
6. **User units** land in `~/.config/systemd/user` and are enabled, not started:
they are all `WantedBy=graphical-session.target`, so they come up with the they are all `WantedBy=graphical-session.target`, so they come up with the
session. `blob-speaker-tuning.service` is left out, because session. `blob-speaker-tuning.service` is left out, because
`blob-audio-tuning` installs it only on machines with a tuning profile. `blob-audio-tuning` installs it only on machines with a tuning profile.
6. **systemd drop-ins** are written under `/etc`: the shutdown timeouts, the 7. **systemd drop-ins** are written under `/etc`: the shutdown timeouts, the
oomd pressure thresholds, the plocate prune paths, and the sleep hooks. oomd pressure thresholds, the plocate prune paths, and the sleep hooks.
7. **System services** are enabled: NetworkManager, resolved, bluetooth, cups, 8. **System services** are enabled: NetworkManager, resolved, bluetooth, cups,
avahi, docker, power-profiles-daemon, and oomd. avahi, docker, power-profiles-daemon, and oomd.
`NetworkManager-wait-online.service` is masked so a slow DHCP lease cannot `NetworkManager-wait-online.service` is masked so a slow DHCP lease cannot
hold up the login screen. hold up the login screen, and `plymouth-start.service` and
8. **The login screen** is SDDM. Three pieces have to be in place: `plymouth-quit-wait.service` are masked because an unconfigured plymouth
holds the console and leaves the machine with neither a greeter nor a TTY.
`blob boot` unmasks them when it sets the splash up. See
[packages.md](packages.md#the-boot-splash).
9. **The login screen** is SDDM. It has to be installed first: if
`sddm.service` is not there, the installer says whether the package is
missing or whether systemd has simply not re-read its units yet, and carries
on without enabling anything. A unit counts as present when `systemctl cat`
finds it or when its file is under `/etc/systemd/system`,
`/run/systemd/system`, `/usr/local/lib/systemd/system` or
`/usr/lib/systemd/system` - `systemctl` goes through the service manager, so
it reports a perfectly good unit as missing inside a chroot or right after
pacman wrote it.
Three pieces have to be in place:
| File | Why | | File | Why |
| --- | --- | | --- | --- |
| `/usr/local/share/wayland-sessions/blob.desktop` | the session SDDM offers | | `/usr/local/share/wayland-sessions/blob.desktop` | the session SDDM offers |
| `/etc/sddm.conf.d/zz-blob.conf` | Wayland greeter, run on Hyprland, stock theme, remember the last session | | `/etc/sddm.conf.d/zz-blob.conf` | Wayland greeter, run on Hyprland, stock theme, remember the last session |
| `/etc/sddm/hyprland-greeter.conf` | the greeter's own tiny Hyprland config | | `/etc/sddm/hyprland-greeter.lua` | the greeter's own tiny Hyprland config |
| `/usr/local/bin/blob-hw-gpu-accelerated` | the renderer check that config runs, where SDDM's user can read it |
| `/etc/pam.d/blob-lock-password` | the PAM stack the lock screen authenticates against |
The greeter config matters more than it looks: SDDM's stock Wayland greeter The greeter config matters more than it looks: SDDM's stock Wayland greeter
runs on `weston`, which Blob does not install, so a default SDDM would fail runs on `weston`, which Blob does not install, so a default SDDM would fail
to draw anything. `sddm.service` is enabled and the default systemd target is to draw anything. The installer checks that the `CompositorCommand` named in
set to `graphical.target`, which an Arch install without a display manager `zz-blob.conf` is actually installed and warns when it is not, because SDDM
does not have. with a compositor command it cannot run starts, fails, and leaves the machine
sitting at a text console with no explanation. That greeter config has to be Lua: Hyprland dropped the old
hyprlang format, and a config it cannot parse leaves the greeter with no
compositor and the screen black. `sddm.service` is enabled and the default
systemd target is set to `graphical.target`, which an Arch install without a
display manager does not have.
The `zz-` prefix is load-bearing. SDDM reads `/etc/sddm.conf.d` in The `zz-` prefix is load-bearing. SDDM reads `/etc/sddm.conf.d` in
alphabetical order and the last file wins, so a leftover `10-wayland.conf`, alphabetical order and the last file wins, so a leftover `10-wayland.conf`,
@@ -70,10 +127,13 @@ with `--check`.
`--autologin` writes `zzz-blob-autologin.conf`, the only file that sorts after `--autologin` writes `zzz-blob-autologin.conf`, the only file that sorts after
`zz-blob.conf`. `zz-blob.conf`.
9. **Launch.** When the installer was run from a console and nothing graphical 10. **Launch.** `sddm`, `hyprland`, `quickshell` and `uwsm` are checked one last
is running, it starts `sddm.service` so the desktop appears without a reboot. time and any that are missing are named, because that is the whole difference
`--no-launch` prints the command instead. `--autologin` writes between a desktop and a console. Then, when the installer was run from a
`/etc/sddm.conf.d/99-blob-autologin.conf` for the current user. console and nothing graphical is running, it starts `sddm.service` so the
desktop appears without a reboot. `--no-launch` prints the command instead.
`--autologin` writes `/etc/sddm.conf.d/zzz-blob-autologin.conf` for the
current user.
## Flags ## Flags
@@ -85,10 +145,90 @@ with `--check`.
| `--autologin` | skip the SDDM prompt for this user | | `--autologin` | skip the SDDM prompt for this user |
| `--no-launch` | leave the login screen for the next reboot | | `--no-launch` | leave the login screen for the next reboot |
## The interactive shell
`session/bashrc` is sourced from `~/.bashrc` and returns immediately in a
non-interactive shell, so it cannot disturb `scp` or `rsync`. It does three
things, each only when the tool is installed:
| Line | Effect |
| --- | --- |
| `session/env-bootstrap` | `BLOB_PATH` and `$BLOB_PATH/bin` on `PATH`, so `blob` resolves |
| `starship init bash` | the themed prompt, see [themes.md](themes.md#the-prompt) |
| `zoxide init bash --cmd cd` | `cd` learns the directories you actually use |
`--cmd cd` replaces `cd` rather than adding a `z` command, so
`cd blomarchy` jumps to the last matching directory you have visited from
anywhere, while `cd ./relative/path`, `cd -` and `cd` with no argument keep
behaving exactly as the builtin does. `cdi` picks from the matches
interactively through `fzf`, and `zoxide query -ls` prints the database.
The database is built as you go: it learns a directory the first time you `cd`
there, so the jumping is only as good as the history behind it and a fresh
install knows nothing yet.
## The lock screen
The lock plugin watches `/etc/pam.d/blob-lock-password` and refuses to lock at
all when it is not there - `blob system lock` returns `missing-pam` and nothing
appears, which reads as a keybinding that does nothing. The installer writes it;
it is one line including `system-auth`, the same stack every other Wayland
locker on Arch authenticates against.
Fingerprint unlock is separate and opt-in: `blob setup-security-fingerprint`
writes `/etc/pam.d/blob-lock-fingerprint`, and the lock only offers fingerprint
when that file and a reader are both present.
## Choosing a renderer
Hyprland renders through Mesa, and on an adapter Mesa has no driver for it exits
with `failed to create dri2 screen` instead of drawing. The greeter is Hyprland
too, so that is a black screen at boot and a second one after login, which is
how a machine with no real GPU behind its display arrives.
`blob-hw-gpu-accelerated` decides which way to go. It reads the kernel driver of
each `/sys/class/drm/card*` from sysfs, not from `lspci`, which reads PCI config
space and resumes runtime-suspended GPUs. A card driven by `amdgpu`, `i915`,
`nouveau`, `nvidia`, `nvidia-drm`, `radeon` or `xe` is a real integrated or
dedicated GPU and hardware rendering is left alone. Everything else - `vmwgfx`,
`vboxvideo`, `qxl`, `bochs-drm`, `cirrus`, `simpledrm`, `vkms`, `virtio_gpu` -
is virtual or framebuffer-only, and two settings are applied:
| Setting | Effect |
| --- | --- |
| `LIBGL_ALWAYS_SOFTWARE=1` | Mesa uses llvmpipe, which renders on the CPU |
| `WLR_NO_HARDWARE_CURSORS=1`, `cursor:no_hardware_cursors` | the cursor is drawn by the compositor, since there is no plane to put it on |
llvmpipe is slow and it draws. A machine with a real GPU never touches it.
Two places read the detector, because they run as different users:
| Where | How |
| --- | --- |
| the session | `default/hypr/renderer.lua`, required from `envs.lua` beside `nvidia.lua`, through `$BLOB_PATH/bin` |
| the greeter | `default/sddm/hyprland-greeter.lua`, through `/usr/local/bin/blob-hw-gpu-accelerated` |
The greeter runs as SDDM's own user, with no `BLOB_PATH` and no read access to
your home directory, so `install.sh` copies the detector to `/usr/local/bin`.
A detector that is not there yet reads as no acceleration, which is the fallback
that always draws.
To add a driver to the accelerated list, edit `accelerated_drivers` in
`bin/blob-hw-gpu-accelerated` and rerun `./install.sh` so the greeter's copy
follows. `virtio_gpu` is deliberately not on it: QEMU only accelerates it when
the host enables virgl, and Mesa fails the same way when it does not.
To see the difference by hand, from a console:
```bash
Hyprland # fails with the dri2 error
LIBGL_ALWAYS_SOFTWARE=1 Hyprland # starts
```
## Uninstall ## Uninstall
`./uninstall.sh` removes the config, the user units, the `/etc` drop-ins, the `./uninstall.sh` removes the config, the user units, the `/etc` drop-ins, the
session entry and the symlink, restoring any `.bak` the installer left. session entry, the wallpaper link and the symlink, restoring any `.bak` the
`--keep-session` keeps the session entry and the SDDM config, `--keep-state` installer left. `--keep-session` keeps the session entry and the SDDM config,
keeps `~/.local/state/blob`. `sddm.service` is left enabled either way, so the `--keep-state` keeps `~/.local/state/blob`. `sddm.service` is left enabled either way, so the
machine still reaches a login screen. machine still reaches a login screen.
+47 -7
View File
@@ -58,8 +58,11 @@ than usual.
Two are worth knowing about: Two are worth knowing about:
- `xdg-terminal-exec` is load-bearing. Every TUI launcher's `Exec=` line calls - `xdg-terminal-exec` used to be load-bearing: every TUI launcher, the floating
it, so if its AUR build ever fails, TUI shortcuts stop opening. launcher behind most of the menu, and `$TERMINAL` called it by name, so a
failed AUR build silently broke about half the menu. They all go through
`blob-terminal-exec` now, which prefers it and falls back to `foot`, then
`kitty`, both from the official repositories.
- `aether` is in the AUR at a version newer than the one the repository shipped. - `aether` is in the AUR at a version newer than the one the repository shipped.
`quickshell` never came from the Omarchy repository - it is in official `extra`. `quickshell` never came from the Omarchy repository - it is in official `extra`.
@@ -69,13 +72,28 @@ Two are worth knowing about:
`install.sh` installs what is missing before it writes any config: `install.sh` installs what is missing before it writes any config:
``` ```
packages/blob.packages 119 packages from core, extra, multilib packages/blob.packages 123 packages from core, extra, multilib
packages/blob-aur.packages 10 packages with no official equivalent packages/blob-aur.packages 11 packages with no official equivalent
``` ```
Only missing packages are touched, through `pacman -S --needed` and Only missing packages are touched, through `pacman -S --needed` and
`yay -S --needed`. `./install.sh --check` lists what it would install without `yay -S --needed`. An entry is treated as already installed, and skipped, when a
installing anything, and `--skip-packages` does the directories and config only. package of that name is installed, when an installed package provides that name,
or when the name is a group whose members are all installed - so `base-devel` and
packages pulled in under a different name no longer come back on every run. AUR
packages are built one at a time, so a build that fails is reported at the end
instead of stopping the install.
Repo packages go in one `pacman` call. If that call fails the databases are
refreshed with `pacman -Sy` and it is tried again, because a stale database is
what turns an installed-and-present package into `target not found`. If it still
fails the packages are installed one at a time, so one unresolvable name does not
block the other hundred - which is how a missing `sddm` used to turn into a
machine with no login screen. `install.sh` ends by naming any of `sddm`,
`hyprland`, `quickshell` and `uwsm` that are still not installed.
`./install.sh --check` lists what it would install without installing anything,
and `--skip-packages` does the directories and config only.
### Validating the lists ### Validating the lists
@@ -111,12 +129,34 @@ Omarchy's own package list, because they were installed by hand on this machine:
| Package | Needed by | | Package | Needed by |
| --- | --- | | --- | --- |
| `python-pywal` | `blob-wallpaper-set`, palette extraction | | `python-pywal16` | `blob-wallpaper-set`, palette extraction |
| `python-terminaltexteffects` | `blob-screensaver`, the effects it draws |
| `awww` | the wallpaper daemon `blob-wallpaper-set` and `blob-theme-menu` drive | | `awww` | the wallpaper daemon `blob-wallpaper-set` and `blob-theme-menu` drive |
| `playerctl` | the quick settings media row | | `playerctl` | the quick settings media row |
`playerctl` is in official `extra`, so it sits in the repo list. `playerctl` is in official `extra`, so it sits in the repo list.
## The boot splash
`plymouth` is installed but left inert, and the splash is opt-in through one
command:
```
blob boot # branding/boot_flash.png
blob boot ~/some/image.png # or any other image
```
The reason it is opt-in is that an installed but *unconfigured* plymouth is not
harmless: it takes the console at boot and never hands it back, and
`getty@tty1` waits on `plymouth-quit-wait.service`, so the machine reaches
neither the greeter nor a TTY. `install.sh` therefore masks
`plymouth-start.service` and `plymouth-quit-wait.service`, and `blob boot`
unmasks them as part of configuring everything else - the theme in
`default/plymouth/`, the `plymouth` mkinitcpio hook, and the initramfs rebuild.
It finishes by printing the kernel parameters your bootloader still needs
(`quiet splash vt.global_cursor_default=0`), because that line is the one thing
it will not rewrite for a bootloader other than GRUB.
## Updating ## Updating
``` ```
+62 -4
View File
@@ -9,7 +9,7 @@ retints the running desktop.
| File | Used by | | File | Used by |
| --- | --- | | --- | --- |
| `colors.toml` | required; the palette everything else derives from | | `colors.toml` | required; the palette everything else derives from |
| `backgrounds/` | the background switcher | | `backgrounds/` | the background switcher; a theme without one keeps the current wallpaper |
| `shell.lock.toml` | lock screen surface tokens | | `shell.lock.toml` | lock screen surface tokens |
| `neovim.lua` | the editor colorscheme | | `neovim.lua` | the editor colorscheme |
| `icons.theme` | GTK icon theme name | | `icons.theme` | GTK icon theme name |
@@ -27,9 +27,17 @@ the shell and the CLI never disagree about what a slot means.
## Templates ## Templates
Each rendered file is picked up differently: `foot.ini` and `kitty.conf` by an
`include` in the shipped terminal config, `hyprland.lua` through Hyprland's Lua
module path, `shell.toml` and `chromium.theme` read straight from the state
directory, and `btop.theme`, `neovim.lua`, `gtk.css`, `starship.toml` and
`fastfetch.jsonc` through symlinks the installer makes into those apps' own
config trees.
`default/themed/*.tpl` are rendered per theme into `default/themed/*.tpl` are rendered per theme into
`~/.local/state/blob/current/theme/`. Nine ship: foot, kitty, btop, chromium, `~/.local/state/blob/current/theme/`. Twelve ship: foot, kitty, btop, chromium,
hyprland, the screenshare picker, neovim, `shell.toml`, and zen. hyprland, the screenshare picker, neovim, `shell.toml`, zen, `gtk.css`,
`starship.toml`, and `fastfetch.jsonc`.
An app picks its colors up in one of two ways. Most include the generated file An app picks its colors up in one of two ways. Most include the generated file
directly, so nothing has to run: directly, so nothing has to run:
@@ -41,11 +49,56 @@ include ~/.local/state/blob/current/theme/kitty.conf
Zen does the same through a CSS import in `userChrome.css`. The rest are pushed Zen does the same through a CSS import in `userChrome.css`. The rest are pushed
by a short applier listed in `post_theme_commands` inside `blob-theme-set`: by a short applier listed in `post_theme_commands` inside `blob-theme-set`:
`blob-theme-foot`, `blob-theme-browser`, `blob-restart-terminal`, `blob-theme-foot`, `blob-theme-browser`, `blob-restart-terminal`,
`blob-restart-btop`, `blob-hypr-restart`. `blob-restart-btop`, `blob-hypr-restart`, `blob-theme-gtk`.
## GTK apps
Nautilus and the other GTK apps read two things. The palette comes from
`gtk.css`, linked into both `~/.config/gtk-3.0/gtk.css` and
`~/.config/gtk-4.0/gtk.css`, which redefines the libadwaita named colors
(`window_bg_color`, `headerbar_bg_color`, `accent_bg_color` and the rest) so a
stock Adwaita app is drawn in the theme's own colors. The light or dark
preference comes from `blob-theme-gtk`, which reads the theme's `mode` and sets
`org.gnome.desktop.interface color-scheme` and `gtk-theme` through `gsettings`.
The preference applies to running apps immediately; the CSS is read at startup,
so an app that was already open keeps its old colors until it is restarted.
Adding a template for another app means dropping a `.tpl` in `default/themed/` Adding a template for another app means dropping a `.tpl` in `default/themed/`
and, if it cannot include a file, adding an applier to that list. and, if it cannot include a file, adding an applier to that list.
## Tokens
A template may reference `{{ key }}` for any resolved palette key, plus three
derived forms: `{{ key_strip }}` without the leading `#`, `{{ key_rgb }}` as
`R,G,B`, and `{{ key_ansi }}` as `38;2;R;G;B`, which is what a terminal program
that takes a raw escape sequence rather than a hex color needs.
## About
`fastfetch.jsonc` is linked to `~/.config/fastfetch/config.jsonc`. It draws
`~/.config/blob/branding/about.txt` as the logo, with 2 columns of padding to
its left and 6 between it and the modules - the exact numbers
`blob-launch-about` measures the About window against, so replacing the logo
resizes the window to fit it. Title and keys take the theme's accent.
A config of your own at that path is a regular file rather than the symlink, and
`blob-launch-about` leaves the window sizing alone when it finds one.
## The prompt
`starship.toml` is linked to `~/.config/starship.toml`. It carries a
`[palettes.blob]` block built from the theme - `accent`, `foreground`, `muted`,
`success`, `warning`, `error` - and every style in the file names one of those
rather than a literal color, so the prompt moves with the theme. Starship reads
its config on each prompt, so a theme change shows up on the next command, in
shells that are already open.
`starship init bash` runs from `session/bashrc`, which `install.sh` sources from
`~/.bashrc` with a single line. That file returns immediately in a
non-interactive shell, so it cannot disturb `scp` or `rsync`, and it also
sources `session/env-bootstrap` so `blob` is on `PATH` in any interactive shell.
## The three ways to set a theme ## The three ways to set a theme
| Command | Does | | Command | Does |
@@ -66,3 +119,8 @@ Twenty-two came across from upstream, plus `flats` and `pitch-dark`. Their
`backgrounds/` directories are empty on purpose: the upstream images were `backgrounds/` directories are empty on purpose: the upstream images were
Omarchy branding, and `~/wallpapers` holds 103 of your own. A theme with no Omarchy branding, and `~/wallpapers` holds 103 of your own. A theme with no
background of its own leaves the current wallpaper alone. background of its own leaves the current wallpaper alone.
Because of that, a machine with no wallpaper yet would get none from the theme
either. The installer covers the gap: it symlinks `~/wallpapers` to the
checkout, applies `flats`, and points the background symlink at the first image
it finds. See [install.md](install.md).
+20 -2
View File
@@ -48,6 +48,7 @@ source "$repo_dir/install/steps/directories.sh"
source "$repo_dir/install/steps/packages.sh" source "$repo_dir/install/steps/packages.sh"
source "$repo_dir/install/steps/config.sh" source "$repo_dir/install/steps/config.sh"
source "$repo_dir/install/steps/services.sh" source "$repo_dir/install/steps/services.sh"
source "$repo_dir/install/steps/theme.sh"
source "$repo_dir/install/steps/login.sh" source "$repo_dir/install/steps/login.sh"
say "Blob installer" say "Blob installer"
@@ -57,15 +58,23 @@ if [[ $check == true ]]; then
fi fi
say "" say ""
link_blob_path
create_directories create_directories
link_blob_path
install_packages install_packages
install_shipped_config install_shipped_config
refresh_font_cache
install_shell_profile
install_bash_integration
install_user_units install_user_units
install_systemd_dropins install_systemd_dropins
install_zen_config install_zen_config
link_wallpapers
install_default_theme
install_default_background
link_app_themes
enable_system_services enable_system_services
mask_system_services mask_system_services
enable_user_units enable_user_units
@@ -74,7 +83,9 @@ install_login_config
install_session_entry install_session_entry
install_autologin install_autologin
enable_display_manager enable_display_manager
report_greeter_compositor
report_login_conflicts report_login_conflicts
report_desktop_essentials
say "" say ""
if [[ $check == true ]]; then if [[ $check == true ]]; then
@@ -82,7 +93,14 @@ if [[ $check == true ]]; then
(( changes == 0 )) || exit 1 (( changes == 0 )) || exit 1
else else
say "$changes item(s) changed." say "$changes item(s) changed."
say "Set a theme with 'blob theme set <name>' once the session is up."
active_theme="$(cat "$current_state_dir/theme.name" 2>/dev/null || true)"
if [[ -n $active_theme ]]; then
say "Theme: $active_theme. Change it with 'blob theme set <name>'."
fi
say "New shells find the blob command through /etc/profile.d/blob.sh."
say "For this shell: source $blob_path/session/env-bootstrap"
say "" say ""
launch_desktop launch_desktop
fi fi
+16 -1
View File
@@ -8,8 +8,23 @@ note_change() {
changes=$((changes + 1)) changes=$((changes + 1))
} }
unit_search_dirs=(
/etc/systemd/system
/run/systemd/system
/usr/local/lib/systemd/system
/usr/lib/systemd/system
)
system_unit_exists() { system_unit_exists() {
systemctl cat -- "$1" &>/dev/null local unit="$1" dir
systemctl cat -- "$unit" &>/dev/null && return 0
for dir in "${unit_search_dirs[@]}"; do
[[ -e $dir/$unit ]] && return 0
done
return 1
} }
user_manager_available() { user_manager_available() {
+45
View File
@@ -1,6 +1,10 @@
# BLOB_PATH is a symlink to this checkout rather than a copy, so bin/, shell/, # BLOB_PATH is a symlink to this checkout rather than a copy, so bin/, shell/,
# themes/ and default/ are always the working tree. Every path the shell # themes/ and default/ are always the working tree. Every path the shell
# resolves as $BLOB_PATH/... therefore needs no install step at all. # resolves as $BLOB_PATH/... therefore needs no install step at all.
#
# ~/.local/share does not exist on a fresh Arch install, and ln does not create
# the parent of its link, so make it here rather than relying on another step
# having run first.
link_blob_path() { link_blob_path() {
local current="" local current=""
[[ -L $blob_path ]] && current="$(readlink -f "$blob_path")" [[ -L $blob_path ]] && current="$(readlink -f "$blob_path")"
@@ -22,6 +26,7 @@ link_blob_path() {
return 0 return 0
fi fi
mkdir -p "$(dirname "$blob_path")"
ln -sfn "$repo_dir" "$blob_path" ln -sfn "$repo_dir" "$blob_path"
say "link BLOB_PATH -> $repo_dir" say "link BLOB_PATH -> $repo_dir"
} }
@@ -58,6 +63,46 @@ install_shipped_config() {
install_file "$repo_dir/fonts/omarchy.ttf" "$font_dir/omarchy.ttf" "fonts/omarchy.ttf" install_file "$repo_dir/fonts/omarchy.ttf" "$font_dir/omarchy.ttf" "fonts/omarchy.ttf"
} }
bashrc_line='[ -r "$HOME/.local/share/blob/session/bashrc" ] && . "$HOME/.local/share/blob/session/bashrc"'
install_bash_integration() {
local bashrc="$HOME/.bashrc"
if [[ -f $bashrc ]] && grep -Fqx "$bashrc_line" "$bashrc"; then
say "ok bashrc"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would source session/bashrc from ~/.bashrc"
return 0
fi
[[ -f $bashrc ]] && cp "$bashrc" "$bashrc.bak"
printf '\n%s\n' "$bashrc_line" >>"$bashrc"
say "write bashrc"
}
refresh_font_cache() {
if ! command -v fc-cache &>/dev/null; then
say "SKIP font cache (fc-cache not installed)"
return 0
fi
if [[ $check == true ]]; then
say "would refresh the font cache"
return 0
fi
fc-cache -f "$font_dir" >/dev/null 2>&1 || true
say "ok font cache"
}
install_shell_profile() {
install_root_file "$repo_dir/session/profile.sh" /etc/profile.d/blob.sh "profile.d/blob.sh"
}
install_zen_config() { install_zen_config() {
local profile local profile
profile="$(find "$config_dir/zen" -maxdepth 1 -type d -name '*.Default (release)*' 2>/dev/null | head -1)" profile="$(find "$config_dir/zen" -maxdepth 1 -type d -name '*.Default (release)*' 2>/dev/null | head -1)"
+1 -1
View File
@@ -17,7 +17,7 @@ directories=(
"$HOME/.local/share/fonts" "$HOME/.local/share/fonts"
"$HOME/.local/share/applications" "$HOME/.local/share/applications"
"$HOME/.local/share/icons/hicolor/256x256/apps" "$HOME/.local/share/icons/hicolor/256x256/apps"
"$HOME/wallpapers" "$HOME/.cache/blob"
) )
create_directories() { create_directories() {
+48 -10
View File
@@ -1,21 +1,28 @@
session_entry_dir=/usr/local/share/wayland-sessions session_entry_dir=/usr/local/share/wayland-sessions
session_entry_search_dirs=(/usr/local/share/wayland-sessions /usr/share/wayland-sessions) session_entry_search_dirs=(/usr/local/share/wayland-sessions /usr/share/wayland-sessions)
greeter_config_dir=/etc/sddm.conf.d greeter_config_dir=/etc/sddm.conf.d
greeter_compositor_config=/etc/sddm/hyprland-greeter.conf greeter_compositor_config=/etc/sddm/hyprland-greeter.lua
# SDDM reads /etc/sddm.conf.d in alphabetical order and the last file wins, so # SDDM reads /etc/sddm.conf.d in alphabetical order and the last file wins, so
# Blob's drop-ins are named to sort after anything a distribution, an installer # Blob's drop-ins are named to sort after anything a distribution, an installer
# ISO, or a previous desktop left behind. # ISO, or a previous desktop left behind.
blob_greeter_conf="$greeter_config_dir/zz-blob.conf" blob_greeter_conf="$greeter_config_dir/zz-blob.conf"
blob_autologin_conf="$greeter_config_dir/zzz-blob-autologin.conf" blob_autologin_conf="$greeter_config_dir/zzz-blob-autologin.conf"
greeter_gpu_detector=/usr/local/bin/blob-hw-gpu-accelerated
# SDDM's stock Wayland greeter runs on weston, which Blob does not install. # SDDM's stock Wayland greeter runs on weston, which Blob does not install.
# Point it at Hyprland with a greeter-only config instead. # Point it at Hyprland with a greeter-only config instead. That config is Lua:
# Hyprland dropped the old hyprlang format, and a config it cannot parse leaves
# the greeter with no compositor and the screen black.
install_login_config() { install_login_config() {
install_root_file "$repo_dir/default/sddm/hyprland-greeter.conf" \ install_root_file "$repo_dir/default/sddm/hyprland-greeter.lua" \
"$greeter_compositor_config" "sddm/hyprland-greeter.conf" "$greeter_compositor_config" "sddm/hyprland-greeter.lua"
install_root_file "$repo_dir/default/sddm/zz-blob.conf" \ install_root_file "$repo_dir/default/sddm/zz-blob.conf" \
"$blob_greeter_conf" "sddm.conf.d/zz-blob.conf" "$blob_greeter_conf" "sddm.conf.d/zz-blob.conf"
install_root_file "$repo_dir/bin/blob-hw-gpu-accelerated" \
"$greeter_gpu_detector" "blob-hw-gpu-accelerated" 0755
install_root_file "$repo_dir/default/pam.d/blob-lock-password" \
/etc/pam.d/blob-lock-password "pam.d/blob-lock-password"
} }
install_session_entry() { install_session_entry() {
@@ -52,6 +59,22 @@ setting_value() {
sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//p" "$file" | tail -1 sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//p" "$file" | tail -1
} }
# SDDM runs CompositorCommand to get a Wayland greeter on screen. When that
# binary is not installed SDDM fails with no greeter at all, which reads as a
# machine that boots to a console for no reason, so name it here instead.
report_greeter_compositor() {
local command_line binary
command_line="$(setting_value "$repo_dir/default/sddm/zz-blob.conf" CompositorCommand)"
binary="${command_line%% *}"
[[ -n $binary ]] || return 0
command -v "$binary" &>/dev/null && return 0
say "WARN the greeter's compositor command '$binary' is not installed"
say " SDDM cannot draw a greeter without it; it is set in $blob_greeter_conf"
}
# zz-blob.conf blanks Theme and Autologin, so a leftover drop-in cannot point the # zz-blob.conf blanks Theme and Autologin, so a leftover drop-in cannot point the
# greeter at a theme that was uninstalled or autolog into a session that no # greeter at a theme that was uninstalled or autolog into a session that no
# longer exists. Either one leaves SDDM on a black screen it never falls back # longer exists. Either one leaves SDDM on a black screen it never falls back
@@ -83,7 +106,13 @@ report_login_conflicts() {
# its default target is multi-user.target, which never pulls in graphical.target. # its default target is multi-user.target, which never pulls in graphical.target.
enable_display_manager() { enable_display_manager() {
if ! system_unit_exists sddm.service; then if ! system_unit_exists sddm.service; then
say "WARN sddm.service not found; the desktop cannot start at boot" if package_installed sddm; then
say "WARN sddm is installed but sddm.service is not visible to systemd"
say " run 'sudo systemctl daemon-reload', then ./install.sh again"
else
say "WARN sddm is not installed; the desktop cannot start at boot"
say " install it with 'sudo pacman -S sddm', then ./install.sh again"
fi
return 0 return 0
fi fi
@@ -112,13 +141,19 @@ enable_display_manager() {
fi fi
if [[ $enable_needed == true ]]; then if [[ $enable_needed == true ]]; then
as_root systemctl enable sddm.service if as_root systemctl enable sddm.service; then
say "enable sddm.service" say "enable sddm.service"
else
say "WARN could not enable sddm.service"
fi
fi fi
if [[ $target_needed == true ]]; then if [[ $target_needed == true ]]; then
as_root systemctl set-default graphical.target if as_root systemctl set-default graphical.target; then
say "set default target to graphical.target" say "set default target to graphical.target"
else
say "WARN could not set the default target to graphical.target"
fi
fi fi
} }
@@ -135,6 +170,7 @@ launch_desktop() {
fi fi
if ! system_unit_exists sddm.service; then if ! system_unit_exists sddm.service; then
say "No login screen to start: sddm is not installed."
return 0 return 0
fi fi
@@ -149,5 +185,7 @@ launch_desktop() {
fi fi
say "Starting the login screen. Pick the Blob session to log in." say "Starting the login screen. Pick the Blob session to log in."
as_root systemctl start sddm.service as_root systemctl start sddm.service || {
say "WARN sddm.service did not start; see 'systemctl status sddm'"
}
} }
+72 -4
View File
@@ -3,6 +3,21 @@ read_package_list() {
grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$1" grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$1"
} }
package_installed() {
local name="$1" member
pacman -Q "$name" &>/dev/null && return 0
pacman -T "$name" &>/dev/null && return 0
local -a members=()
mapfile -t members < <(pacman -Sg "$name" 2>/dev/null | awk '{print $2}')
(( ${#members[@]} )) || return 1
for member in "${members[@]}"; do
pacman -Q "$member" &>/dev/null || return 1
done
}
# A list entry may be "wanted|already-fine", which asks for the first name but # A list entry may be "wanted|already-fine", which asks for the first name but
# treats either as satisfying the requirement. That is how the AUR replacement # treats either as satisfying the requirement. That is how the AUR replacement
# for a package still shipped by another repository avoids a file conflict. # for a package still shipped by another repository avoids a file conflict.
@@ -11,8 +26,8 @@ missing_packages() {
for entry in $(read_package_list "$1"); do for entry in $(read_package_list "$1"); do
wanted=${entry%%|*} wanted=${entry%%|*}
alternative=${entry#*|} alternative=${entry#*|}
pacman -Q "$wanted" &>/dev/null && continue package_installed "$wanted" && continue
[[ $alternative != "$entry" ]] && pacman -Q "$alternative" &>/dev/null && continue [[ $alternative != "$entry" ]] && package_installed "$alternative" && continue
printf '%s\n' "$wanted" printf '%s\n' "$wanted"
done done
} }
@@ -43,7 +58,7 @@ install_packages() {
if (( ${#repo_missing[@]} )); then if (( ${#repo_missing[@]} )); then
say "install ${#repo_missing[@]} repo package(s)" say "install ${#repo_missing[@]} repo package(s)"
as_root pacman -S --needed --noconfirm "${repo_missing[@]}" install_repo_packages "${repo_missing[@]}"
fi fi
if (( ${#aur_missing[@]} )); then if (( ${#aur_missing[@]} )); then
@@ -52,10 +67,63 @@ install_packages() {
return 0 return 0
} }
say "install ${#aur_missing[@]} AUR package(s)" say "install ${#aur_missing[@]} AUR package(s)"
yay -S --needed --noconfirm "${aur_missing[@]}" install_aur_packages "${aur_missing[@]}"
fi fi
} }
install_repo_packages() {
local package
local -a failed=()
as_root pacman -S --needed --noconfirm "$@" && return 0
say "WARN the batch install failed; refreshing the package databases"
as_root pacman -Sy --noconfirm || true
as_root pacman -S --needed --noconfirm "$@" && return 0
say "WARN still failing; retrying one package at a time"
for package in "$@"; do
as_root pacman -S --needed --noconfirm "$package" || failed+=("$package")
done
(( ${#failed[@]} == 0 )) && return 0
say "WARN ${#failed[@]} repo package(s) failed to install: ${failed[*]}"
}
desktop_essentials=(sddm hyprland quickshell uwsm)
report_desktop_essentials() {
local package
local -a missing=()
for package in "${desktop_essentials[@]}"; do
package_installed "$package" || missing+=("$package")
done
if (( ${#missing[@]} == 0 )); then
say "ok desktop essentials"
return 0
fi
say "WARN the desktop cannot start without: ${missing[*]}"
say " install them with 'sudo pacman -S ${missing[*]}', then ./install.sh again"
}
install_aur_packages() {
local package
local -a failed=()
for package in "$@"; do
yay -S --needed --noconfirm "$package" || failed+=("$package")
done
(( ${#failed[@]} == 0 )) && return 0
say "WARN ${#failed[@]} AUR package(s) failed to build: ${failed[*]}"
say "WARN the install continued; retry them with 'yay -S <name>'"
}
# A fresh Arch install has no AUR helper, and yay is not in the official # A fresh Arch install has no AUR helper, and yay is not in the official
# repositories, so it is built from the AUR once with makepkg. Everything after # repositories, so it is built from the AUR once with makepkg. Everything after
# this point can then use yay normally. # this point can then use yay normally.
+7
View File
@@ -11,8 +11,15 @@ system_services=(
# network-online.target must not hold up graphical.target waiting for DHCP or # network-online.target must not hold up graphical.target waiting for DHCP or
# Wi-Fi association. Nothing in the session blocks on the network. # Wi-Fi association. Nothing in the session blocks on the network.
#
# Blob configures no boot splash: no mkinitcpio hook, no kernel command line,
# and no theme. An unconfigured plymouth still takes the console at boot and
# never hands it back, and getty@tty1 waits on plymouth-quit-wait.service, so
# the machine comes up with no greeter and no TTY to fix it from.
masked_services=( masked_services=(
NetworkManager-wait-online.service NetworkManager-wait-online.service
plymouth-start.service
plymouth-quit-wait.service
) )
# blob-speaker-tuning.service is deliberately absent: blob-audio-tuning installs # blob-speaker-tuning.service is deliberately absent: blob-audio-tuning installs
+144
View File
@@ -0,0 +1,144 @@
default_theme=flats
wallpaper_dir="$HOME/wallpapers"
current_state_dir="$HOME/.local/state/blob/current"
# ~/wallpapers is what the wallpaper picker and `blob wallpaper set` read, and
# the checkout already carries the collection. Link it for the same reason
# BLOB_PATH is a link: one copy, and edits in the checkout are live.
link_wallpapers() {
local source="$repo_dir/wallpapers"
local current=""
[[ -L $wallpaper_dir ]] && current="$(readlink -f "$wallpaper_dir")"
if [[ $current == "$source" ]]; then
say "ok wallpapers"
return 0
fi
if [[ -d $wallpaper_dir && ! -L $wallpaper_dir ]] && [[ -n "$(ls -A "$wallpaper_dir")" ]]; then
say "ok wallpapers (own directory kept)"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would link $wallpaper_dir -> $source"
return 0
fi
[[ -d $wallpaper_dir && ! -L $wallpaper_dir ]] && rmdir "$wallpaper_dir"
ln -sfn "$source" "$wallpaper_dir"
say "link wallpapers -> $source"
}
# Without a theme there is no palette, no themed app config, and no background
# for the shell to draw, so the first login lands on a black screen that looks
# like a failed session. Applied headless: no shell and no session bus exist
# while the installer runs.
install_default_theme() {
local current_name=""
[[ -s $current_state_dir/theme.name ]] && current_name="$(cat "$current_state_dir/theme.name")"
if [[ -n $current_name && -f $current_state_dir/theme/colors.toml ]]; then
say "ok theme ($current_name)"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would apply the $default_theme theme"
return 0
fi
if BLOB_PATH="$blob_path" BLOB_THEME_HEADLESS=1 PATH="$repo_dir/bin:$PATH" \
blob-theme-set "$default_theme" >/dev/null; then
say "theme $default_theme"
else
say "WARN could not apply the $default_theme theme"
say " the session will come up without a palette; run 'blob theme set $default_theme'"
fi
}
# Only a theme that ships its own backgrounds/ sets this link, and most ship
# none: they are meant to leave the current wallpaper alone. On a first install
# there is no current wallpaper, so pick one.
install_default_background() {
local link="$current_state_dir/background"
local search="$wallpaper_dir"
local first
if [[ -e $link ]]; then
say "ok background"
return 0
fi
# --check writes nothing, so the wallpaper link is not there yet to look in.
[[ -d $search ]] || search="$repo_dir/wallpapers"
first="$(find -L "$search" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) \
2>/dev/null | sort | head -1)"
if [[ -z $first ]]; then
say "SKIP background (no image in $search)"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would set the background to $(basename "$first")"
return 0
fi
ln -nsf "$first" "$link"
say "bg $(basename "$first")"
}
# The theme pipeline renders one file per app into the current-theme directory,
# but each app only ever reads its own config tree. These links are what join
# the two. Without them a theme switch silently leaves the app on stock colors,
# because nothing errors: the app just never sees the generated file.
link_theme_file() {
local theme_file="$1" dest="$2" label="$3"
local target="$current_state_dir/theme/$theme_file"
if [[ -L $dest ]] && [[ "$(readlink "$dest")" == "$target" ]]; then
say "ok $label"
return 0
fi
if [[ -e $dest && ! -L $dest ]] && [[ $force == false ]]; then
note_change
say "DIFF $label (own file kept; --force to replace)"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would link $label"
return 0
fi
mkdir -p "$(dirname "$dest")"
ln -nsf "$target" "$dest"
say "link $label"
}
# btop reads color_theme = "current" out of its own themes directory, and
# Neovim loads whatever theme.lua its plugin directory holds. Neovim is only
# linked when that directory already exists: Blob ships no Neovim config, so
# creating the tree would be inventing a layout the user has not chosen.
link_app_themes() {
link_theme_file btop.theme "$config_dir/btop/themes/current.theme" "btop theme"
link_theme_file gtk.css "$config_dir/gtk-3.0/gtk.css" "gtk-3.0 theme"
link_theme_file gtk.css "$config_dir/gtk-4.0/gtk.css" "gtk-4.0 theme"
link_theme_file starship.toml "$config_dir/starship.toml" "starship prompt"
link_theme_file fastfetch.jsonc "$config_dir/fastfetch/config.jsonc" "fastfetch"
if [[ -d $config_dir/nvim/lua/plugins ]]; then
link_theme_file neovim.lua "$config_dir/nvim/lua/plugins/theme.lua" "neovim theme"
else
say "SKIP neovim theme (no ~/.config/nvim/lua/plugins)"
fi
}
+5 -2
View File
@@ -3,8 +3,11 @@
# #
# These have no official-repo equivalent. See docs/packages.md. # These have no official-repo equivalent. See docs/packages.md.
# Palette extraction behind blob-wallpaper-set # Palette extraction behind blob-wallpaper-set; pywal16 is the maintained fork
python-pywal python-pywal16|python-pywal
# Terminal text effects behind the screensaver; ttfx was the Omarchy build
python-terminaltexteffects
# Terminal dispatch; every TUI launcher's Exec= line calls it # Terminal dispatch; every TUI launcher's Exec= line calls it
xdg-terminal-exec xdg-terminal-exec
+1
View File
@@ -6,6 +6,7 @@
# Compositor and shell # Compositor and shell
hyprland hyprland
mesa
quickshell quickshell
uwsm uwsm
sddm sddm
+10
View File
@@ -0,0 +1,10 @@
case $- in
*i*) ;;
*) return 0 ;;
esac
[ -r "$HOME/.local/share/blob/session/env-bootstrap" ] && . "$HOME/.local/share/blob/session/env-bootstrap"
command -v starship >/dev/null 2>&1 && eval "$(starship init bash)"
command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init bash --cmd cd)"
+6
View File
@@ -0,0 +1,6 @@
# Put the blob command on PATH for login shells. The graphical session gets the
# same environment from ~/.config/uwsm/env.d/10-blob.
if [ -r "$HOME/.local/share/blob/session/env-bootstrap" ]; then
. "$HOME/.local/share/blob/session/env-bootstrap"
fi
+1 -1
View File
@@ -1,2 +1,2 @@
export TERMINAL=xdg-terminal-exec export TERMINAL=blob-terminal-exec
export EDITOR="blob-launch-editor --inline" export EDITOR="blob-launch-editor --inline"
+1 -1
View File
@@ -3,7 +3,7 @@
if [ -f "${BLOB_PATH%/}/session/uwsm/default" ]; then if [ -f "${BLOB_PATH%/}/session/uwsm/default" ]; then
. "${BLOB_PATH%/}/session/uwsm/default" . "${BLOB_PATH%/}/session/uwsm/default"
else else
export TERMINAL=xdg-terminal-exec export TERMINAL=blob-terminal-exec
export EDITOR="blob-launch-editor --inline" export EDITOR="blob-launch-editor --inline"
fi fi
+6 -2
View File
@@ -39,8 +39,12 @@ BarWidget {
Process { Process {
id: updateProc id: updateProc
command: ["blob-update-available"] command: ["blob-update-available"]
onExited: function(exitCode) { stdout: StdioCollector {
root.updateAvailable = exitCode === 0 waitForEnd: true
onStreamFinished: {
const pending = parseInt(String(text).trim(), 10)
root.updateAvailable = !isNaN(pending) && pending > 0
}
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@ BarWidget {
id: root id: root
moduleName: "blob.menu" moduleName: "blob.menu"
readonly property string iconPath: Quickshell.env("HOME") + "/.config/blob/branding/blob_icon.svg" readonly property string iconPath: Quickshell.env("HOME") + "/.config/blob/branding/chai.svg"
readonly property int iconSize: Math.round(Style.font.body * 1.35) readonly property int iconSize: Math.round(Style.font.body * 1.35)
property string iconSvg: "" property string iconSvg: ""
@@ -48,7 +48,7 @@ BarWidget {
horizontalMargin: 7.5 horizontalMargin: 7.5
onPressed: function(button) { onPressed: function(button) {
if (!root.bar) return if (!root.bar) return
if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec") if (button === Qt.RightButton) root.bar.run("blob-terminal-exec")
else root.bar.run("blob-shell shell toggle blob.menu '{\"menu\":\"root\"}'") else root.bar.run("blob-shell shell toggle blob.menu '{\"menu\":\"root\"}'")
} }
+1
View File
@@ -1243,6 +1243,7 @@ Item {
font.family: row.iconFont.length > 0 ? row.iconFont : root.fontFamily font.family: row.iconFont.length > 0 ? row.iconFont : root.fontFamily
font.pixelSize: Style.font.iconLarge font.pixelSize: Style.font.iconLarge
width: Style.space(36) width: Style.space(36)
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
anchors.left: parent.left anchors.left: parent.left
@@ -20,7 +20,7 @@ Item {
Process { Process {
id: listProc id: listProc
command: ["bash", "-c", command: ["bash", "-c",
'find "$1" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" ' + 'find -L "$1" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" ' +
'-o -iname "*.png" -o -iname "*.webp" -o -iname "*.gif" \\) | sort', '-o -iname "*.png" -o -iname "*.webp" -o -iname "*.gif" \\) | sort',
"--", root.directory] "--", root.directory]
stdout: StdioCollector { stdout: StdioCollector {
@@ -15,6 +15,13 @@ Item {
readonly property string pluginId: (manifest && manifest.id) || "blob.quick-settings" readonly property string pluginId: (manifest && manifest.id) || "blob.quick-settings"
readonly property int barInset: {
var bar = root.shell ? root.shell.bar : null
if (!bar || bar.barHidden === true) return 0
if (String(bar.position || "top") !== "top") return 0
return Math.max(0, bar.barSize || 0)
}
function open(payloadJson) { function open(payloadJson) {
root.opened = true root.opened = true
} }
@@ -185,83 +192,82 @@ Item {
onClicked: root.dismiss() onClicked: root.dismiss()
} }
Row { WeatherCard {
anchors.top: controlCentre.top
anchors.right: controlCentre.left
anchors.rightMargin: Style.spaceReal(12)
available: root.weatherAvailable
glyph: root.weatherGlyph
place: root.weatherPlace
temperature: root.weatherTemperature
wind: root.weatherWind
}
Card {
id: controlCentre
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Style.spaceReal(10) anchors.topMargin: Style.spaceReal(10) + root.barInset
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Style.spaceReal(400)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(12) spacing: Style.spaceReal(12)
WeatherCard { Header {
available: root.weatherAvailable width: parent.width
glyph: root.weatherGlyph clock: clockValue.value
place: root.weatherPlace today: dateValue.value
temperature: root.weatherTemperature onLockRequested: root.runAndDismiss("blob-system-lock")
wind: root.weatherWind onLogoutRequested: root.runAndDismiss("blob-system-logout")
onCloseRequested: root.dismiss()
} }
Card { Calendar {
id: controlCentre width: parent.width
implicitWidth: Style.spaceReal(400) polling: root.opened
fillColor: Util.alpha(Color.background, Style.panelFillAlpha) }
spacing: Style.spaceReal(12)
Header { Tiles {
width: parent.width bluetoothOn: bluetoothFlag.value
clock: clockValue.value doNotDisturb: dndFlag.value
today: dateValue.value nightLight: nightLightFlag.value
onLockRequested: root.runAndDismiss("blob-system-lock") recording: recordingFlag.value
onLogoutRequested: root.runAndDismiss("blob-system-logout") stayAwake: stayAwakeFlag.value
onCloseRequested: root.dismiss() tabletFollow: tabletFlag.value
onRunRequested: function(command) { root.runAndDismiss(command) }
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
onToggleRequested: function(command) { root.toggleAndRefresh(command) }
}
SliderRow {
width: parent.width
icon: mutedFlag.value ? "" : ""
dimmed: mutedFlag.value
value: Number(volumeValue.value) || 0
onIconActivated: root.toggleAndRefresh("pamixer --toggle-mute")
onValueRequested: function(next) {
root.run("pamixer --set-volume " + next)
volumeValue.value = String(next)
} }
}
Calendar { SliderRow {
width: parent.width width: parent.width
polling: root.opened icon: ""
value: Number(brightnessValue.value) || 0
onValueRequested: function(next) {
root.run("brightnessctl set " + next + "%")
brightnessValue.value = String(next)
} }
}
Tiles { MediaRow {
bluetoothOn: bluetoothFlag.value width: parent.width
doNotDisturb: dndFlag.value title: root.mediaParts.length > 0 ? root.mediaParts[0] : ""
nightLight: nightLightFlag.value artist: root.mediaParts.length > 1 ? root.mediaParts[1] : ""
recording: recordingFlag.value status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped"
stayAwake: stayAwakeFlag.value onControlRequested: function(action) {
tabletFollow: tabletFlag.value root.run("playerctl " + action)
onRunRequested: function(command) { root.runAndDismiss(command) } stateSettleTimer.restart()
onPanelRequested: function(pluginId) { root.openPanel(pluginId) }
onToggleRequested: function(command) { root.toggleAndRefresh(command) }
}
SliderRow {
width: parent.width
icon: mutedFlag.value ? "" : ""
dimmed: mutedFlag.value
value: Number(volumeValue.value) || 0
onIconActivated: root.toggleAndRefresh("pamixer --toggle-mute")
onValueRequested: function(next) {
root.run("pamixer --set-volume " + next)
volumeValue.value = String(next)
}
}
SliderRow {
width: parent.width
icon: ""
value: Number(brightnessValue.value) || 0
onValueRequested: function(next) {
root.run("brightnessctl set " + next + "%")
brightnessValue.value = String(next)
}
}
MediaRow {
width: parent.width
title: root.mediaParts.length > 0 ? root.mediaParts[0] : ""
artist: root.mediaParts.length > 1 ? root.mediaParts[1] : ""
status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped"
onControlRequested: function(action) {
root.run("playerctl " + action)
stateSettleTimer.restart()
}
} }
} }
} }
+30 -22
View File
@@ -1,23 +1,31 @@
accent = "#7babe3" mode = "dark"
cursor = "#cbd2d9"
foreground = "#cbd2d9"
background = "#0f1012"
selection_foreground = "#0f1012"
selection_background = "#7babe3"
color0 = "#000000" accent = "#5fa3e8"
color1 = "#d53838" selection = "#1e2228"
color2 = "#38d4d5" muted = "#5c656e"
color3 = "#d58638"
color4 = "#d53886" background = "#0f1012"
color5 = "#38d586" dark_background = "#0a0b0c"
color6 = "#3886d5" darker_background = "#050607"
color7 = "#d4d4d4" lighter_background = "#191c20"
color8 = "#949494"
color9 = "#d53838" foreground = "#cbd2d9"
color10 = "#38d4d5" dark_foreground = "#78828c"
color11 = "#d58638" light_foreground = "#dde3e9"
color12 = "#d53886" bright_foreground = "#f0f4f8"
color13 = "#38d586"
color14 = "#3886d5" red = "#d53838"
color15 = "#d4d4d4" yellow = "#d58638"
orange = "#d56338"
green = "#38d586"
cyan = "#38d4d5"
blue = "#3886d5"
magenta = "#d53886"
brown = "#6a321c"
bright_red = "#dd6060"
bright_yellow = "#dd9e60"
bright_green = "#60dd9e"
bright_cyan = "#60dddd"
bright_blue = "#609edd"
bright_magenta = "#dd609e"
+15
View File
@@ -7,6 +7,9 @@ config_dir="$HOME/.config"
session_entry="/usr/local/share/wayland-sessions/blob.desktop" session_entry="/usr/local/share/wayland-sessions/blob.desktop"
root_files=( root_files=(
/etc/profile.d/blob.sh
/usr/local/bin/blob-hw-gpu-accelerated
/etc/pam.d/blob-lock-password
/etc/sddm.conf.d/zz-blob.conf /etc/sddm.conf.d/zz-blob.conf
/etc/sddm.conf.d/zzz-blob-autologin.conf /etc/sddm.conf.d/zzz-blob-autologin.conf
/etc/sddm/hyprland-greeter.conf /etc/sddm/hyprland-greeter.conf
@@ -92,6 +95,11 @@ if [[ -L $blob_path ]]; then
say "remove BLOB_PATH symlink" say "remove BLOB_PATH symlink"
fi fi
if [[ -L $HOME/wallpapers ]]; then
rm -f "$HOME/wallpapers"
say "remove ~/wallpapers symlink"
fi
for relative in blob/shell.json uwsm/default uwsm/env.d/10-blob; do for relative in blob/shell.json uwsm/default uwsm/env.d/10-blob; do
restore_or_remove "$config_dir/$relative" "$relative" restore_or_remove "$config_dir/$relative" "$relative"
done done
@@ -105,6 +113,13 @@ for lua in hyprland.lua monitors.lua input.lua looknfeel.lua bindings.lua autost
restore_or_remove "$config_dir/hypr/$lua" "hypr/$lua" restore_or_remove "$config_dir/hypr/$lua" "hypr/$lua"
done done
[[ -L $config_dir/starship.toml ]] && restore_or_remove "$config_dir/starship.toml" "starship.toml"
[[ -L $config_dir/fastfetch/config.jsonc ]] && restore_or_remove "$config_dir/fastfetch/config.jsonc" "fastfetch/config.jsonc"
for gtk in gtk-3.0 gtk-4.0; do
[[ -L $config_dir/$gtk/gtk.css ]] && restore_or_remove "$config_dir/$gtk/gtk.css" "$gtk/gtk.css"
done
if [[ $keep_state == false && -d $HOME/.local/state/blob ]]; then if [[ $keep_state == false && -d $HOME/.local/state/blob ]]; then
rm -rf "$HOME/.local/state/blob" rm -rf "$HOME/.local/state/blob"
say "remove ~/.local/state/blob" say "remove ~/.local/state/blob"