Start the desktop after install by enabling the login screen and services

This commit is contained in:
2026-09-20 06:40:41 -04:00
parent f4dd3b7532
commit 8f124f47c2
17 changed files with 784 additions and 279 deletions
+25 -5
View File
@@ -39,6 +39,7 @@ and nothing to re-apply after somebody else's release.
| `branding/` | icon, ASCII art, boot splash | | `branding/` | icon, ASCII art, boot splash |
| `wallpapers/` | 103 wallpapers, previewed in [the gallery](wallpaper-gallery/index.md) | | `wallpapers/` | 103 wallpapers, previewed in [the gallery](wallpaper-gallery/index.md) |
| `session/` | wayland session entry and uwsm environment | | `session/` | wayland session entry and uwsm environment |
| `install/` | the installer steps `install.sh` runs |
| `docs/` | everything below | | `docs/` | everything below |
## Docs ## Docs
@@ -51,6 +52,7 @@ and nothing to re-apply after somebody else's release.
| [themes.md](docs/themes.md) | the palette pipeline and how to add a theme | | [themes.md](docs/themes.md) | the palette pipeline and how to add a theme |
| [widgets.md](docs/widgets.md) | the ported GTK widgets and where they went | | [widgets.md](docs/widgets.md) | the ported GTK widgets and where they went |
| [menu.md](docs/menu.md) | the menu tree and what was trimmed | | [menu.md](docs/menu.md) | the menu tree and what was trimmed |
| [install.md](docs/install.md) | what the installer does, step by step |
| [packages.md](docs/packages.md) | leaving the Omarchy repository, and updating | | [packages.md](docs/packages.md) | leaving the Omarchy repository, and updating |
| [upstream.md](docs/upstream.md) | the fork base, for diffing later | | [upstream.md](docs/upstream.md) | the fork base, for diffing later |
@@ -61,16 +63,34 @@ and nothing to re-apply after somebody else's release.
files that must sit under `~/.config` get copied. files that must sit under `~/.config` get copied.
```bash ```bash
./install.sh # link and deploy ./install.sh # directories, packages, config, services, login screen
./install.sh --check # report what would change, write nothing ./install.sh --check # report what would change, write nothing
./install.sh --force # overwrite files with local changes ./install.sh --force # overwrite files with local changes
./install.sh --autologin # log this user straight into the Blob session
./install.sh --no-launch # do not open the login screen at the end
``` ```
Then log out and pick the Blob session. It runs these steps, in this order, and every one of them is idempotent:
To back out, `./uninstall.sh` removes the session entry, the config, and the | Step | What it does |
symlink, restoring any `.bak` the installer made. The checkout, the wallpapers, | --- | --- |
and the themes stay where they are. | 1. `BLOB_PATH` | symlinks `~/.local/share/blob` to this checkout |
| 2. Directories | creates the 19 directories the shell and scripts expect |
| 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` |
| 5. User units | installs the session services into `~/.config/systemd/user` and enables them |
| 6. systemd drop-ins | writes the shutdown timeouts, oomd thresholds, plocate prune paths, and sleep hooks under `/etc` |
| 7. System services | enables NetworkManager, resolved, bluetooth, cups, avahi, docker, power-profiles-daemon, and oomd, and masks `NetworkManager-wait-online` |
| 8. Login screen | installs the session entry and the SDDM config, enables `sddm.service`, and sets the default target to `graphical.target` |
| 9. 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
greeter runs on `weston`, which Blob does not install, so the greeter is pointed
at Hyprland instead. [install.md](docs/install.md) covers each step in full.
To back out, `./uninstall.sh` removes the session entry, the config, the user
services, and the symlink, restoring any `.bak` the installer made. The
checkout, the wallpapers, and the themes stay where they are.
## Regenerating docs ## Regenerating docs
+9
View File
@@ -0,0 +1,9 @@
[General]
DisplayServer=wayland
[Wayland]
CompositorCommand=start-hyprland -- --config /etc/sddm/hyprland-greeter.lua
[Users]
RememberLastUser=true
RememberLastSession=true
+11
View File
@@ -0,0 +1,11 @@
hl.config({
misc = {
disable_hyprland_logo = true,
disable_splash_rendering = true,
force_default_wallpaper = 0,
},
animations = {
enabled = false,
},
})
+21
View File
@@ -0,0 +1,21 @@
# Without an OOM daemon there is nothing between "memory is tight" and
# "processes start dying at random". The kernel OOM killer only fires once an
# allocation has already failed outright, which is far too late for the
# desktop: the machine thrashes in reclaim until something fails somewhere
# fatal. Seen in practice as Hyprland taking SIGBUS mid-memcpy from a Wayland
# client's shm pool, because the client was being torn down by the same memory
# pressure. One app misbehaving should not cost the whole session.
#
# systemd-oomd keys on PSI stall time rather than on free pages, so it acts
# while the machine is still thrashing instead of after an allocation failed.
# Deliberately not earlyoom: that fires only when free RAM *and* free swap are
# both under threshold, so a large, mostly-unused swapfile keeps it asleep
# through exactly this failure.
#
# 50% means half of a ten-second window was spent stalled on memory reclaim.
# Held for 20s straight, the desktop is already unusable and losing one app is
# the cheaper outcome. Which cgroups are eligible is set separately, in
# ~/.config/systemd/user/app.slice.d/10-oomd.conf.
[OOM]
DefaultMemoryPressureDurationSec=20s
DefaultMemoryPressureLimit=50%
@@ -11,7 +11,7 @@ ConditionPathExists=!%h/.local/state/blob/toggles/crash-capture-off
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/blob-crash-watch ExecStart=%h/.local/share/blob/bin/blob-crash-watch
Restart=always Restart=always
RestartSec=5 RestartSec=5
+1 -1
View File
@@ -11,7 +11,7 @@ ConditionEnvironment=WAYLAND_DISPLAY
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/blob-system-sleep-monitor ExecStart=%h/.local/share/blob/bin/blob-system-sleep-monitor
Restart=always Restart=always
RestartSec=2 RestartSec=2
+75
View File
@@ -0,0 +1,75 @@
# Install
`./install.sh` takes a machine with nothing but Arch on it to a running Blob
desktop. Every step is idempotent, reports what it did, and can be previewed
with `--check`.
## Layout
| Path | Role |
| --- | --- |
| `install.sh` | flags and the order the steps run in |
| `install/helpers/lib.sh` | reporting, file copies, unit queries |
| `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/packages.sh` | repo and AUR packages, and the yay bootstrap |
| `install/steps/config.sh` | the `BLOB_PATH` symlink and everything under `~/.config` |
| `install/steps/services.sh` | system services, systemd drop-ins, user units |
| `install/steps/login.sh` | SDDM, the session entry, and the first launch |
## Steps
1. **`BLOB_PATH`** is symlinked to the checkout at `~/.local/share/blob`. Nothing
under `bin/`, `shell/`, `themes/` or `default/` is ever copied.
2. **Directories** are created, including `~/.config/systemd/user/app.slice.d`
and `~/wallpapers`.
3. **Packages** come from `packages/blob.packages` and
`packages/blob-aur.packages`. A machine with no AUR helper gets `yay-bin`
built once with `makepkg`. See [packages.md](packages.md).
4. **Config** is copied into `~/.config`: `hypr/`, the themed app configs,
`blob/shell.json`, the uwsm environment, and the icon font.
5. **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
session. `blob-speaker-tuning.service` is left out, because
`blob-audio-tuning` installs it only on machines with a tuning profile.
6. **systemd drop-ins** are written under `/etc`: the shutdown timeouts, the
oomd pressure thresholds, the plocate prune paths, and the sleep hooks.
7. **System services** are enabled: NetworkManager, resolved, bluetooth, cups,
avahi, docker, power-profiles-daemon, and oomd.
`NetworkManager-wait-online.service` is masked so a slow DHCP lease cannot
hold up the login screen.
8. **The login screen** is SDDM. Three pieces have to be in place:
| File | Why |
| --- | --- |
| `/usr/local/share/wayland-sessions/blob.desktop` | the session SDDM offers |
| `/etc/sddm.conf.d/10-blob.conf` | Wayland greeter, run on Hyprland, remember the last session |
| `/etc/sddm/hyprland-greeter.lua` | the greeter's own tiny Hyprland config |
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
to draw anything. `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.
9. **Launch.** When the installer was run from a 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/99-blob-autologin.conf` for the current user.
## Flags
| Flag | Effect |
| --- | --- |
| `--check` | report every change, write nothing, exit 1 if anything differs |
| `--force` | overwrite files that differ from the repo copy |
| `--skip-packages` | directories, config and services only |
| `--autologin` | skip the SDDM prompt for this user |
| `--no-launch` | leave the login screen for the next reboot |
## Uninstall
`./uninstall.sh` removes the config, the user units, the `/etc` drop-ins, the
session entry and the symlink, restoring any `.bak` the installer left.
`--keep-session` keeps the session entry and the SDDM config, `--keep-state`
keeps `~/.local/state/blob`. `sddm.service` is left enabled either way, so the
machine still reaches a login screen.
+29 -268
View File
@@ -5,22 +5,27 @@ set -e
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
blob_path="$HOME/.local/share/blob" blob_path="$HOME/.local/share/blob"
config_dir="$HOME/.config" config_dir="$HOME/.config"
state_dir="$HOME/.local/state/blob"
session_dir="/usr/share/wayland-sessions"
font_dir="$HOME/.local/share/fonts" font_dir="$HOME/.local/share/fonts"
force=false force=false
check=false check=false
skip_packages=false skip_packages=false
no_launch=false
autologin=false
changes=0 changes=0
usage() { usage() {
cat <<USAGE cat <<USAGE
Usage: ./install.sh [OPTIONS] Usage: ./install.sh [OPTIONS]
Creates every directory Blob needs, installs the packages that are missing,
writes the config, enables the services, and opens the login screen.
--check Report what would change, write nothing --check Report what would change, write nothing
--force Overwrite files that have local changes --force Overwrite files that have local changes
--skip-packages Do not install packages, only directories and config --skip-packages Do not install packages, only directories and config
--autologin Log this user straight into the Blob session
--no-launch Do not start the login screen at the end
--help Show this message --help Show this message
USAGE USAGE
exit 0 exit 0
@@ -31,244 +36,19 @@ while (( $# > 0 )); do
--force) force=true; shift ;; --force) force=true; shift ;;
--check) check=true; shift ;; --check) check=true; shift ;;
--skip-packages) skip_packages=true; shift ;; --skip-packages) skip_packages=true; shift ;;
--autologin) autologin=true; shift ;;
--no-launch) no_launch=true; shift ;;
--help) usage ;; --help) usage ;;
*) echo "Unknown option: $1" >&2; usage ;; *) echo "Unknown option: $1" >&2; usage ;;
esac esac
done done
say() { source "$repo_dir/install/helpers/lib.sh"
printf '%s\n' "$1" source "$repo_dir/install/steps/directories.sh"
} source "$repo_dir/install/steps/packages.sh"
source "$repo_dir/install/steps/config.sh"
note_change() { source "$repo_dir/install/steps/services.sh"
changes=$((changes + 1)) source "$repo_dir/install/steps/login.sh"
}
directories=(
"$HOME/.config/blob/hooks"
"$HOME/.config/blob/extensions"
"$HOME/.config/blob/plugins"
"$HOME/.config/blob/themed"
"$HOME/.config/blob/themes"
"$HOME/.config/blob/branding"
"$HOME/.config/hypr"
"$HOME/.config/uwsm/env.d"
"$HOME/.local/state/blob/toggles/hypr"
"$HOME/.local/state/blob/indicators"
"$HOME/.local/state/blob/notifications/history"
"$HOME/.local/state/blob/notifications/images"
"$HOME/.local/state/blob/backgrounds"
"$HOME/.local/state/blob/current"
"$HOME/.local/share/fonts"
"$HOME/.local/share/applications"
"$HOME/.local/share/icons/hicolor/256x256/apps"
"$HOME/wallpapers"
)
create_directories() {
local missing=()
local dir
for dir in "${directories[@]}"; do
[[ -d $dir ]] || missing+=("$dir")
done
if (( ${#missing[@]} == 0 )); then
say "ok directories"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would create ${#missing[@]} directory(ies):"
printf ' %s\n' "${missing[@]}"
return 0
fi
mkdir -p "${missing[@]}"
say "mkdir ${#missing[@]} directory(ies)"
}
read_package_list() {
[[ -f $1 ]] || return 0
grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$1"
}
# 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
# for a package still shipped by another repository avoids a file conflict.
missing_packages() {
local entry wanted alternative
for entry in $(read_package_list "$1"); do
wanted=${entry%%|*}
alternative=${entry#*|}
pacman -Q "$wanted" &>/dev/null && continue
[[ $alternative != "$entry" ]] && pacman -Q "$alternative" &>/dev/null && continue
printf '%s\n' "$wanted"
done
}
install_packages() {
if [[ $skip_packages == true ]]; then
say "skip packages (--skip-packages)"
return 0
fi
local -a repo_missing aur_missing
mapfile -t repo_missing < <(missing_packages "$repo_dir/packages/blob.packages")
mapfile -t aur_missing < <(missing_packages "$repo_dir/packages/blob-aur.packages")
if (( ${#repo_missing[@]} == 0 && ${#aur_missing[@]} == 0 )); then
say "ok packages"
return 0
fi
note_change
if [[ $check == true ]]; then
(( ${#repo_missing[@]} )) && say "would install ${#repo_missing[@]} repo package(s): ${repo_missing[*]}"
(( ${#aur_missing[@]} )) && say "would install ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
return 0
fi
blob_sudo_keepalive
if (( ${#repo_missing[@]} )); then
say "install ${#repo_missing[@]} repo package(s)"
sudo pacman -S --needed --noconfirm "${repo_missing[@]}"
fi
if (( ${#aur_missing[@]} )); then
ensure_aur_helper || {
say "WARN no AUR helper; skipping ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
return 0
}
say "install ${#aur_missing[@]} AUR package(s)"
yay -S --needed --noconfirm "${aur_missing[@]}"
fi
}
# 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
# this point can then use yay normally.
ensure_aur_helper() {
command -v yay &>/dev/null && return 0
say "bootstrap yay from the AUR"
sudo pacman -S --needed --noconfirm base-devel git
local build_dir
build_dir="$(mktemp -d)"
if ! git clone --depth 1 https://aur.archlinux.org/yay-bin.git "$build_dir/yay-bin"; then
rm -rf "$build_dir"
return 1
fi
( cd "$build_dir/yay-bin" && makepkg -si --noconfirm ) || {
rm -rf "$build_dir"
return 1
}
rm -rf "$build_dir"
command -v yay &>/dev/null
}
blob_sudo_keepalive() {
sudo -v 2>/dev/null || true
}
# 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
# resolves as $BLOB_PATH/... therefore needs no install step at all.
link_blob_path() {
local current=""
[[ -L $blob_path ]] && current="$(readlink -f "$blob_path")"
if [[ $current == "$repo_dir" ]]; then
say "ok BLOB_PATH -> $repo_dir"
return 0
fi
if [[ -e $blob_path && ! -L $blob_path ]]; then
say "WARN $blob_path exists and is not a symlink; move it aside first"
note_change
return 0
fi
note_change
if [[ $check == true ]]; then
say "would link BLOB_PATH -> $repo_dir"
return 0
fi
ln -sfn "$repo_dir" "$blob_path"
say "link BLOB_PATH -> $repo_dir"
}
# Copy a file only when the destination is missing or matches what we last
# wrote. A destination that differs is a local edit, reported and kept unless
# --force says otherwise.
install_file() {
local source="$1" dest="$2" label="$3"
if [[ ! -e $source ]]; then
say "SKIP $label (missing in repo)"
return 0
fi
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
say "ok $label"
return 0
fi
note_change
if [[ -e $dest ]] && [[ $force == false ]]; then
say "DIFF $label (local changes kept; --force to overwrite)"
return 0
fi
if [[ $check == true ]]; then
say "would write $label"
return 0
fi
mkdir -p "$(dirname "$dest")"
[[ -e $dest ]] && cp "$dest" "$dest.bak"
cp "$source" "$dest"
say "write $label"
}
install_tree() {
local source="$1" dest="$2" label="$3"
if [[ ! -d $source ]]; then
say "SKIP $label (missing in repo)"
return 0
fi
local relative
while IFS= read -r relative; do
install_file "$source/$relative" "$dest/$relative" "$label/$relative"
done < <(cd "$source" && find . -type f -printf '%P\n' | sort)
}
install_session_entry() {
local source="$repo_dir/session/blob.desktop"
local dest="$session_dir/blob.desktop"
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
say "ok wayland session entry"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would install wayland session entry (needs sudo)"
return 0
fi
sudo install -Dm644 "$source" "$dest"
say "write wayland session entry"
}
say "Blob installer" say "Blob installer"
say "repo: $repo_dir" say "repo: $repo_dir"
@@ -281,39 +61,19 @@ link_blob_path
create_directories create_directories
install_packages install_packages
install_tree "$repo_dir/hypr" "$config_dir/hypr" "hypr" install_shipped_config
# config/ is the shipped-defaults directory. Everything in it is reachable as install_user_units
# $BLOB_PATH/config/... so `blob-refresh-config` can reset a file, and most of it install_systemd_dropins
# is also the right thing to deploy on a fresh install. Two subdirectories are install_zen_config
# reference-only and must not be copied into ~/.config:
#
# blob/ the shell defaults; copying them over ~/.config/blob/shell.json would
# replace the user's bar layout with the stock one
# hypr/ the stock Hyprland config; the personal hypr/ below is authoritative
# and is installed to the same place
for entry in "$repo_dir"/config/*; do
entry_name="$(basename "$entry")"
[[ $entry_name == blob || $entry_name == hypr ]] && continue
if [[ -d $entry ]]; then
install_tree "$entry" "$config_dir/$entry_name" "config/$entry_name"
else
install_file "$entry" "$config_dir/$entry_name" "config/$entry_name"
fi
done
install_tree "$repo_dir/hooks" "$config_dir/blob/hooks" "hooks"
install_tree "$repo_dir/branding" "$config_dir/blob/branding" "branding"
install_file "$repo_dir/shell.json" "$config_dir/blob/shell.json" "shell.json"
install_file "$repo_dir/session/uwsm/default" "$config_dir/uwsm/default" "uwsm/default"
install_file "$repo_dir/session/uwsm/env.d/10-blob" "$config_dir/uwsm/env.d/10-blob" "uwsm/env.d/10-blob"
install_file "$repo_dir/fonts/omarchy.ttf" "$font_dir/omarchy.ttf" "fonts/omarchy.ttf"
install_session_entry
zen_profile="$(find "$config_dir/zen" -maxdepth 1 -type d -name '*.Default (release)*' 2>/dev/null | head -1)" enable_system_services
if [[ -n $zen_profile ]]; then mask_system_services
install_file "$repo_dir/zen/userChrome.css" "$zen_profile/chrome/userChrome.css" "zen/userChrome.css" enable_user_units
else
say "SKIP zen/userChrome.css (no Zen profile found)" install_login_config
fi install_session_entry
install_autologin
enable_display_manager
say "" say ""
if [[ $check == true ]]; then if [[ $check == true ]]; then
@@ -321,6 +81,7 @@ 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."
say "" say ""
say "Next: log out and pick the Blob session, then run 'blob theme set <name>'." launch_desktop
fi fi
+7
View File
@@ -0,0 +1,7 @@
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
+105
View File
@@ -0,0 +1,105 @@
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/as-root.sh"
say() {
printf '%s\n' "$1"
}
note_change() {
changes=$((changes + 1))
}
system_unit_exists() {
systemctl cat -- "$1" &>/dev/null
}
user_manager_available() {
systemctl --user show-environment &>/dev/null
}
system_unit_state() {
systemctl is-enabled -- "$1" 2>/dev/null || true
}
user_unit_state() {
systemctl --user is-enabled -- "$1" 2>/dev/null || true
}
# Copy a file only when the destination is missing or matches what we last
# wrote. A destination that differs is a local edit, reported and kept unless
# --force says otherwise.
install_file() {
local source="$1" dest="$2" label="$3"
if [[ ! -e $source ]]; then
say "SKIP $label (missing in repo)"
return 0
fi
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
say "ok $label"
return 0
fi
note_change
if [[ -e $dest ]] && [[ $force == false ]]; then
say "DIFF $label (local changes kept; --force to overwrite)"
return 0
fi
if [[ $check == true ]]; then
say "would write $label"
return 0
fi
mkdir -p "$(dirname "$dest")"
[[ -e $dest ]] && cp "$dest" "$dest.bak"
cp "$source" "$dest"
say "write $label"
}
install_tree() {
local source="$1" dest="$2" label="$3"
if [[ ! -d $source ]]; then
say "SKIP $label (missing in repo)"
return 0
fi
local relative
while IFS= read -r relative; do
install_file "$source/$relative" "$dest/$relative" "$label/$relative"
done < <(cd "$source" && find . -type f -printf '%P\n' | sort)
}
# Same reporting as install_file for a destination outside $HOME. The mode is
# explicit because these land in /etc and /usr/local, and no .bak is kept: the
# only writer of these paths is the installer.
install_root_file() {
local source="$1" dest="$2" label="$3" mode="${4:-0644}"
if [[ ! -e $source ]]; then
say "SKIP $label (missing in repo)"
return 0
fi
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
say "ok $label"
return 0
fi
note_change
if [[ -e $dest ]] && [[ $force == false ]]; then
say "DIFF $label (local changes kept; --force to overwrite)"
return 0
fi
if [[ $check == true ]]; then
say "would write $label (needs sudo)"
return 0
fi
as_root install -Dm "$mode" "$source" "$dest"
say "write $label"
}
+71
View File
@@ -0,0 +1,71 @@
# 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
# resolves as $BLOB_PATH/... therefore needs no install step at all.
link_blob_path() {
local current=""
[[ -L $blob_path ]] && current="$(readlink -f "$blob_path")"
if [[ $current == "$repo_dir" ]]; then
say "ok BLOB_PATH -> $repo_dir"
return 0
fi
if [[ -e $blob_path && ! -L $blob_path ]]; then
say "WARN $blob_path exists and is not a symlink; move it aside first"
note_change
return 0
fi
note_change
if [[ $check == true ]]; then
say "would link BLOB_PATH -> $repo_dir"
return 0
fi
ln -sfn "$repo_dir" "$blob_path"
say "link BLOB_PATH -> $repo_dir"
}
# config/ is the shipped-defaults directory. Everything in it is reachable as
# $BLOB_PATH/config/... so `blob-refresh-config` can reset a file, and most of it
# is also the right thing to deploy on a fresh install. Two subdirectories are
# reference-only and must not be copied into ~/.config:
#
# blob/ the shell defaults; copying them over ~/.config/blob/shell.json would
# replace the user's bar layout with the stock one
# hypr/ the stock Hyprland config; the personal hypr/ below is authoritative
# and is installed to the same place
install_shipped_config() {
local entry entry_name
install_tree "$repo_dir/hypr" "$config_dir/hypr" "hypr"
for entry in "$repo_dir"/config/*; do
entry_name="$(basename "$entry")"
[[ $entry_name == blob || $entry_name == hypr ]] && continue
if [[ -d $entry ]]; then
install_tree "$entry" "$config_dir/$entry_name" "config/$entry_name"
else
install_file "$entry" "$config_dir/$entry_name" "config/$entry_name"
fi
done
install_tree "$repo_dir/hooks" "$config_dir/blob/hooks" "hooks"
install_tree "$repo_dir/branding" "$config_dir/blob/branding" "branding"
install_file "$repo_dir/shell.json" "$config_dir/blob/shell.json" "shell.json"
install_file "$repo_dir/session/uwsm/default" "$config_dir/uwsm/default" "uwsm/default"
install_file "$repo_dir/session/uwsm/env.d/10-blob" "$config_dir/uwsm/env.d/10-blob" "uwsm/env.d/10-blob"
install_file "$repo_dir/fonts/omarchy.ttf" "$font_dir/omarchy.ttf" "fonts/omarchy.ttf"
}
install_zen_config() {
local profile
profile="$(find "$config_dir/zen" -maxdepth 1 -type d -name '*.Default (release)*' 2>/dev/null | head -1)"
if [[ -z $profile ]]; then
say "SKIP zen/userChrome.css (no Zen profile found)"
return 0
fi
install_file "$repo_dir/zen/userChrome.css" "$profile/chrome/userChrome.css" "zen/userChrome.css"
}
+44
View File
@@ -0,0 +1,44 @@
directories=(
"$HOME/.config/blob/hooks"
"$HOME/.config/blob/extensions"
"$HOME/.config/blob/plugins"
"$HOME/.config/blob/themed"
"$HOME/.config/blob/themes"
"$HOME/.config/blob/branding"
"$HOME/.config/hypr"
"$HOME/.config/uwsm/env.d"
"$HOME/.config/systemd/user/app.slice.d"
"$HOME/.local/state/blob/toggles/hypr"
"$HOME/.local/state/blob/indicators"
"$HOME/.local/state/blob/notifications/history"
"$HOME/.local/state/blob/notifications/images"
"$HOME/.local/state/blob/backgrounds"
"$HOME/.local/state/blob/current"
"$HOME/.local/share/fonts"
"$HOME/.local/share/applications"
"$HOME/.local/share/icons/hicolor/256x256/apps"
"$HOME/wallpapers"
)
create_directories() {
local missing=()
local dir
for dir in "${directories[@]}"; do
[[ -d $dir ]] || missing+=("$dir")
done
if (( ${#missing[@]} == 0 )); then
say "ok directories"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would create ${#missing[@]} directory(ies):"
printf ' %s\n' "${missing[@]}"
return 0
fi
mkdir -p "${missing[@]}"
say "mkdir ${#missing[@]} directory(ies)"
}
+102
View File
@@ -0,0 +1,102 @@
session_entry_dir=/usr/local/share/wayland-sessions
greeter_compositor_config=/etc/sddm/hyprland-greeter.lua
# SDDM's stock Wayland greeter runs on weston, which Blob does not install.
# Point it at Hyprland with a greeter-only config instead.
install_login_config() {
install_root_file "$repo_dir/default/sddm/hyprland-greeter.lua" \
"$greeter_compositor_config" "sddm/hyprland-greeter.lua"
install_root_file "$repo_dir/default/sddm/10-blob.conf" \
/etc/sddm.conf.d/10-blob.conf "sddm.conf.d/10-blob.conf"
}
install_session_entry() {
install_root_file "$repo_dir/session/blob.desktop" \
"$session_entry_dir/blob.desktop" "wayland session entry"
}
install_autologin() {
if [[ $autologin == false ]]; then
return 0
fi
local generated
generated="$(mktemp)"
printf '[Autologin]\nUser=%s\nSession=blob.desktop\n' "$USER" >"$generated"
install_root_file "$generated" \
/etc/sddm.conf.d/99-blob-autologin.conf "sddm.conf.d/99-blob-autologin.conf"
rm -f "$generated"
}
# Enabling sddm is not enough on a machine installed without a display manager:
# its default target is multi-user.target, which never pulls in graphical.target.
enable_display_manager() {
if ! system_unit_exists sddm.service; then
say "WARN sddm.service not found; the desktop cannot start at boot"
return 0
fi
local enable_needed=false
case "$(system_unit_state sddm.service)" in
enabled | enabled-runtime) ;;
*) enable_needed=true ;;
esac
local default_target
default_target="$(systemctl get-default 2>/dev/null || true)"
local target_needed=false
[[ $default_target == graphical.target ]] || target_needed=true
if [[ $enable_needed == false && $target_needed == false ]]; then
say "ok display manager"
return 0
fi
note_change
if [[ $check == true ]]; then
[[ $enable_needed == true ]] && say "would enable sddm.service"
[[ $target_needed == true ]] && say "would set the default target to graphical.target"
return 0
fi
if [[ $enable_needed == true ]]; then
as_root systemctl enable sddm.service
say "enable sddm.service"
fi
if [[ $target_needed == true ]]; then
as_root systemctl set-default graphical.target
say "set default target to graphical.target"
fi
}
# The installer is usually run from a bare console on a fresh machine, where
# enabling the display manager alone leaves the user looking at a shell prompt.
launch_desktop() {
if [[ $check == true ]]; then
return 0
fi
if [[ $no_launch == true ]]; then
say "Run 'sudo systemctl start sddm' or reboot to reach the desktop."
return 0
fi
if ! system_unit_exists sddm.service; then
return 0
fi
if [[ -n ${WAYLAND_DISPLAY:-} || -n ${DISPLAY:-} ]]; then
say "Log out and pick the Blob session at the login screen."
return 0
fi
if systemctl is-active --quiet sddm.service; then
say "The login screen is already running."
return 0
fi
say "Starting the login screen. Pick the Blob session to log in."
as_root systemctl start sddm.service
}
+86
View File
@@ -0,0 +1,86 @@
read_package_list() {
[[ -f $1 ]] || return 0
grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$1"
}
# 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
# for a package still shipped by another repository avoids a file conflict.
missing_packages() {
local entry wanted alternative
for entry in $(read_package_list "$1"); do
wanted=${entry%%|*}
alternative=${entry#*|}
pacman -Q "$wanted" &>/dev/null && continue
[[ $alternative != "$entry" ]] && pacman -Q "$alternative" &>/dev/null && continue
printf '%s\n' "$wanted"
done
}
install_packages() {
if [[ $skip_packages == true ]]; then
say "skip packages (--skip-packages)"
return 0
fi
local -a repo_missing aur_missing
mapfile -t repo_missing < <(missing_packages "$repo_dir/packages/blob.packages")
mapfile -t aur_missing < <(missing_packages "$repo_dir/packages/blob-aur.packages")
if (( ${#repo_missing[@]} == 0 && ${#aur_missing[@]} == 0 )); then
say "ok packages"
return 0
fi
note_change
if [[ $check == true ]]; then
(( ${#repo_missing[@]} )) && say "would install ${#repo_missing[@]} repo package(s): ${repo_missing[*]}"
(( ${#aur_missing[@]} )) && say "would install ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
return 0
fi
blob_sudo_keepalive
if (( ${#repo_missing[@]} )); then
say "install ${#repo_missing[@]} repo package(s)"
as_root pacman -S --needed --noconfirm "${repo_missing[@]}"
fi
if (( ${#aur_missing[@]} )); then
ensure_aur_helper || {
say "WARN no AUR helper; skipping ${#aur_missing[@]} AUR package(s): ${aur_missing[*]}"
return 0
}
say "install ${#aur_missing[@]} AUR package(s)"
yay -S --needed --noconfirm "${aur_missing[@]}"
fi
}
# 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
# this point can then use yay normally.
ensure_aur_helper() {
command -v yay &>/dev/null && return 0
say "bootstrap yay from the AUR"
as_root pacman -S --needed --noconfirm base-devel git
local build_dir
build_dir="$(mktemp -d)"
if ! git clone --depth 1 https://aur.archlinux.org/yay-bin.git "$build_dir/yay-bin"; then
rm -rf "$build_dir"
return 1
fi
( cd "$build_dir/yay-bin" && makepkg -si --noconfirm ) || {
rm -rf "$build_dir"
return 1
}
rm -rf "$build_dir"
command -v yay &>/dev/null
}
blob_sudo_keepalive() {
sudo -v 2>/dev/null || true
}
+144
View File
@@ -0,0 +1,144 @@
system_services=(
NetworkManager.service
systemd-resolved.service
bluetooth.service
cups.service
avahi-daemon.service
docker.socket
power-profiles-daemon.service
systemd-oomd.service
)
# network-online.target must not hold up graphical.target waiting for DHCP or
# Wi-Fi association. Nothing in the session blocks on the network.
masked_services=(
NetworkManager-wait-online.service
)
# blob-speaker-tuning.service is deliberately absent: blob-audio-tuning installs
# and enables it itself, only on machines with a tuning profile.
user_services=(
bt-agent.service
blob-sleep-lock.service
blob-fcitx5.service
blob-crash-watch.service
)
enable_system_services() {
local pending=()
local unit
for unit in "${system_services[@]}"; do
system_unit_exists "$unit" || continue
case "$(system_unit_state "$unit")" in
enabled | enabled-runtime | static | alias | indirect | masked) continue ;;
esac
pending+=("$unit")
done
if (( ${#pending[@]} == 0 )); then
say "ok system services"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would enable ${#pending[@]} system service(s): ${pending[*]}"
return 0
fi
as_root systemctl enable "${pending[@]}"
say "enable ${#pending[@]} system service(s)"
}
mask_system_services() {
local pending=()
local unit
for unit in "${masked_services[@]}"; do
system_unit_exists "$unit" || continue
[[ "$(system_unit_state "$unit")" == masked ]] && continue
pending+=("$unit")
done
if (( ${#pending[@]} == 0 )); then
say "ok masked services"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would mask ${#pending[@]} service(s): ${pending[*]}"
return 0
fi
as_root systemctl mask "${pending[@]}"
say "mask ${#pending[@]} service(s)"
}
install_systemd_dropins() {
local systemd_dir="$repo_dir/default/systemd"
install_root_file "$systemd_dir/faster-shutdown.conf" \
/etc/systemd/system.conf.d/10-blob.conf "systemd/system.conf.d/10-blob.conf"
install_root_file "$systemd_dir/user@.service.d/faster-shutdown.conf" \
/etc/systemd/system/user@.service.d/10-blob.conf "systemd/user@.service.d/10-blob.conf"
install_root_file "$systemd_dir/oomd.conf.d/10-blob.conf" \
/etc/systemd/oomd.conf.d/10-blob.conf "systemd/oomd.conf.d/10-blob.conf"
install_root_file "$systemd_dir/system-sleep/keyboard-backlight" \
/etc/systemd/system-sleep/keyboard-backlight "systemd/system-sleep/keyboard-backlight" 0755
install_root_file "$systemd_dir/system-sleep/unmount-fuse" \
/etc/systemd/system-sleep/unmount-fuse "systemd/system-sleep/unmount-fuse" 0755
if system_unit_exists plocate-updatedb.service; then
install_root_file "$systemd_dir/system/plocate-updatedb.service.d/10-blob.conf" \
/etc/systemd/system/plocate-updatedb.service.d/10-blob.conf \
"systemd/plocate-updatedb.service.d/10-blob.conf"
else
say "SKIP systemd/plocate-updatedb.service.d/10-blob.conf (plocate not installed)"
fi
}
install_user_units() {
local unit
for unit in "${user_services[@]}"; do
install_file "$repo_dir/default/systemd/user/$unit" \
"$config_dir/systemd/user/$unit" "systemd/user/$unit"
done
install_file "$repo_dir/default/systemd/user/app.slice.d/10-oomd.conf" \
"$config_dir/systemd/user/app.slice.d/10-oomd.conf" "systemd/user/app.slice.d/10-oomd.conf"
}
# Enable only. The units are all WantedBy=graphical-session.target, so they come
# up with the session rather than from a TTY where their conditions cannot pass.
enable_user_units() {
local pending=()
local unit
if ! user_manager_available; then
say "WARN no user systemd manager; skipping user services"
return 0
fi
for unit in "${user_services[@]}"; do
[[ "$(user_unit_state "$unit")" == enabled ]] && continue
pending+=("$unit")
done
if (( ${#pending[@]} == 0 )); then
say "ok user services"
return 0
fi
note_change
if [[ $check == true ]]; then
say "would enable ${#pending[@]} user service(s): ${pending[*]}"
return 0
fi
systemctl --user daemon-reload
systemctl --user enable "${pending[@]}"
say "enable ${#pending[@]} user service(s)"
}
+5
View File
@@ -41,6 +41,11 @@ bluez
bluez-utils bluez-utils
bluez-tools bluez-tools
# Input method, for the XCompose sequences blob-fcitx5.service serves
fcitx5
fcitx5-gtk
fcitx5-qt
# Audio # Audio
pipewire pipewire
pipewire-alsa pipewire-alsa
+46 -2
View File
@@ -4,9 +4,29 @@ set -e
blob_path="$HOME/.local/share/blob" blob_path="$HOME/.local/share/blob"
config_dir="$HOME/.config" config_dir="$HOME/.config"
session_entry="/usr/share/wayland-sessions/blob.desktop" session_entry="/usr/local/share/wayland-sessions/blob.desktop"
root_files=(
/etc/sddm.conf.d/10-blob.conf
/etc/sddm.conf.d/99-blob-autologin.conf
/etc/sddm/hyprland-greeter.lua
/etc/systemd/system.conf.d/10-blob.conf
/etc/systemd/system/user@.service.d/10-blob.conf
/etc/systemd/oomd.conf.d/10-blob.conf
/etc/systemd/system/plocate-updatedb.service.d/10-blob.conf
/etc/systemd/system-sleep/keyboard-backlight
/etc/systemd/system-sleep/unmount-fuse
)
user_units=(
bt-agent.service
blob-sleep-lock.service
blob-fcitx5.service
blob-crash-watch.service
)
keep_state=false keep_state=false
keep_session=false
assume_yes=false assume_yes=false
usage() { usage() {
@@ -16,6 +36,7 @@ Usage: ./uninstall.sh [OPTIONS]
Removes what install.sh wrote. The checkout itself is never touched. Removes what install.sh wrote. The checkout itself is never touched.
--keep-state Leave ~/.local/state/blob in place --keep-state Leave ~/.local/state/blob in place
--keep-session Leave the Blob session entry and the login screen config
--yes Do not prompt --yes Do not prompt
--help Show this message --help Show this message
USAGE USAGE
@@ -25,6 +46,7 @@ USAGE
while (( $# > 0 )); do while (( $# > 0 )); do
case "$1" in case "$1" in
--keep-state) keep_state=true; shift ;; --keep-state) keep_state=true; shift ;;
--keep-session) keep_session=true; shift ;;
--yes) assume_yes=true; shift ;; --yes) assume_yes=true; shift ;;
--help) usage ;; --help) usage ;;
*) echo "Unknown option: $1" >&2; usage ;; *) echo "Unknown option: $1" >&2; usage ;;
@@ -41,7 +63,7 @@ restore_or_remove() {
if [[ -e $path.bak ]]; then if [[ -e $path.bak ]]; then
mv "$path.bak" "$path" mv "$path.bak" "$path"
say "restore $label (from .bak)" say "restore $label (from .bak)"
return return 0
fi fi
if [[ -e $path ]]; then if [[ -e $path ]]; then
@@ -85,6 +107,28 @@ if [[ $keep_state == false && -d $HOME/.local/state/blob ]]; then
say "remove ~/.local/state/blob" say "remove ~/.local/state/blob"
fi fi
for unit in "${user_units[@]}"; do
if [[ "$(systemctl --user is-enabled -- "$unit" 2>/dev/null || true)" == enabled ]]; then
systemctl --user disable --now "$unit" >/dev/null 2>&1 || true
say "disable $unit"
fi
rm -f "$config_dir/systemd/user/$unit"
done
rm -f "$config_dir/systemd/user/app.slice.d/10-oomd.conf"
if [[ $keep_session == true ]]; then
say ""
say "Done. The Blob session entry and the login screen config were kept."
exit 0
fi
for path in "${root_files[@]}"; do
if [[ -e $path ]]; then
sudo rm -f "$path"
say "remove $path"
fi
done
if [[ -e $session_entry ]]; then if [[ -e $session_entry ]]; then
sudo rm -f "$session_entry" sudo rm -f "$session_entry"
say "remove wayland session entry" say "remove wayland session entry"