Compare commits
31
Commits
1cd50be73a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
747b7ebdb7 | ||
|
|
5858eec50d | ||
|
|
8d8dee2342 | ||
|
|
5372c6ea29 | ||
|
|
740586611a | ||
|
|
3211350435 | ||
|
|
933f3a9efd | ||
|
|
06446f36f8 | ||
|
|
49d2f0e41e | ||
|
|
c0c4eadfe1 | ||
|
|
90c22b4e0c | ||
|
|
d316308bf5 | ||
|
|
64afc07250 | ||
|
|
e74c9c5bc1 | ||
|
|
a9b9ba6ba4 | ||
|
|
deeacbe580 | ||
|
|
bfba908ef4 | ||
|
|
ef02250a8f | ||
|
|
e5c20e0a46 | ||
|
|
737d601bca | ||
|
|
00803e59a4 | ||
|
|
3fbdd754c1 | ||
|
|
06204a357a | ||
|
|
2f29cb8bae | ||
|
|
52e747fe3e | ||
|
|
f00d09eb02 | ||
|
|
421c58fab0 | ||
|
|
8f124f47c2 | ||
|
|
f4dd3b7532 | ||
|
|
9e34929e48 | ||
|
|
a32254c398 |
@@ -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,38 @@ 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. Directories | creates the 19 directories the shell and scripts expect, before anything writes into them |
|
||||||
|
| 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 |
|
||||||
|
| 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. 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. User units | installs the session services into `~/.config/systemd/user` and enables them |
|
||||||
|
| 7. systemd drop-ins | writes the shutdown timeouts, oomd thresholds, plocate prune paths, and sleep hooks under `/etc` |
|
||||||
|
| 8. System services | enables NetworkManager, resolved, bluetooth, cups, avahi, docker, power-profiles-daemon, and oomd, and masks `NetworkManager-wait-online` |
|
||||||
|
| 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 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
|
||||||
|
at Hyprland with a Lua config, the only format Hyprland still reads. It also
|
||||||
|
reports any leftover drop-in that autologs into a session that is not installed,
|
||||||
|
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
|
||||||
|
services, and the symlinks, restoring any `.bak` the installer made. The
|
||||||
|
checkout, the wallpapers, and the themes stay where they are.
|
||||||
|
|
||||||
## Regenerating docs
|
## Regenerating docs
|
||||||
|
|
||||||
|
|||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# blob:summary=Assign a wallpaper to one monitor, leaving the others alone
|
||||||
|
# blob:args=<--list|--clear-all|<output> <image>|<output> --clear>
|
||||||
|
# blob:examples=blob bg monitor DP-1 ~/wallpapers/astronaut2.jpg
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
state_dir="$HOME/.local/state/blob/backgrounds"
|
||||||
|
mkdir -p "$state_dir"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
Usage: blob-bg-monitor <output> <image> assign a wallpaper to one monitor
|
||||||
|
blob-bg-monitor <output> --clear fall back to the global wallpaper
|
||||||
|
blob-bg-monitor --list show current assignments
|
||||||
|
blob-bg-monitor --clear-all drop every assignment
|
||||||
|
|
||||||
|
Monitors with no assignment show the global wallpaper set by blob-bg-set.
|
||||||
|
Output names are what 'hyprctl monitors' reports, such as DP-1 or eDP-1.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_shell() {
|
||||||
|
blob-shell -q background refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
known_output() {
|
||||||
|
hyprctl monitors all -j 2>/dev/null | jq -e --arg name "$1" 'any(.[]; .name == $name)' >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1-}" in
|
||||||
|
--list)
|
||||||
|
found=false
|
||||||
|
for link in "$state_dir"/*; do
|
||||||
|
[[ -e $link ]] || continue
|
||||||
|
found=true
|
||||||
|
printf '%-12s %s\n' "$(basename "$link")" "$(readlink -f "$link")"
|
||||||
|
done
|
||||||
|
[[ $found == true ]] || echo "No per-monitor wallpapers assigned."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--clear-all)
|
||||||
|
rm -f "$state_dir"/*
|
||||||
|
notify_shell
|
||||||
|
echo "Cleared every per-monitor wallpaper."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
""|--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
output="$1"
|
||||||
|
image="${2-}"
|
||||||
|
|
||||||
|
if [[ -z $image ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! known_output "$output"; then
|
||||||
|
echo "Warning: '$output' is not a connected monitor right now." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $image == --clear ]]; then
|
||||||
|
rm -f "$state_dir/$output"
|
||||||
|
notify_shell
|
||||||
|
echo "Cleared $output; it follows the global wallpaper again."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f $image ]]; then
|
||||||
|
echo "Not a file: $image" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -sfn "$(realpath "$image")" "$state_dir/$output"
|
||||||
|
notify_shell
|
||||||
|
echo "Set $output to $(basename "$image")"
|
||||||
+126
-8
@@ -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."
|
||||||
|
|
||||||
|
if limine_present; then
|
||||||
reclaim_esp_space "$machine_id" "$backup"
|
reclaim_esp_space "$machine_id" "$backup"
|
||||||
restore_standard_initramfs
|
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
|
||||||
|
|||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# blob:summary=Apply a monitor layout at runtime without touching monitors.lua
|
||||||
|
# blob:args=<list|apply <keyword>...|reset>
|
||||||
|
# blob:examples=blob display arrange apply 'DP-1,1920x1080@60,0x0,1'
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
action="${1-}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
Usage: blob-display-arrange <command>
|
||||||
|
|
||||||
|
list print the current layout as monitor keywords
|
||||||
|
apply <keyword>... apply one or more monitor keywords now
|
||||||
|
reset reload the Hyprland config, restoring monitors.lua
|
||||||
|
|
||||||
|
A keyword is Hyprland's own monitor form:
|
||||||
|
|
||||||
|
<name>,<mode>,<x>x<y>,<scale>[,transform,<n>][,mirror,<name>]
|
||||||
|
<name>,disable
|
||||||
|
|
||||||
|
Changes made with apply are runtime only. They last until the config is
|
||||||
|
reloaded or the session ends; monitors.lua is never written.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
list)
|
||||||
|
hyprctl monitors all -j | jq -r '
|
||||||
|
.[]
|
||||||
|
| if .disabled then "\(.name),disable"
|
||||||
|
else
|
||||||
|
"\(.name),\(.width)x\(.height)@\((.refreshRate * 100 | round) / 100),\(.x)x\(.y),\(.scale)"
|
||||||
|
+ (if .transform != 0 then ",transform,\(.transform)" else "" end)
|
||||||
|
+ (if .mirrorOf != "none" then ",mirror,\(.mirrorOf)" else "" end)
|
||||||
|
end
|
||||||
|
'
|
||||||
|
;;
|
||||||
|
apply)
|
||||||
|
if (( $# == 0 )); then
|
||||||
|
echo "apply needs at least one monitor keyword" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for keyword in "$@"; do
|
||||||
|
if [[ $keyword != *,* ]]; then
|
||||||
|
echo "Not a monitor keyword: $keyword" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
hyprctl keyword monitor "$keyword" >/dev/null
|
||||||
|
echo "applied $keyword"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
reset)
|
||||||
|
hyprctl reload >/dev/null
|
||||||
|
echo "reloaded; monitors.lua is authoritative again"
|
||||||
|
;;
|
||||||
|
""|--help|-h)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $action" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+28
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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*) ;;
|
||||||
|
|||||||
Executable
+6
@@ -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
@@ -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}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# blob:summary=Verify every package list entry resolves, ignoring extra repositories
|
||||||
|
# blob:hidden=true
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
bin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_dir="$(dirname "$bin_dir")"
|
||||||
|
official_list="$repo_dir/packages/blob.packages"
|
||||||
|
aur_list="$repo_dir/packages/blob-aur.packages"
|
||||||
|
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
entries() {
|
||||||
|
grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$1" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# pacman -Si and yay -Si both search every configured repository, so a package
|
||||||
|
# that exists only in a third-party repo passes them and then fails on a machine
|
||||||
|
# that does not have that repo. These checks name the repositories explicitly.
|
||||||
|
official_names="$(pacman -Sl core extra multilib 2>/dev/null | awk '{print $2}' | sort -u)"
|
||||||
|
|
||||||
|
echo "Checking $official_list against core, extra, multilib"
|
||||||
|
for entry in $(entries "$official_list"); do
|
||||||
|
name=${entry%%|*}
|
||||||
|
if ! printf '%s\n' "$official_names" | grep -qx "$name"; then
|
||||||
|
echo " NOT IN OFFICIAL REPOS: $name"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Checking $aur_list against the AUR"
|
||||||
|
for entry in $(entries "$aur_list"); do
|
||||||
|
name=${entry%%|*}
|
||||||
|
if printf '%s\n' "$official_names" | grep -qx "$name"; then
|
||||||
|
echo " IN OFFICIAL REPOS, MOVE IT: $name"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! yay -Ssa "$name" 2>/dev/null | grep -qE "^aur/$name "; then
|
||||||
|
echo " NOT IN AUR: $name"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if (( failures == 0 )); then
|
||||||
|
echo "All entries resolve."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$failures problem(s)."
|
||||||
|
exit 1
|
||||||
+28
-5
@@ -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
|
||||||
|
|||||||
Executable
+13
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Executable
+67
@@ -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[@]}"
|
||||||
Executable
+22
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+20
-21
@@ -1,26 +1,25 @@
|
|||||||
@@@@@@@@@@@@
|
@@ @@ @@
|
||||||
|
@@ @@ @@
|
||||||
|
@@ @@ @@
|
||||||
|
@@ @@ @@
|
||||||
|
@@ @@ @@
|
||||||
|
|
||||||
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
@@ @@
|
||||||
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||||
@@ @@@@@@
|
@@ @@@@@@
|
||||||
@@ @@ @@
|
@@ @@ @@
|
||||||
@@ @@ @@
|
@@ @@ @@
|
||||||
@@@ @@
|
|
||||||
@@ @@ @@ @@
|
|
||||||
@ @@ @@ @@ @@ @@
|
|
||||||
@ @@ ##### @ @ @ @@
|
|
||||||
@ @ ##### @ @ ##### @ @@
|
|
||||||
@@ @ ### @ @ ###### @ @@
|
|
||||||
@ @ @ @ ######@@ @@
|
|
||||||
@@ @@@ @@@ @@ @@@ @
|
|
||||||
@@ @@
|
|
||||||
@@ @@
|
|
||||||
@@ @
|
|
||||||
@ @@
|
|
||||||
@@ @@
|
|
||||||
@@ @@
|
|
||||||
@ @@
|
|
||||||
@@ @@
|
|
||||||
@@ @@
|
|
||||||
@@@ @@
|
|
||||||
@@ @@ @@
|
@@ @@ @@
|
||||||
@@ @@ @@
|
@@ @@ @@
|
||||||
@@@@ @@@@
|
@@ @@@@@@@
|
||||||
@@@@@@@@@@@@
|
@@ @@
|
||||||
|
@@ @@
|
||||||
|
@@ @@
|
||||||
|
@@ @@
|
||||||
|
@@@@@@@@@@@@@@@@
|
||||||
|
|
||||||
|
|
||||||
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
@@ @@
|
||||||
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
|||||||
@@ -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 |
@@ -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>
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = ""
|
|
||||||
@@ -111,7 +111,8 @@
|
|||||||
"system": {"icon":"","label":"System","aliases":["power-menu"]},
|
"system": {"icon":"","label":"System","aliases":["power-menu"]},
|
||||||
|
|
||||||
// Blob
|
// Blob
|
||||||
"blob.wallpaper": {"icon":"","label":"Wallpaper","action":"blob-wallpaper-set --menu","description":"Pick a wallpaper from ~/wallpapers"},
|
"blob.wallpaper": {"icon":"","label":"Wallpaper","action":"blob-shell shell toggle blob.displays","description":"Pick a wallpaper, globally or per monitor"},
|
||||||
|
"blob.display-layout": {"icon":"","label":"Display layout","action":"blob-shell shell toggle blob.display-layout","description":"Arrange monitors, set mode, scale, and mirroring"},
|
||||||
"blob.theme": {"icon":"","label":"Theme","action":"blob-theme-menu","description":"Pick a local or bundled color theme"},
|
"blob.theme": {"icon":"","label":"Theme","action":"blob-theme-menu","description":"Pick a local or bundled color theme"},
|
||||||
"blob.dynamic": {"icon":"","label":"Dynamic theme","action":"blob-theme-dynamic","description":"Recolor from the current wallpaper"},
|
"blob.dynamic": {"icon":"","label":"Dynamic theme","action":"blob-theme-dynamic","description":"Recolor from the current wallpaper"},
|
||||||
"blob.glass": {"icon":"","label":"Toggle glass","action":"blob-toggle-glass toggle","checked":"test -f $HOME/.local/state/blob/toggles/hypr/blob-glass.lua"},
|
"blob.glass": {"icon":"","label":"Toggle glass","action":"blob-toggle-glass toggle","checked":"test -f $HOME/.local/state/blob/toggles/hypr/blob-glass.lua"},
|
||||||
@@ -253,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"},
|
||||||
@@ -268,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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#%PAM-1.0
|
||||||
|
auth include system-auth
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[General]
|
||||||
|
DisplayServer=wayland
|
||||||
|
|
||||||
|
[Wayland]
|
||||||
|
CompositorCommand=start-hyprland -- --config /etc/sddm/hyprland-greeter.lua
|
||||||
|
|
||||||
|
[Theme]
|
||||||
|
Current=
|
||||||
|
|
||||||
|
[Autologin]
|
||||||
|
User=
|
||||||
|
Session=
|
||||||
|
|
||||||
|
[Users]
|
||||||
|
RememberLastUser=true
|
||||||
|
RememberLastSession=true
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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 }};
|
||||||
@@ -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 = ""
|
||||||
+12
-1
@@ -43,6 +43,7 @@ of the `blob` listing.
|
|||||||
| `blob-bg-cache` | Cache background switcher thumbnails for the current theme | - |
|
| `blob-bg-cache` | Cache background switcher thumbnails for the current theme | - |
|
||||||
| `blob-bg-current` | Show current background | - |
|
| `blob-bg-current` | Show current background | - |
|
||||||
| `blob-bg-install` | Open the current theme's user background folder | - |
|
| `blob-bg-install` | Open the current theme's user background folder | - |
|
||||||
|
| `blob-bg-monitor` | Assign a wallpaper to one monitor, leaving the others alone | <--list\|--clear-all\|<output> <image>\|<output> --clear> |
|
||||||
| `blob-bg-next` | Cycle to the next background for the current theme | - |
|
| `blob-bg-next` | Cycle to the next background for the current theme | - |
|
||||||
| `blob-bg-set` | Set the current background image | <path-to-image> |
|
| `blob-bg-set` | Set the current background image | <path-to-image> |
|
||||||
| `blob-bg-switcher` | Open the Blob background switcher | - |
|
| `blob-bg-switcher` | Open the Blob background switcher | - |
|
||||||
@@ -138,6 +139,7 @@ of the `blob` listing.
|
|||||||
|
|
||||||
| Command | Does | Arguments |
|
| Command | Does | Arguments |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| `blob-display-arrange` | Apply a monitor layout at runtime without touching monitors.lua | <list\|apply <keyword>...\|reset> |
|
||||||
| `blob-display-size` | Scale text everywhere — blob shell, GTK apps, and terminals | [size\|reset] |
|
| `blob-display-size` | Scale text everywhere — blob shell, GTK apps, and terminals | [size\|reset] |
|
||||||
| `blob-display-state` | Print monitor panel state for the shell | - |
|
| `blob-display-state` | Print monitor panel state for the shell | - |
|
||||||
|
|
||||||
@@ -204,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) | - |
|
||||||
@@ -293,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> |
|
||||||
|
|
||||||
@@ -346,6 +350,12 @@ of the `blob` listing.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `blob-osd` | Show the Blob Quickshell on-screen display | [-i\|--icon <icon>] [-m\|--message <text>] [-p\|--progress <0-100>] [-d\|--duration <ms>] |
|
| `blob-osd` | Show the Blob Quickshell on-screen display | [-i\|--icon <icon>] [-m\|--message <text>] [-p\|--progress <0-100>] [-d\|--duration <ms>] |
|
||||||
|
|
||||||
|
## packages
|
||||||
|
|
||||||
|
| Command | Does | Arguments |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `blob-packages-check` | Verify every package list entry resolves, ignoring extra repositories (hidden) | - |
|
||||||
|
|
||||||
## pkg
|
## pkg
|
||||||
|
|
||||||
| Command | Does | Arguments |
|
| Command | Does | Arguments |
|
||||||
@@ -523,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]) |
|
||||||
@@ -620,4 +631,4 @@ of the `blob` listing.
|
|||||||
|
|
||||||
## Totals
|
## Totals
|
||||||
|
|
||||||
291 commands.
|
294 commands.
|
||||||
|
|||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
# 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, everything under `~/.config`, and the login shell profile |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Directories** are created first, because nothing below can write into a
|
||||||
|
directory that is not there: `~/.config/blob`, `~/.config/systemd/user/app.slice.d`,
|
||||||
|
`~/.local/state/blob`, `~/.local/share` and `~/.cache/blob`. A fresh Arch
|
||||||
|
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
|
||||||
|
`packages/blob-aur.packages`. A machine with no AUR helper gets `yay-bin`
|
||||||
|
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,
|
||||||
|
`blob/shell.json`, the uwsm environment, `fontconfig/fonts.conf`, and the
|
||||||
|
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
|
||||||
|
session. `blob-speaker-tuning.service` is left out, because
|
||||||
|
`blob-audio-tuning` installs it only on machines with a tuning profile.
|
||||||
|
7. **systemd drop-ins** are written under `/etc`: the shutdown timeouts, the
|
||||||
|
oomd pressure thresholds, the plocate prune paths, and the sleep hooks.
|
||||||
|
8. **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, and `plymouth-start.service` and
|
||||||
|
`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 |
|
||||||
|
| --- | --- |
|
||||||
|
| `/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/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
|
||||||
|
runs on `weston`, which Blob does not install, so a default SDDM would fail
|
||||||
|
to draw anything. The installer checks that the `CompositorCommand` named in
|
||||||
|
`zz-blob.conf` is actually installed and warns when it is not, because SDDM
|
||||||
|
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
|
||||||
|
alphabetical order and the last file wins, so a leftover `10-wayland.conf`,
|
||||||
|
`10-theme.conf` or `autologin.conf` from another desktop would otherwise
|
||||||
|
override Blob's. For the same reason `zz-blob.conf` blanks `Theme` and
|
||||||
|
`Autologin`: a greeter pointed at an uninstalled theme, or an autologin
|
||||||
|
pointed at a session entry that no longer exists, leaves SDDM on a black
|
||||||
|
screen it never falls back from, and the only way in is a TTY.
|
||||||
|
|
||||||
|
The Omarchy ISO wrote exactly such an autologin, owned by no package, so it
|
||||||
|
outlives Omarchy. The installer reports it and overrides it; deleting it stops
|
||||||
|
the warning:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm /etc/sddm.conf.d/autologin.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
`--autologin` writes `zzz-blob-autologin.conf`, the only file that sorts after
|
||||||
|
`zz-blob.conf`.
|
||||||
|
10. **Launch.** `sddm`, `hyprland`, `quickshell` and `uwsm` are checked one last
|
||||||
|
time and any that are missing are named, because that is the whole difference
|
||||||
|
between a desktop and a console. Then, 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/zzz-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 |
|
||||||
|
|
||||||
|
## 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.sh` removes the config, the user units, the `/etc` drop-ins, the
|
||||||
|
session entry, the wallpaper link 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.
|
||||||
+2
-1
@@ -11,9 +11,10 @@ Personal overrides in `hypr/bindings.lua` win over the defaults, and
|
|||||||
| Keys | Action |
|
| Keys | Action |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `SUPER + ALT + SPACE` | Blob menu |
|
| `SUPER + ALT + SPACE` | Blob menu |
|
||||||
| `SUPER + ALT + W` | Wallpaper picker |
|
| `SUPER + ALT + W` | Wallpapers |
|
||||||
| `SUPER + CTRL + M` | System monitor |
|
| `SUPER + CTRL + M` | System monitor |
|
||||||
| `SUPER + CTRL + Q` | Quick settings |
|
| `SUPER + CTRL + Q` | Quick settings |
|
||||||
|
| `SUPER + SHIFT + CTRL + D` | Display layout |
|
||||||
| `SUPER + SPACE` | Apps menu |
|
| `SUPER + SPACE` | Apps menu |
|
||||||
|
|
||||||
## Applications
|
## Applications
|
||||||
|
|||||||
+95
-2
@@ -58,12 +58,105 @@ 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`.
|
||||||
|
|
||||||
|
## Installing packages
|
||||||
|
|
||||||
|
`install.sh` installs what is missing before it writes any config:
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/blob.packages 123 packages from core, extra, multilib
|
||||||
|
packages/blob-aur.packages 11 packages with no official equivalent
|
||||||
|
```
|
||||||
|
|
||||||
|
Only missing packages are touched, through `pacman -S --needed` and
|
||||||
|
`yay -S --needed`. An entry is treated as already installed, and skipped, when a
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
blob-packages-check
|
||||||
|
```
|
||||||
|
|
||||||
|
`pacman -Si` and `yay -Si` both search *every* configured repository, so a
|
||||||
|
package that exists only in a third-party repo passes them and then fails on a
|
||||||
|
machine without that repo. `blob-packages-check` names the repositories
|
||||||
|
explicitly instead: official entries must be in core, extra or multilib, and AUR
|
||||||
|
entries must have an exact `aur/` match and must not be in an official repo.
|
||||||
|
|
||||||
|
Run it after editing either list. It caught `localsend` and `tzupdate` sitting in
|
||||||
|
the official list when they only exist in the Omarchy repository, and `awww`,
|
||||||
|
`lazydocker` and `woff2-font-awesome` sitting in the AUR list when they are in
|
||||||
|
official `extra`.
|
||||||
|
|
||||||
|
### Bootstrapping an AUR helper
|
||||||
|
|
||||||
|
A fresh Arch install has no AUR helper, and `yay` is not in the official
|
||||||
|
repositories. `install.sh` builds `yay-bin` from the AUR once with `makepkg`
|
||||||
|
before installing anything from the AUR list, which is why `base-devel` and
|
||||||
|
`git` are in the official list.
|
||||||
|
|
||||||
|
An entry may be written `wanted|already-fine`, which asks for the first name but
|
||||||
|
accepts either as satisfying the requirement. That is how
|
||||||
|
`hyprland-preview-share-picker-git` avoids conflicting with the non-git build
|
||||||
|
that the Omarchy repository still ships.
|
||||||
|
|
||||||
|
Three of the AUR entries are load-bearing for this fork and were not in
|
||||||
|
Omarchy's own package list, because they were installed by hand on this machine:
|
||||||
|
|
||||||
|
| Package | Needed by |
|
||||||
|
| --- | --- |
|
||||||
|
| `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 |
|
||||||
|
| `playerctl` | the quick settings media row |
|
||||||
|
|
||||||
|
`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
@@ -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).
|
||||||
|
|||||||
@@ -52,3 +52,61 @@ binding to the shell's own audio, brightness and media services. Polling only
|
|||||||
runs while a panel is open, so the cost is bounded, but wiring these to the
|
runs while a panel is open, so the cost is bounded, but wiring these to the
|
||||||
services is the obvious next refinement and would remove the last per-tick
|
services is the obvious next refinement and would remove the last per-tick
|
||||||
process spawns.
|
process spawns.
|
||||||
|
|
||||||
|
## Display layout
|
||||||
|
|
||||||
|
`blob.display-layout` arranges monitors. Opened with `Super + Shift + Ctrl + D`
|
||||||
|
or the Blob menu's Display layout row.
|
||||||
|
|
||||||
|
A scaled picture of the desktop, one draggable rectangle per monitor. Dropping
|
||||||
|
one applies the new position immediately. Edges snap to a neighbour's edge
|
||||||
|
within 60 logical pixels, so monitors end up touching exactly. Per-monitor
|
||||||
|
controls: mode, scale, enable or disable, and mirror.
|
||||||
|
|
||||||
|
**Scale options are filtered, not free.** Hyprland steps fractional scaling in
|
||||||
|
1/120 and rejects a scale that does not land on a whole number of physical
|
||||||
|
pixels on both axes. `DisplayModel.scaleIsValid` applies that rule, so only
|
||||||
|
usable scales are offered. On a 1920x1080 panel that means 1.0 and 1.2 are
|
||||||
|
adjacent with nothing between them, which is the constraint `hypr/monitors.lua`
|
||||||
|
documents by hand.
|
||||||
|
|
||||||
|
**Nothing is written to `monitors.lua`.** Every change goes through
|
||||||
|
`hyprctl keyword monitor` and lasts until the config reloads or the session
|
||||||
|
ends. "Reset to monitors.lua" runs `hyprctl reload` and makes the checked-in
|
||||||
|
config authoritative again. To keep a layout, edit `monitors.lua` yourself -
|
||||||
|
`blob display arrange list` prints the current layout in the exact keyword form,
|
||||||
|
ready to copy.
|
||||||
|
|
||||||
|
Rotation and primary-monitor selection are not implemented. `DisplayModel` reads
|
||||||
|
`transform` and round-trips it, so a rotated monitor is drawn and preserved
|
||||||
|
correctly, but nothing in the widget sets it.
|
||||||
|
|
||||||
|
## Wallpapers
|
||||||
|
|
||||||
|
`blob.displays` picks wallpapers. Opened with `Super + Alt + W` or the Blob
|
||||||
|
menu's Wallpaper row.
|
||||||
|
|
||||||
|
A thumbnail grid of `~/wallpapers`, with a row of chips selecting the target:
|
||||||
|
"All monitors" sets the global wallpaper through `blob-bg-set`, and a specific
|
||||||
|
output assigns just that monitor through `blob-bg-monitor`, which symlinks the
|
||||||
|
image under `~/.local/state/blob/backgrounds/<output>`. An output with its own
|
||||||
|
wallpaper is marked with an asterisk.
|
||||||
|
|
||||||
|
`blob.background` already drew one window per screen but pointed them all at the
|
||||||
|
same image. It now resolves per screen: a monitor with an assignment shows it,
|
||||||
|
and a monitor without one shows the global wallpaper. The crossfade machinery
|
||||||
|
stays on the global path only, so an assigned monitor swaps without the reveal
|
||||||
|
animation rather than risking that code.
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| `blob bg set <image>` | set the global wallpaper |
|
||||||
|
| `blob bg monitor <output> <image>` | assign a wallpaper to one monitor |
|
||||||
|
| `blob bg monitor <output> --clear` | fall back to the global wallpaper |
|
||||||
|
| `blob bg monitor --list` | show assignments |
|
||||||
|
| `blob display arrange list` | print the layout as monitor keywords |
|
||||||
|
| `blob display arrange apply <kw>` | apply a keyword now |
|
||||||
|
| `blob display arrange reset` | reload, restoring monitors.lua |
|
||||||
|
|
||||||
|
Both widgets share `Ui/Chip.qml`, `Ui/Card.qml` and `Ui/CardIconButton.qml`
|
||||||
|
rather than carrying their own copies.
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
-- Personal keybinding overrides on top of the defaults.
|
-- Personal keybinding overrides on top of the defaults.
|
||||||
-- See current bindings and descriptions: blob menu keybindings --print
|
-- See current bindings and descriptions: blob menu keybindings --print
|
||||||
|
|
||||||
o.bind("SUPER + ALT + W", "Wallpaper picker", "blob-wallpaper-set --menu")
|
o.bind("SUPER + ALT + W", "Wallpapers", "blob-shell shell toggle blob.displays")
|
||||||
o.bind("SUPER + CTRL + M", "System monitor", "blob-shell shell toggle blob.sysmon")
|
o.bind("SUPER + CTRL + M", "System monitor", "blob-shell shell toggle blob.sysmon")
|
||||||
|
o.bind("SUPER + SHIFT + CTRL + D", "Display layout", "blob-shell shell toggle blob.display-layout")
|
||||||
o.bind("SUPER + CTRL + Q", "Quick settings", "blob-shell shell toggle blob.quick-settings")
|
o.bind("SUPER + CTRL + Q", "Quick settings", "blob-shell shell toggle blob.quick-settings")
|
||||||
|
|
||||||
-- Apps on SUPER + SPACE, root menu on ALT, swapping the shipped defaults back.
|
-- Apps on SUPER + SPACE, root menu on ALT, swapping the shipped defaults back.
|
||||||
|
|||||||
+52
-142
@@ -5,20 +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
|
||||||
|
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
|
||||||
|
--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
|
||||||
@@ -28,113 +35,21 @@ while (( $# > 0 )); do
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--force) force=true; shift ;;
|
--force) force=true; shift ;;
|
||||||
--check) check=true; shift ;;
|
--check) check=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/theme.sh"
|
||||||
}
|
source "$repo_dir/install/steps/login.sh"
|
||||||
|
|
||||||
# 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
|
|
||||||
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
|
|
||||||
fi
|
|
||||||
|
|
||||||
note_change
|
|
||||||
if [[ $check == true ]]; then
|
|
||||||
say "would link BLOB_PATH -> $repo_dir"
|
|
||||||
return
|
|
||||||
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
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
|
|
||||||
say "ok $label"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
note_change
|
|
||||||
|
|
||||||
if [[ -e $dest ]] && [[ $force == false ]]; then
|
|
||||||
say "DIFF $label (local changes kept; --force to overwrite)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $check == true ]]; then
|
|
||||||
say "would write $label"
|
|
||||||
return
|
|
||||||
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
|
|
||||||
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
|
|
||||||
fi
|
|
||||||
|
|
||||||
note_change
|
|
||||||
if [[ $check == true ]]; then
|
|
||||||
say "would install wayland session entry (needs sudo)"
|
|
||||||
return
|
|
||||||
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"
|
||||||
@@ -143,47 +58,34 @@ if [[ $check == true ]]; then
|
|||||||
fi
|
fi
|
||||||
say ""
|
say ""
|
||||||
|
|
||||||
|
create_directories
|
||||||
link_blob_path
|
link_blob_path
|
||||||
|
install_packages
|
||||||
|
|
||||||
if [[ $check == false ]]; then
|
install_shipped_config
|
||||||
mkdir -p "$state_dir"/{toggles/hypr,indicators,notifications}
|
refresh_font_cache
|
||||||
mkdir -p "$config_dir/blob"/{hooks,extensions,plugins,themed,themes}
|
install_shell_profile
|
||||||
mkdir -p "$HOME/wallpapers"
|
install_bash_integration
|
||||||
fi
|
install_user_units
|
||||||
|
install_systemd_dropins
|
||||||
|
install_zen_config
|
||||||
|
|
||||||
install_tree "$repo_dir/hypr" "$config_dir/hypr" "hypr"
|
link_wallpapers
|
||||||
# config/ is the shipped-defaults directory. Everything in it is reachable as
|
install_default_theme
|
||||||
# $BLOB_PATH/config/... so `blob-refresh-config` can reset a file, and most of it
|
install_default_background
|
||||||
# is also the right thing to deploy on a fresh install. Two subdirectories are
|
link_app_themes
|
||||||
# reference-only and must not be copied into ~/.config:
|
|
||||||
#
|
enable_system_services
|
||||||
# blob/ the shell defaults; copying them over ~/.config/blob/shell.json would
|
mask_system_services
|
||||||
# replace the user's bar layout with the stock one
|
enable_user_units
|
||||||
# hypr/ the stock Hyprland config; the personal hypr/ below is authoritative
|
|
||||||
# and is installed to the same place
|
install_login_config
|
||||||
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
|
install_session_entry
|
||||||
|
install_autologin
|
||||||
zen_profile="$(find "$config_dir/zen" -maxdepth 1 -type d -name '*.Default (release)*' 2>/dev/null | head -1)"
|
enable_display_manager
|
||||||
if [[ -n $zen_profile ]]; then
|
report_greeter_compositor
|
||||||
install_file "$repo_dir/zen/userChrome.css" "$zen_profile/chrome/userChrome.css" "zen/userChrome.css"
|
report_login_conflicts
|
||||||
else
|
report_desktop_essentials
|
||||||
say "SKIP zen/userChrome.css (no Zen profile found)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
say ""
|
say ""
|
||||||
if [[ $check == true ]]; then
|
if [[ $check == true ]]; then
|
||||||
@@ -191,6 +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."
|
||||||
|
|
||||||
|
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 ""
|
||||||
say "Next: log out and pick the Blob session, then run 'blob theme set <name>'."
|
launch_desktop
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
as_root() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/as-root.sh"
|
||||||
|
|
||||||
|
say() {
|
||||||
|
printf '%s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
note_change() {
|
||||||
|
changes=$((changes + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
unit_search_dirs=(
|
||||||
|
/etc/systemd/system
|
||||||
|
/run/systemd/system
|
||||||
|
/usr/local/lib/systemd/system
|
||||||
|
/usr/lib/systemd/system
|
||||||
|
)
|
||||||
|
|
||||||
|
system_unit_exists() {
|
||||||
|
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() {
|
||||||
|
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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# ~/.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() {
|
||||||
|
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
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$blob_path")"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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"
|
||||||
|
}
|
||||||
@@ -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/.cache/blob"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
session_entry_dir=/usr/local/share/wayland-sessions
|
||||||
|
session_entry_search_dirs=(/usr/local/share/wayland-sessions /usr/share/wayland-sessions)
|
||||||
|
greeter_config_dir=/etc/sddm.conf.d
|
||||||
|
greeter_compositor_config=/etc/sddm/hyprland-greeter.lua
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# ISO, or a previous desktop left behind.
|
||||||
|
blob_greeter_conf="$greeter_config_dir/zz-blob.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.
|
||||||
|
# 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_root_file "$repo_dir/default/sddm/hyprland-greeter.lua" \
|
||||||
|
"$greeter_compositor_config" "sddm/hyprland-greeter.lua"
|
||||||
|
install_root_file "$repo_dir/default/sddm/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_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" "$blob_autologin_conf" "sddm.conf.d/zzz-blob-autologin.conf"
|
||||||
|
rm -f "$generated"
|
||||||
|
}
|
||||||
|
|
||||||
|
session_entry_exists() {
|
||||||
|
local entry="$1"
|
||||||
|
local dir
|
||||||
|
|
||||||
|
for dir in "${session_entry_search_dirs[@]}"; do
|
||||||
|
[[ -e $dir/$entry ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
setting_value() {
|
||||||
|
local file="$1" key="$2"
|
||||||
|
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
# from. Report the leftovers anyway: the file is confusing to find later, and the
|
||||||
|
# Omarchy ISO wrote an autologin that no package owns, so it outlives Omarchy.
|
||||||
|
report_login_conflicts() {
|
||||||
|
local file session
|
||||||
|
local found=false
|
||||||
|
|
||||||
|
for file in "$greeter_config_dir"/*.conf; do
|
||||||
|
[[ -e $file ]] || continue
|
||||||
|
[[ $file == "$blob_greeter_conf" || $file == "$blob_autologin_conf" ]] && continue
|
||||||
|
|
||||||
|
session="$(setting_value "$file" Session)"
|
||||||
|
[[ -n $session ]] || continue
|
||||||
|
session_entry_exists "$session" && continue
|
||||||
|
|
||||||
|
found=true
|
||||||
|
say "WARN $file autologs into $session, which is not installed"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $found == true ]]; then
|
||||||
|
say " Blob's config overrides it, so the greeter still comes up."
|
||||||
|
say " Delete the file to stop it coming back."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
if as_root systemctl enable sddm.service; then
|
||||||
|
say "enable sddm.service"
|
||||||
|
else
|
||||||
|
say "WARN could not enable sddm.service"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $target_needed == true ]]; then
|
||||||
|
if as_root systemctl set-default graphical.target; then
|
||||||
|
say "set default target to graphical.target"
|
||||||
|
else
|
||||||
|
say "WARN could not set the default target to graphical.target"
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
say "No login screen to start: sddm is not installed."
|
||||||
|
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 || {
|
||||||
|
say "WARN sddm.service did not start; see 'systemctl status sddm'"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
read_package_list() {
|
||||||
|
[[ -f $1 ]] || return 0
|
||||||
|
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
|
||||||
|
# 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#*|}
|
||||||
|
package_installed "$wanted" && continue
|
||||||
|
[[ $alternative != "$entry" ]] && package_installed "$alternative" && 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)"
|
||||||
|
install_repo_packages "${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)"
|
||||||
|
install_aur_packages "${aur_missing[@]}"
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
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.
|
||||||
|
#
|
||||||
|
# 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=(
|
||||||
|
NetworkManager-wait-online.service
|
||||||
|
plymouth-start.service
|
||||||
|
plymouth-quit-wait.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)"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Packages this desktop needs from the AUR.
|
||||||
|
# Installed by install.sh with: yay -S --needed
|
||||||
|
#
|
||||||
|
# These have no official-repo equivalent. See docs/packages.md.
|
||||||
|
|
||||||
|
# Palette extraction behind blob-wallpaper-set; pywal16 is the maintained fork
|
||||||
|
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
|
||||||
|
xdg-terminal-exec
|
||||||
|
|
||||||
|
# Screenshare picker used by the Hyprland portal
|
||||||
|
hyprland-preview-share-picker-git|hyprland-preview-share-picker
|
||||||
|
|
||||||
|
# Theming GUI that writes the same colors.toml the CLI does
|
||||||
|
aether
|
||||||
|
|
||||||
|
# Fonts and icons with no official build
|
||||||
|
ttf-ia-writer
|
||||||
|
yaru-icon-theme
|
||||||
|
|
||||||
|
# Tooling
|
||||||
|
mise-bin
|
||||||
|
ufw-docker
|
||||||
|
|
||||||
|
# Only in the Omarchy repository as binaries; these are the AUR sources
|
||||||
|
tzupdate
|
||||||
|
localsend
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Packages this desktop needs from the official Arch repositories.
|
||||||
|
# Installed by install.sh with: pacman -S --needed
|
||||||
|
#
|
||||||
|
# Derived from Omarchy's base list, minus its own software and the features
|
||||||
|
# this fork dropped, plus what the fork's own scripts and widgets need.
|
||||||
|
|
||||||
|
# Compositor and shell
|
||||||
|
hyprland
|
||||||
|
mesa
|
||||||
|
quickshell
|
||||||
|
uwsm
|
||||||
|
sddm
|
||||||
|
hyprland-guiutils
|
||||||
|
hyprpicker
|
||||||
|
hyprsunset
|
||||||
|
xdg-desktop-portal-hyprland
|
||||||
|
xdg-desktop-portal-gtk
|
||||||
|
gtk4-layer-shell
|
||||||
|
qt6-wayland
|
||||||
|
qt6-imageformats
|
||||||
|
wl-clipboard
|
||||||
|
wtype
|
||||||
|
socat
|
||||||
|
inotify-tools
|
||||||
|
plymouth
|
||||||
|
polkit
|
||||||
|
|
||||||
|
# Shell widget backends
|
||||||
|
playerctl
|
||||||
|
pamixer
|
||||||
|
brightnessctl
|
||||||
|
ddcutil
|
||||||
|
|
||||||
|
# Networking
|
||||||
|
networkmanager
|
||||||
|
nss-mdns
|
||||||
|
avahi
|
||||||
|
wireless-regdb
|
||||||
|
|
||||||
|
# Bluetooth
|
||||||
|
bluez
|
||||||
|
bluez-utils
|
||||||
|
bluez-tools
|
||||||
|
|
||||||
|
# Input method, for the XCompose sequences blob-fcitx5.service serves
|
||||||
|
fcitx5
|
||||||
|
fcitx5-gtk
|
||||||
|
fcitx5-qt
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
pipewire
|
||||||
|
pipewire-alsa
|
||||||
|
pipewire-jack
|
||||||
|
pipewire-pulse
|
||||||
|
wireplumber
|
||||||
|
alsa-utils
|
||||||
|
lsp-plugins-lv2
|
||||||
|
|
||||||
|
# Power and thermals
|
||||||
|
power-profiles-daemon
|
||||||
|
thermald
|
||||||
|
|
||||||
|
# Storage and devices
|
||||||
|
udiskie
|
||||||
|
gvfs-mtp
|
||||||
|
gvfs-nfs
|
||||||
|
gvfs-smb
|
||||||
|
dosfstools
|
||||||
|
exfatprogs
|
||||||
|
btrfs-progs
|
||||||
|
bolt
|
||||||
|
kernel-modules-hook
|
||||||
|
gnome-disk-utility
|
||||||
|
|
||||||
|
# Printing
|
||||||
|
cups
|
||||||
|
cups-filters
|
||||||
|
cups-pk-helper
|
||||||
|
system-config-printer
|
||||||
|
|
||||||
|
# Security
|
||||||
|
ufw
|
||||||
|
gnome-keyring
|
||||||
|
libsecret
|
||||||
|
|
||||||
|
# Boot and snapshots
|
||||||
|
limine
|
||||||
|
snapper
|
||||||
|
zram-generator
|
||||||
|
|
||||||
|
# System utilities
|
||||||
|
plocate
|
||||||
|
man-db
|
||||||
|
expac
|
||||||
|
pacman-contrib
|
||||||
|
|
||||||
|
# Terminals
|
||||||
|
foot
|
||||||
|
kitty
|
||||||
|
|
||||||
|
# Fonts and icons
|
||||||
|
ttf-jetbrains-mono-nerd
|
||||||
|
noto-fonts
|
||||||
|
noto-fonts-cjk
|
||||||
|
noto-fonts-emoji
|
||||||
|
fontconfig
|
||||||
|
woff2-font-awesome
|
||||||
|
gnome-themes-extra
|
||||||
|
|
||||||
|
# Image and video handling
|
||||||
|
imagemagick
|
||||||
|
libvips
|
||||||
|
webp-pixbuf-loader
|
||||||
|
ffmpegthumbnailer
|
||||||
|
|
||||||
|
# Theming
|
||||||
|
awww
|
||||||
|
|
||||||
|
# Capture
|
||||||
|
grim
|
||||||
|
slurp
|
||||||
|
gpu-screen-recorder
|
||||||
|
tesseract
|
||||||
|
tesseract-data-eng
|
||||||
|
qrencode
|
||||||
|
zbar
|
||||||
|
|
||||||
|
# Media
|
||||||
|
mpv
|
||||||
|
mpv-mpris
|
||||||
|
imv
|
||||||
|
evince
|
||||||
|
|
||||||
|
# Files
|
||||||
|
nautilus
|
||||||
|
nautilus-python
|
||||||
|
sushi
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
neovim
|
||||||
|
|
||||||
|
# Browser for web apps
|
||||||
|
chromium
|
||||||
|
|
||||||
|
# Build tools, needed to bootstrap an AUR helper
|
||||||
|
base-devel
|
||||||
|
git
|
||||||
|
|
||||||
|
# Command line
|
||||||
|
bat
|
||||||
|
eza
|
||||||
|
fd
|
||||||
|
ripgrep
|
||||||
|
fzf
|
||||||
|
zoxide
|
||||||
|
jq
|
||||||
|
starship
|
||||||
|
lazygit
|
||||||
|
btop
|
||||||
|
fastfetch
|
||||||
|
inxi
|
||||||
|
dua-cli
|
||||||
|
tldr
|
||||||
|
whois
|
||||||
|
inetutils
|
||||||
|
unzip
|
||||||
|
yt-dlp
|
||||||
|
gum
|
||||||
|
less
|
||||||
|
python-gobject
|
||||||
|
|
||||||
|
# Containers
|
||||||
|
docker
|
||||||
|
docker-compose
|
||||||
|
docker-buildx
|
||||||
|
lazydocker
|
||||||
@@ -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)"
|
||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string label: ""
|
||||||
|
property bool active: false
|
||||||
|
signal activated()
|
||||||
|
|
||||||
|
implicitWidth: text.implicitWidth + Style.spaceReal(16)
|
||||||
|
implicitHeight: Style.spaceReal(22)
|
||||||
|
radius: Style.cardRadius
|
||||||
|
opacity: enabled ? 1 : 0.4
|
||||||
|
color: root.active
|
||||||
|
? Color.accent
|
||||||
|
: (hover.hovered ? Util.alpha(Color.blue, 0.25) : Util.alpha(Color.background, Style.cardFillAlpha))
|
||||||
|
border.width: 1
|
||||||
|
border.color: root.active ? Color.accent : Util.alpha(Color.blue, Style.cardBorderAlpha)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: text
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.label
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: root.active ? Color.background : Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hover
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: root.activated()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,3 +37,4 @@ Card 1.0 Card.qml
|
|||||||
CardHeader 1.0 CardHeader.qml
|
CardHeader 1.0 CardHeader.qml
|
||||||
StatRow 1.0 StatRow.qml
|
StatRow 1.0 StatRow.qml
|
||||||
CardIconButton 1.0 CardIconButton.qml
|
CardIconButton 1.0 CardIconButton.qml
|
||||||
|
Chip 1.0 Chip.qml
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ Item {
|
|||||||
readonly property string home: Quickshell.env("HOME")
|
readonly property string home: Quickshell.env("HOME")
|
||||||
readonly property string stateHome: home + "/.local/state"
|
readonly property string stateHome: home + "/.local/state"
|
||||||
readonly property string currentBackgroundLink: stateHome + "/blob/current/background"
|
readonly property string currentBackgroundLink: stateHome + "/blob/current/background"
|
||||||
|
readonly property string monitorBackgroundDir: stateHome + "/blob/backgrounds"
|
||||||
|
|
||||||
|
// Per-monitor assignments, keyed by output name, written by blob-bg-monitor.
|
||||||
|
// A monitor with no entry falls back to the global background, so the
|
||||||
|
// transition machinery below stays on the single-image path it was built for.
|
||||||
|
property var monitorBackgrounds: ({})
|
||||||
|
|
||||||
|
function monitorBackgroundFor(name) {
|
||||||
|
var assigned = root.monitorBackgrounds[String(name || "")]
|
||||||
|
return assigned ? String(assigned) : ""
|
||||||
|
}
|
||||||
|
|
||||||
property string currentBackground: ""
|
property string currentBackground: ""
|
||||||
property string displayedBackground: ""
|
property string displayedBackground: ""
|
||||||
@@ -32,6 +43,20 @@ Item {
|
|||||||
|
|
||||||
function refreshBackground() {
|
function refreshBackground() {
|
||||||
if (!readlinkProc.running) readlinkProc.running = true
|
if (!readlinkProc.running) readlinkProc.running = true
|
||||||
|
if (!monitorBackgroundProc.running) monitorBackgroundProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMonitorBackgrounds(raw) {
|
||||||
|
var next = ({})
|
||||||
|
var lines = String(raw || "").split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
var split = line.indexOf("\t")
|
||||||
|
if (split <= 0) continue
|
||||||
|
next[line.substring(0, split)] = line.substring(split + 1)
|
||||||
|
}
|
||||||
|
root.monitorBackgrounds = next
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBackground(path, instant) {
|
function setBackground(path, instant) {
|
||||||
@@ -128,6 +153,18 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: monitorBackgroundProc
|
||||||
|
command: ["bash", "-c",
|
||||||
|
'dir="$1"; [[ -d $dir ]] || exit 0; ' +
|
||||||
|
'for link in "$dir"/*; do [[ -e $link ]] || continue; ' +
|
||||||
|
'printf "%s\\t%s\\n" "${link##*/}" "$(readlink -f "$link")"; done',
|
||||||
|
"--", root.monitorBackgroundDir]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.loadMonitorBackgrounds(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "background"
|
target: "background"
|
||||||
|
|
||||||
@@ -187,6 +224,9 @@ Item {
|
|||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
visible: !remapGuard.remapping
|
visible: !remapGuard.remapping
|
||||||
|
|
||||||
|
readonly property string assignedBackground: root.monitorBackgroundFor(modelData ? modelData.name : "")
|
||||||
|
readonly property bool hasAssignment: assignedBackground.length > 0
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
|
|
||||||
ScreenMoveRemap {
|
ScreenMoveRemap {
|
||||||
@@ -221,7 +261,7 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
id: base
|
id: base
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: root.imageUrl(root.displayedBackground)
|
source: root.imageUrl(panel.hasAssignment ? panel.assignedBackground : root.displayedBackground)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: true
|
cache: true
|
||||||
@@ -243,14 +283,14 @@ Item {
|
|||||||
cache: false
|
cache: false
|
||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
visible: root.oldBackground !== "" && root.revealProgress < 1
|
visible: !panel.hasAssignment && root.oldBackground !== "" && root.revealProgress < 1
|
||||||
onStatusChanged: panel.maybeStartReveal()
|
onStatusChanged: panel.maybeStartReveal()
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: incomingLayer
|
id: incomingLayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: root.incomingBackground !== "" && incomingFrame.status === Image.Ready && (root.revealProgress >= 1 || panel.maskReady)
|
visible: !panel.hasAssignment && root.incomingBackground !== "" && incomingFrame.status === Image.Ready && (root.revealProgress >= 1 || panel.maskReady)
|
||||||
layer.enabled: root.incomingBackground !== "" && root.revealProgress < 1
|
layer.enabled: root.incomingBackground !== "" && root.revealProgress < 1
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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\"}'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
import "DisplayModel.js" as DisplayModel
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var shell: null
|
||||||
|
property var manifest: null
|
||||||
|
property bool opened: false
|
||||||
|
|
||||||
|
readonly property string pluginId: (manifest && manifest.id) || "blob.display-layout"
|
||||||
|
readonly property int snapThreshold: 60
|
||||||
|
|
||||||
|
property var monitors: []
|
||||||
|
property string selectedName: ""
|
||||||
|
property bool layoutDirty: false
|
||||||
|
|
||||||
|
readonly property var selectedMonitor: {
|
||||||
|
for (var i = 0; i < root.monitors.length; i++)
|
||||||
|
if (root.monitors[i].name === root.selectedName) return root.monitors[i]
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(payloadJson) {
|
||||||
|
root.opened = true
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
root.opened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
root.opened = false
|
||||||
|
if (root.shell && typeof root.shell.hide === "function")
|
||||||
|
root.shell.hide(root.pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command) {
|
||||||
|
Quickshell.execDetached(["bash", "-c", command])
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!monitorProc.running) monitorProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMonitors(raw) {
|
||||||
|
var parsed = DisplayModel.parseMonitors(raw)
|
||||||
|
root.monitors = parsed
|
||||||
|
root.layoutDirty = false
|
||||||
|
if (root.selectedName.length > 0) {
|
||||||
|
for (var i = 0; i < parsed.length; i++)
|
||||||
|
if (parsed[i].name === root.selectedName) return
|
||||||
|
}
|
||||||
|
root.selectedName = parsed.length > 0 ? parsed[0].name : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutating the array in place would not retrigger the bindings the canvas
|
||||||
|
// reads, so every edit rebuilds it.
|
||||||
|
function replaceMonitor(name, changes) {
|
||||||
|
var next = []
|
||||||
|
for (var i = 0; i < root.monitors.length; i++) {
|
||||||
|
var monitor = root.monitors[i]
|
||||||
|
if (monitor.name !== name) {
|
||||||
|
next.push(monitor)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var copy = ({})
|
||||||
|
for (var key in monitor) copy[key] = monitor[key]
|
||||||
|
for (var change in changes) copy[change] = changes[change]
|
||||||
|
next.push(copy)
|
||||||
|
}
|
||||||
|
root.monitors = next
|
||||||
|
root.layoutDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveMonitor(name, x, y) {
|
||||||
|
var snapped = DisplayModel.snapPosition(root.monitors, name, x, y, root.snapThreshold)
|
||||||
|
root.replaceMonitor(name, { x: snapped.x, y: snapped.y })
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMonitor(name) {
|
||||||
|
for (var i = 0; i < root.monitors.length; i++) {
|
||||||
|
if (root.monitors[i].name !== name) continue
|
||||||
|
root.run("blob-display-arrange apply "
|
||||||
|
+ Util.shellQuote(DisplayModel.monitorKeyword(root.monitors[i])))
|
||||||
|
reloadTimer.restart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLayout() {
|
||||||
|
var keywords = DisplayModel.keywordsFor(root.monitors)
|
||||||
|
var quoted = []
|
||||||
|
for (var i = 0; i < keywords.length; i++) quoted.push(Util.shellQuote(keywords[i]))
|
||||||
|
root.run("blob-display-arrange apply " + quoted.join(" "))
|
||||||
|
root.layoutDirty = false
|
||||||
|
reloadTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLayout() {
|
||||||
|
root.run("blob-display-arrange reset")
|
||||||
|
reloadTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: monitorProc
|
||||||
|
command: ["hyprctl", "monitors", "all", "-j"]
|
||||||
|
stdout: StdioCollector { onStreamFinished: root.loadMonitors(text) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: reloadTimer
|
||||||
|
interval: 400
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 4000
|
||||||
|
running: root.opened && !root.layoutDirty
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenedChanged: if (root.opened) root.refresh()
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
visible: root.opened
|
||||||
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.namespace: "blob-display-layout"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Card {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: Style.spaceReal(720)
|
||||||
|
implicitHeight: Style.spaceReal(540)
|
||||||
|
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
|
||||||
|
spacing: Style.spaceReal(10)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: Math.max(title.implicitHeight, closeButton.implicitHeight)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: title
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Display layout"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
id: closeButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: "Drag a monitor to move it. Edges snap to a neighbour. Changes apply as you drop and last until the session ends."
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.6)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorCanvas {
|
||||||
|
width: parent.width
|
||||||
|
height: Style.spaceReal(220)
|
||||||
|
monitors: root.monitors
|
||||||
|
selectedName: root.selectedName
|
||||||
|
onSelected: function(name) { root.selectedName = name }
|
||||||
|
onMonitorMoved: function(name, x, y) { root.moveMonitor(name, x, y) }
|
||||||
|
onLayoutSettled: root.applyMonitor(root.selectedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorControls {
|
||||||
|
width: parent.width
|
||||||
|
monitor: root.selectedMonitor
|
||||||
|
monitors: root.monitors
|
||||||
|
onScaleRequested: function(scale) {
|
||||||
|
root.replaceMonitor(root.selectedName, { scale: scale })
|
||||||
|
root.applyMonitor(root.selectedName)
|
||||||
|
}
|
||||||
|
onModeRequested: function(mode) {
|
||||||
|
root.replaceMonitor(root.selectedName, { mode: mode })
|
||||||
|
root.applyMonitor(root.selectedName)
|
||||||
|
}
|
||||||
|
onEnabledRequested: function(enabled) {
|
||||||
|
root.replaceMonitor(root.selectedName, { disabled: !enabled })
|
||||||
|
root.applyMonitor(root.selectedName)
|
||||||
|
}
|
||||||
|
onMirrorRequested: function(target) {
|
||||||
|
root.replaceMonitor(root.selectedName, { mirrorOf: target })
|
||||||
|
root.applyMonitor(root.selectedName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(6)
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: root.layoutDirty ? "Apply all" : "Re-apply all"
|
||||||
|
active: root.layoutDirty
|
||||||
|
onActivated: root.applyLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "Reset to monitors.lua"
|
||||||
|
onActivated: root.resetLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
// hyprctl reports logical size as the raw mode divided by scale, so a 1920x1200
|
||||||
|
// panel at 1.5 occupies 1280x800 of layout space. Positions are in that same
|
||||||
|
// logical space, which is what the canvas has to draw.
|
||||||
|
function logicalSize(monitor) {
|
||||||
|
var scale = Number(monitor.scale) || 1
|
||||||
|
var width = Number(monitor.width) || 0
|
||||||
|
var height = Number(monitor.height) || 0
|
||||||
|
if (isRotated(monitor)) {
|
||||||
|
var swap = width
|
||||||
|
width = height
|
||||||
|
height = swap
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: Math.round(width / scale),
|
||||||
|
height: Math.round(height / scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRotated(monitor) {
|
||||||
|
var transform = Number(monitor.transform) || 0
|
||||||
|
return transform === 1 || transform === 3 || transform === 5 || transform === 7
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMonitors(raw) {
|
||||||
|
var parsed
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(String(raw || "[]"))
|
||||||
|
} catch (e) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (!Array.isArray(parsed)) return []
|
||||||
|
|
||||||
|
var monitors = []
|
||||||
|
for (var i = 0; i < parsed.length; i++) {
|
||||||
|
var m = parsed[i] || {}
|
||||||
|
var size = logicalSize(m)
|
||||||
|
monitors.push({
|
||||||
|
name: String(m.name || ""),
|
||||||
|
description: String(m.description || ""),
|
||||||
|
mode: modeStringOf(m),
|
||||||
|
modes: Array.isArray(m.availableModes) ? m.availableModes : [],
|
||||||
|
x: Number(m.x) || 0,
|
||||||
|
y: Number(m.y) || 0,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
scale: Number(m.scale) || 1,
|
||||||
|
transform: Number(m.transform) || 0,
|
||||||
|
disabled: m.disabled === true,
|
||||||
|
focused: m.focused === true,
|
||||||
|
mirrorOf: String(m.mirrorOf || "none")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
monitors.sort(function(a, b) { return a.x - b.x })
|
||||||
|
return monitors
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeStringOf(monitor) {
|
||||||
|
var width = Number(monitor.width) || 0
|
||||||
|
var height = Number(monitor.height) || 0
|
||||||
|
var rate = Number(monitor.refreshRate) || 0
|
||||||
|
if (!width || !height) return "preferred"
|
||||||
|
return width + "x" + height + "@" + rate.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The canvas is a scaled-down picture of the desktop. Monitors can sit at
|
||||||
|
// negative coordinates, so the bounding box is translated to the origin before
|
||||||
|
// a single scale factor is chosen for both axes.
|
||||||
|
function layoutBounds(monitors) {
|
||||||
|
if (!monitors.length) return { x: 0, y: 0, width: 1, height: 1 }
|
||||||
|
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
||||||
|
for (var i = 0; i < monitors.length; i++) {
|
||||||
|
var m = monitors[i]
|
||||||
|
if (m.disabled) continue
|
||||||
|
minX = Math.min(minX, m.x)
|
||||||
|
minY = Math.min(minY, m.y)
|
||||||
|
maxX = Math.max(maxX, m.x + m.width)
|
||||||
|
maxY = Math.max(maxY, m.y + m.height)
|
||||||
|
}
|
||||||
|
if (minX === Infinity) return { x: 0, y: 0, width: 1, height: 1 }
|
||||||
|
return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasScale(bounds, availableWidth, availableHeight) {
|
||||||
|
return Math.min(availableWidth / bounds.width, availableHeight / bounds.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap a dragged edge to a neighbour's edge when it lands within the threshold,
|
||||||
|
// so monitors end up touching exactly rather than a few pixels apart.
|
||||||
|
function snapPosition(monitors, name, x, y, threshold) {
|
||||||
|
var snappedX = x
|
||||||
|
var snappedY = y
|
||||||
|
var self = null
|
||||||
|
for (var i = 0; i < monitors.length; i++) {
|
||||||
|
if (monitors[i].name === name) { self = monitors[i]; break }
|
||||||
|
}
|
||||||
|
if (!self) return { x: Math.round(x), y: Math.round(y) }
|
||||||
|
|
||||||
|
for (var j = 0; j < monitors.length; j++) {
|
||||||
|
var other = monitors[j]
|
||||||
|
if (other.name === name || other.disabled) continue
|
||||||
|
|
||||||
|
var candidatesX = [
|
||||||
|
other.x + other.width,
|
||||||
|
other.x - self.width,
|
||||||
|
other.x
|
||||||
|
]
|
||||||
|
for (var cx = 0; cx < candidatesX.length; cx++) {
|
||||||
|
if (Math.abs(snappedX - candidatesX[cx]) <= threshold) {
|
||||||
|
snappedX = candidatesX[cx]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidatesY = [
|
||||||
|
other.y + other.height,
|
||||||
|
other.y - self.height,
|
||||||
|
other.y
|
||||||
|
]
|
||||||
|
for (var cy = 0; cy < candidatesY.length; cy++) {
|
||||||
|
if (Math.abs(snappedY - candidatesY[cy]) <= threshold) {
|
||||||
|
snappedY = candidatesY[cy]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { x: Math.round(snappedX), y: Math.round(snappedY) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hyprland's keyword form: monitor = name,mode,position,scale[,transform,N]
|
||||||
|
function monitorKeyword(monitor) {
|
||||||
|
if (monitor.disabled) return monitor.name + ",disable"
|
||||||
|
var parts = [
|
||||||
|
monitor.name,
|
||||||
|
monitor.mode,
|
||||||
|
monitor.x + "x" + monitor.y,
|
||||||
|
String(monitor.scale)
|
||||||
|
]
|
||||||
|
var keyword = parts.join(",")
|
||||||
|
if (monitor.transform && monitor.transform !== 0)
|
||||||
|
keyword += ",transform," + monitor.transform
|
||||||
|
if (monitor.mirrorOf && monitor.mirrorOf !== "none")
|
||||||
|
keyword += ",mirror," + monitor.mirrorOf
|
||||||
|
return keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
function keywordsFor(monitors) {
|
||||||
|
var out = []
|
||||||
|
for (var i = 0; i < monitors.length; i++) out.push(monitorKeyword(monitors[i]))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hyprland rejects a fractional scale that does not land on a whole number of
|
||||||
|
// physical pixels. It steps in 1/120, so a scale is valid when both axes come
|
||||||
|
// out integral at that granularity.
|
||||||
|
function scaleIsValid(monitor, scale) {
|
||||||
|
if (!scale || scale <= 0) return false
|
||||||
|
var stepped = Math.round(scale * 120) / 120
|
||||||
|
var width = Number(monitor.width) * Number(monitor.scale)
|
||||||
|
var height = Number(monitor.height) * Number(monitor.scale)
|
||||||
|
var logicalWidth = width / stepped
|
||||||
|
var logicalHeight = height / stepped
|
||||||
|
return Math.abs(logicalWidth - Math.round(logicalWidth)) < 0.001
|
||||||
|
&& Math.abs(logicalHeight - Math.round(logicalHeight)) < 0.001
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearbyScales(monitor) {
|
||||||
|
var options = []
|
||||||
|
for (var step = 60; step <= 300; step += 6) {
|
||||||
|
var candidate = step / 120
|
||||||
|
if (scaleIsValid(monitor, candidate)) options.push(candidate)
|
||||||
|
}
|
||||||
|
if (!options.length) options.push(1)
|
||||||
|
return options
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import "DisplayModel.js" as DisplayModel
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var monitors: []
|
||||||
|
property string selectedName: ""
|
||||||
|
readonly property int snapThreshold: 60
|
||||||
|
|
||||||
|
signal selected(string name)
|
||||||
|
signal monitorMoved(string name, int x, int y)
|
||||||
|
signal layoutSettled()
|
||||||
|
|
||||||
|
readonly property var bounds: DisplayModel.layoutBounds(root.monitors)
|
||||||
|
readonly property real fitScale: DisplayModel.canvasScale(
|
||||||
|
bounds,
|
||||||
|
Math.max(1, width - Style.spaceReal(24)),
|
||||||
|
Math.max(1, height - Style.spaceReal(24)))
|
||||||
|
|
||||||
|
color: Util.alpha(Color.background, 0.35)
|
||||||
|
radius: Style.cardRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Util.alpha(Color.foreground, 0.15)
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: stage
|
||||||
|
width: root.bounds.width * root.fitScale
|
||||||
|
height: root.bounds.height * root.fitScale
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.monitors
|
||||||
|
|
||||||
|
MonitorCard {
|
||||||
|
required property var modelData
|
||||||
|
monitor: modelData
|
||||||
|
canvasScale: root.fitScale
|
||||||
|
originX: root.bounds.x
|
||||||
|
originY: root.bounds.y
|
||||||
|
selected: modelData.name === root.selectedName
|
||||||
|
onPicked: root.selected(modelData.name)
|
||||||
|
onMoved: function(x, y) { root.monitorMoved(modelData.name, x, y) }
|
||||||
|
onDropped: root.layoutSettled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: root.monitors.length === 0
|
||||||
|
text: "No monitors reported"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.6)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var monitor: null
|
||||||
|
property bool selected: false
|
||||||
|
property real canvasScale: 1
|
||||||
|
property real originX: 0
|
||||||
|
property real originY: 0
|
||||||
|
|
||||||
|
signal picked()
|
||||||
|
signal moved(int x, int y)
|
||||||
|
signal dropped()
|
||||||
|
|
||||||
|
readonly property string name: monitor ? monitor.name : ""
|
||||||
|
readonly property bool disabled: monitor ? monitor.disabled === true : false
|
||||||
|
|
||||||
|
visible: !!monitor && !disabled
|
||||||
|
width: monitor ? Math.max(Style.spaceReal(24), monitor.width * canvasScale) : 0
|
||||||
|
height: monitor ? Math.max(Style.spaceReal(18), monitor.height * canvasScale) : 0
|
||||||
|
x: monitor ? (monitor.x - originX) * canvasScale : 0
|
||||||
|
y: monitor ? (monitor.y - originY) * canvasScale : 0
|
||||||
|
|
||||||
|
radius: Style.cardRadius
|
||||||
|
color: root.selected
|
||||||
|
? Util.alpha(Color.accent, 0.35)
|
||||||
|
: Util.alpha(Color.background, Style.cardFillAlpha)
|
||||||
|
border.width: Style.cardBorderWidth
|
||||||
|
border.color: root.selected ? Color.accent : Util.alpha(Color.blue, Style.cardBorderAlpha)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Style.spaceReal(2)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: root.name
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: root.height > Style.spaceReal(44)
|
||||||
|
text: root.monitor ? root.monitor.width + "x" + root.monitor.height : ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.7)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: root.monitor && root.monitor.focused && root.height > Style.spaceReal(58)
|
||||||
|
text: "focused"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.blue
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: root.picked()
|
||||||
|
}
|
||||||
|
|
||||||
|
property int dragOriginX: 0
|
||||||
|
property int dragOriginY: 0
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: drag
|
||||||
|
target: null
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
root.picked()
|
||||||
|
root.dragOriginX = root.monitor ? root.monitor.x : 0
|
||||||
|
root.dragOriginY = root.monitor ? root.monitor.y : 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.dropped()
|
||||||
|
}
|
||||||
|
onTranslationChanged: {
|
||||||
|
if (!active || !root.monitor) return
|
||||||
|
root.moved(
|
||||||
|
Math.round(root.dragOriginX + translation.x / root.canvasScale),
|
||||||
|
Math.round(root.dragOriginY + translation.y / root.canvasScale))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
import "DisplayModel.js" as DisplayModel
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var monitor: null
|
||||||
|
property var monitors: []
|
||||||
|
|
||||||
|
signal scaleRequested(real scale)
|
||||||
|
signal modeRequested(string mode)
|
||||||
|
signal enabledRequested(bool enabled)
|
||||||
|
signal mirrorRequested(string target)
|
||||||
|
|
||||||
|
readonly property var scaleOptions: monitor ? DisplayModel.nearbyScales(monitor) : []
|
||||||
|
|
||||||
|
spacing: Style.spaceReal(8)
|
||||||
|
visible: !!monitor
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.monitor ? root.monitor.name : ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.monitor ? root.monitor.description : ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: Util.alpha(Color.foreground, 0.65)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
StatRow {
|
||||||
|
width: parent.width
|
||||||
|
icon: ""
|
||||||
|
label: "Position"
|
||||||
|
value: root.monitor ? root.monitor.x + ", " + root.monitor.y : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
StatRow {
|
||||||
|
width: parent.width
|
||||||
|
icon: ""
|
||||||
|
label: "Logical size"
|
||||||
|
value: root.monitor ? root.monitor.width + "x" + root.monitor.height : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Mode"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.75)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.monitor ? root.monitor.modes : []
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
required property var modelData
|
||||||
|
label: String(modelData).replace("Hz", "")
|
||||||
|
active: root.monitor && String(modelData).indexOf(root.monitor.mode) === 0
|
||||||
|
onActivated: root.modeRequested(String(modelData).replace("Hz", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Scale"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.75)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.scaleOptions
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
required property var modelData
|
||||||
|
label: String(modelData)
|
||||||
|
active: root.monitor && Math.abs(root.monitor.scale - modelData) < 0.001
|
||||||
|
onActivated: root.scaleRequested(Number(modelData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: "Only scales that land on whole pixels are offered. Hyprland steps in 1/120 and rejects the rest."
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.55)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Style.spaceReal(6)
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: root.monitor && root.monitor.disabled ? "Enable" : "Disable"
|
||||||
|
onActivated: root.enabledRequested(root.monitor ? root.monitor.disabled === true : true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: root.monitor && root.monitor.mirrorOf !== "none" ? "Unmirror" : "Mirror"
|
||||||
|
active: root.monitor && root.monitor.mirrorOf !== "none"
|
||||||
|
enabled: root.monitors.length > 1
|
||||||
|
onActivated: {
|
||||||
|
if (!root.monitor) return
|
||||||
|
if (root.monitor.mirrorOf !== "none") {
|
||||||
|
root.mirrorRequested("none")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (var i = 0; i < root.monitors.length; i++) {
|
||||||
|
if (root.monitors[i].name !== root.monitor.name) {
|
||||||
|
root.mirrorRequested(root.monitors[i].name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "Display layout",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Blob",
|
||||||
|
"description": "Arrange monitors by dragging, and set their mode, scale, and mirroring",
|
||||||
|
"id": "blob.display-layout",
|
||||||
|
"kinds": [
|
||||||
|
"overlay"
|
||||||
|
],
|
||||||
|
"keepLoaded": true,
|
||||||
|
"entryPoints": {
|
||||||
|
"overlay": "DisplayLayout.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var shell: null
|
||||||
|
property var manifest: null
|
||||||
|
property bool opened: false
|
||||||
|
|
||||||
|
readonly property string pluginId: (manifest && manifest.id) || "blob.displays"
|
||||||
|
|
||||||
|
property var outputs: []
|
||||||
|
property string selectedName: ""
|
||||||
|
property var assignments: ({})
|
||||||
|
|
||||||
|
readonly property bool globalMode: root.selectedName === ""
|
||||||
|
readonly property string assignedPath: root.globalMode
|
||||||
|
? "" : String(root.assignments[root.selectedName] || "")
|
||||||
|
|
||||||
|
function open(payloadJson) {
|
||||||
|
root.opened = true
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
root.opened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
root.opened = false
|
||||||
|
if (root.shell && typeof root.shell.hide === "function")
|
||||||
|
root.shell.hide(root.pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command) {
|
||||||
|
Quickshell.execDetached(["bash", "-c", command])
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!outputProc.running) outputProc.running = true
|
||||||
|
if (!assignmentProc.running) assignmentProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOutputs(raw) {
|
||||||
|
var names = []
|
||||||
|
var lines = String(raw || "").split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (line.length > 0) names.push(line)
|
||||||
|
}
|
||||||
|
root.outputs = names
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAssignments(raw) {
|
||||||
|
var next = ({})
|
||||||
|
var lines = String(raw || "").split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
var split = line.indexOf("\t")
|
||||||
|
if (split <= 0) continue
|
||||||
|
next[line.substring(0, split)] = line.substring(split + 1)
|
||||||
|
}
|
||||||
|
root.assignments = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(path) {
|
||||||
|
if (root.globalMode) {
|
||||||
|
root.run("blob-bg-set " + Util.shellQuote(path))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.run("blob-bg-monitor " + Util.shellQuote(root.selectedName)
|
||||||
|
+ " " + Util.shellQuote(path))
|
||||||
|
var next = ({})
|
||||||
|
for (var key in root.assignments) next[key] = root.assignments[key]
|
||||||
|
next[root.selectedName] = path
|
||||||
|
root.assignments = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAssignment() {
|
||||||
|
if (root.globalMode) return
|
||||||
|
root.run("blob-bg-monitor " + Util.shellQuote(root.selectedName) + " --clear")
|
||||||
|
var next = ({})
|
||||||
|
for (var key in root.assignments)
|
||||||
|
if (key !== root.selectedName) next[key] = root.assignments[key]
|
||||||
|
root.assignments = next
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: outputProc
|
||||||
|
command: ["bash", "-c", "hyprctl monitors -j | jq -r '.[].name'"]
|
||||||
|
stdout: StdioCollector { onStreamFinished: root.loadOutputs(text) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: assignmentProc
|
||||||
|
command: ["bash", "-c",
|
||||||
|
'dir="$HOME/.local/state/blob/backgrounds"; [[ -d $dir ]] || exit 0; ' +
|
||||||
|
'for link in "$dir"/*; do [[ -e $link ]] || continue; ' +
|
||||||
|
'printf "%s\\t%s\\n" "${link##*/}" "$(readlink -f "$link")"; done']
|
||||||
|
stdout: StdioCollector { onStreamFinished: root.loadAssignments(text) }
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenedChanged: if (root.opened) root.refresh()
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
visible: root.opened
|
||||||
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.namespace: "blob-wallpapers"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Card {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: Style.spaceReal(720)
|
||||||
|
implicitHeight: Style.spaceReal(520)
|
||||||
|
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
|
||||||
|
spacing: Style.spaceReal(10)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: Math.max(title.implicitHeight, closeButton.implicitHeight)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: title
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Wallpapers"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
id: closeButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "All monitors"
|
||||||
|
active: root.globalMode
|
||||||
|
onActivated: root.selectedName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.outputs
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
required property var modelData
|
||||||
|
label: root.assignments[modelData]
|
||||||
|
? String(modelData) + " *" : String(modelData)
|
||||||
|
active: root.selectedName === modelData
|
||||||
|
onActivated: root.selectedName = String(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: status.implicitHeight
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: status
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.globalMode
|
||||||
|
? "Setting the wallpaper for every monitor without its own"
|
||||||
|
: (root.assignedPath.length > 0
|
||||||
|
? root.selectedName + ": " + root.assignedPath.replace(/^.*\//, "")
|
||||||
|
: root.selectedName + ": following the global wallpaper")
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.7)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
label: "Clear"
|
||||||
|
enabled: !root.globalMode && root.assignedPath.length > 0
|
||||||
|
onActivated: root.clearAssignment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperGrid {
|
||||||
|
width: parent.width
|
||||||
|
height: Style.spaceReal(340)
|
||||||
|
polling: root.opened
|
||||||
|
assignedPath: root.globalMode ? "" : root.assignedPath
|
||||||
|
onChosen: function(path) { root.choose(path) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string directory: Quickshell.env("HOME") + "/wallpapers"
|
||||||
|
property string assignedPath: ""
|
||||||
|
property bool polling: false
|
||||||
|
property var wallpapers: []
|
||||||
|
|
||||||
|
signal chosen(string path)
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!listProc.running) listProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: listProc
|
||||||
|
command: ["bash", "-c",
|
||||||
|
'find -L "$1" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" ' +
|
||||||
|
'-o -iname "*.png" -o -iname "*.webp" -o -iname "*.gif" \\) | sort',
|
||||||
|
"--", root.directory]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
var out = []
|
||||||
|
var lines = String(text).split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (line.length > 0) out.push(line)
|
||||||
|
}
|
||||||
|
root.wallpapers = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPollingChanged: if (root.polling && root.wallpapers.length === 0) root.refresh()
|
||||||
|
Component.onCompleted: root.refresh()
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: grid
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
cellWidth: Style.spaceReal(120)
|
||||||
|
cellHeight: Style.spaceReal(80)
|
||||||
|
model: root.wallpapers
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
required property var modelData
|
||||||
|
width: grid.cellWidth
|
||||||
|
height: grid.cellHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.spaceReal(3)
|
||||||
|
radius: Style.cardRadius
|
||||||
|
color: Util.alpha(Color.background, Style.cardFillAlpha)
|
||||||
|
border.width: Style.cardBorderWidth
|
||||||
|
border.color: modelData === root.assignedPath
|
||||||
|
? Color.accent
|
||||||
|
: (hover.hovered ? Color.blue : Util.alpha(Color.blue, Style.cardBorderAlpha))
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.cardBorderWidth
|
||||||
|
source: Util.fileUrl(modelData)
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
sourceSize.width: Math.round(Style.spaceReal(120) * 1.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Style.cardBorderWidth
|
||||||
|
height: caption.implicitHeight + Style.spaceReal(4)
|
||||||
|
visible: hover.hovered || modelData === root.assignedPath
|
||||||
|
color: Util.alpha(Color.background, 0.8)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: caption
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Style.spaceReal(6)
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
text: String(modelData).replace(/^.*\//, "")
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hover
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: root.chosen(String(modelData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: root.wallpapers.length === 0
|
||||||
|
text: "No images in " + root.directory
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Util.alpha(Color.foreground, 0.6)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "Wallpapers",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Blob",
|
||||||
|
"description": "Pick wallpapers, globally or per monitor",
|
||||||
|
"id": "blob.displays",
|
||||||
|
"kinds": [
|
||||||
|
"overlay"
|
||||||
|
],
|
||||||
|
"keepLoaded": true,
|
||||||
|
"entryPoints": {
|
||||||
|
"overlay": "Displays.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,13 +192,10 @@ Item {
|
|||||||
onClicked: root.dismiss()
|
onClicked: root.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Style.spaceReal(10)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Style.spaceReal(12)
|
|
||||||
|
|
||||||
WeatherCard {
|
WeatherCard {
|
||||||
|
anchors.top: controlCentre.top
|
||||||
|
anchors.right: controlCentre.left
|
||||||
|
anchors.rightMargin: Style.spaceReal(12)
|
||||||
available: root.weatherAvailable
|
available: root.weatherAvailable
|
||||||
glyph: root.weatherGlyph
|
glyph: root.weatherGlyph
|
||||||
place: root.weatherPlace
|
place: root.weatherPlace
|
||||||
@@ -201,6 +205,9 @@ Item {
|
|||||||
|
|
||||||
Card {
|
Card {
|
||||||
id: controlCentre
|
id: controlCentre
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Style.spaceReal(10) + root.barInset
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
implicitWidth: Style.spaceReal(400)
|
implicitWidth: Style.spaceReal(400)
|
||||||
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
|
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
|
||||||
spacing: Style.spaceReal(12)
|
spacing: Style.spaceReal(12)
|
||||||
@@ -265,7 +272,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onOpenedChanged: if (root.opened) root.refreshState()
|
onOpenedChanged: if (root.opened) root.refreshState()
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-22
@@ -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"
|
||||||
|
|||||||
+64
-2
@@ -4,9 +4,35 @@ 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/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/zzz-blob-autologin.conf
|
||||||
|
/etc/sddm/hyprland-greeter.conf
|
||||||
|
/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 +42,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 +52,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 +69,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
|
||||||
@@ -67,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
|
||||||
@@ -80,11 +113,40 @@ 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"
|
||||||
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user