diff --git a/bin/omarchy-cmd-first-run b/bin/omarchy-cmd-first-run index 830bef54..67a0c321 100755 --- a/bin/omarchy-cmd-first-run +++ b/bin/omarchy-cmd-first-run @@ -10,6 +10,7 @@ if [[ -f $FIRST_RUN_MODE ]]; then rm -f "$FIRST_RUN_MODE" bash "$OMARCHY_PATH/install/first-run/battery-monitor.sh" + bash "$OMARCHY_PATH/install/first-run/recover-internal-monitor.sh" bash "$OMARCHY_PATH/install/first-run/cleanup-reboot-sudoers.sh" bash "$OMARCHY_PATH/install/first-run/firewall.sh" bash "$OMARCHY_PATH/install/first-run/dns-resolver.sh" diff --git a/bin/omarchy-hw-external-monitors b/bin/omarchy-hw-external-monitors new file mode 100755 index 00000000..b5dc967b --- /dev/null +++ b/bin/omarchy-hw-external-monitors @@ -0,0 +1,10 @@ +#!/bin/bash + +# Returns true when an external monitor is physically connected. +# Uses kernel DRM state so the result is independent of Hyprland's startup timing. + +for status in /sys/class/drm/card*-*/status; do + [[ "$status" == *-eDP-*/status ]] && continue + [[ "$(<"$status")" == "connected" ]] && exit 0 +done +exit 1 diff --git a/bin/omarchy-hw-recover-internal-monitor b/bin/omarchy-hw-recover-internal-monitor new file mode 100755 index 00000000..794cafea --- /dev/null +++ b/bin/omarchy-hw-recover-internal-monitor @@ -0,0 +1,11 @@ +#!/bin/bash + +# Clear the internal-monitor-disable toggle if no external display is connected. +# Runs before the graphical session so Hyprland doesn't block on having no output +# to render to when the user rebooted with the external unplugged. + +TOGGLE="$HOME/.local/state/omarchy/toggles/hypr/internal-monitor-disable.conf" + +if [[ -f $TOGGLE ]] && ! omarchy-hw-external-monitors; then + rm -f "$TOGGLE" +fi diff --git a/bin/omarchy-hyprland-monitor-internal b/bin/omarchy-hyprland-monitor-internal index 74e6f491..6936d2cf 100755 --- a/bin/omarchy-hyprland-monitor-internal +++ b/bin/omarchy-hyprland-monitor-internal @@ -9,7 +9,7 @@ enable() { } disable() { - if omarchy-hyprland-monitors-many; then + if omarchy-hw-external-monitors; then if omarchy-hyprland-toggle-disabled "$TOGGLE"; then omarchy-hyprland-toggle --enabled-notification "󰍹 Laptop display disabled" "$TOGGLE" fi @@ -20,8 +20,8 @@ disable() { } recover() { - if omarchy-hyprland-monitors-none && omarchy-hyprland-toggle-enabled "$TOGGLE"; then - enable + if ! omarchy-hw-external-monitors && omarchy-hyprland-toggle-enabled "$TOGGLE"; then + omarchy-hyprland-toggle "$TOGGLE" fi } diff --git a/bin/omarchy-hyprland-monitors-many b/bin/omarchy-hyprland-monitors-many deleted file mode 100755 index 50035c3f..00000000 --- a/bin/omarchy-hyprland-monitors-many +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# Returns true when there are multiple monitors connected (so we can disable the internal one) - -(( $(hyprctl monitors -j 2>/dev/null | jq length) > 1 )) diff --git a/bin/omarchy-hyprland-monitors-none b/bin/omarchy-hyprland-monitors-none deleted file mode 100755 index e5061f63..00000000 --- a/bin/omarchy-hyprland-monitors-none +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# Returns true when no monitors are connected - -(( $(hyprctl monitors -j 2>/dev/null | jq length) == 0 )) diff --git a/bin/omarchy-sudo-passwordless-toggle b/bin/omarchy-sudo-passwordless-toggle index f2071f62..530a7dea 100755 --- a/bin/omarchy-sudo-passwordless-toggle +++ b/bin/omarchy-sudo-passwordless-toggle @@ -1,12 +1,19 @@ #!/bin/bash # Toggle passwordless sudo for the current user. +# Usage: omarchy-sudo-passwordless-toggle [MINUTES] # First run: enables passwordless sudo for 15 minutes (after confirmation). # Second run: disables it early. NOPASSWD_FILE="/etc/sudoers.d/99-omarchy-nopasswd-${USER}" TIMER_NAME="omarchy-nopasswd-expire-${USER}" +MINUTES=${1:-15} +if [[ $1 && ! $1 =~ ^[0-9]+$ ]]; then + echo "Usage: omarchy-sudo-passwordless-toggle [MINUTES]" >&2 + exit 1 +fi + # Safety: if the file exists but the timer doesn't (e.g. after reboot), clean up if sudo test -f "$NOPASSWD_FILE" && ! systemctl is-active "${TIMER_NAME}.timer" &>/dev/null; then sudo rm "$NOPASSWD_FILE" @@ -14,28 +21,37 @@ fi # Check for the file directly — sudo -n can stay cached or be granted by other rules if sudo test -f "$NOPASSWD_FILE"; then - sudo rm "$NOPASSWD_FILE" - sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null - echo "Passwordless sudo has been DISABLED. Sudo will require a password again." + if [[ $1 ]]; then + sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null + sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \ + rm "$NOPASSWD_FILE" + echo "Passwordless sudo timer updated. It will now automatically disable in ${MINUTES} minutes." + else + sudo rm "$NOPASSWD_FILE" + sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null + echo "Passwordless sudo has been DISABLED. Sudo will require a password again." + fi else echo "" - echo "⚠️ WARNING: This will allow ANY process running as your user to" - echo "execute ANY command as root WITHOUT a password for 15 minutes." + echo "⚠️WARNING: This will allow ANY process running as your user to" + echo "execute ANY command as root WITHOUT a password for ${MINUTES} minutes." echo "" echo "This is useful for AI agents that need to run sudo commands," echo "but it significantly weakens the security of your system." echo "Anyone or anything with access to your user account gets full root." echo "" - echo "Passwordless sudo will automatically disable after 15 minutes." + echo "Passwordless sudo will automatically disable after ${MINUTES} minutes." echo "Run this command again to disable it early." echo "" - if gum confirm "Enable passwordless sudo for 15 minutes? This is a significant security risk!"; then + if gum confirm "Enable passwordless sudo for ${MINUTES} minutes? This is a significant security risk!"; then echo "${USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee "$NOPASSWD_FILE" > /dev/null sudo chmod 440 "$NOPASSWD_FILE" - sudo systemd-run --on-active=15m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \ + sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \ rm "$NOPASSWD_FILE" - echo "Passwordless sudo has been ENABLED. It will automatically disable in 15 minutes." + + echo "" + echo "Passwordless sudo has been ENABLED. It will automatically disable in ${MINUTES} minutes." echo "Note: if you restart before then, run omarchy-sudo-passwordless-toggle again to disable it." else echo "Aborted. No changes made." diff --git a/config/systemd/user/omarchy-recover-internal-monitor.service b/config/systemd/user/omarchy-recover-internal-monitor.service new file mode 100644 index 00000000..fef52180 --- /dev/null +++ b/config/systemd/user/omarchy-recover-internal-monitor.service @@ -0,0 +1,11 @@ +[Unit] +Description=Recover the internal monitor toggle when no external display is connected +Before=graphical-session-pre.target +ConditionPathExists=%h/.local/state/omarchy/toggles/hypr/internal-monitor-disable.conf + +[Service] +Type=oneshot +ExecStart=%h/.local/share/omarchy/bin/omarchy-hw-recover-internal-monitor + +[Install] +WantedBy=graphical-session-pre.target diff --git a/default/bash/aliases b/default/bash/aliases index b8d9cb75..b3f6ad22 100644 --- a/default/bash/aliases +++ b/default/bash/aliases @@ -48,6 +48,7 @@ alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode bypassPermis alias d='docker' alias r='rails' alias t='tmux attach || tmux new -s Work' +alias i='tdl c cx' n() { if [ "$#" -eq 0 ]; then command nvim . ; else command nvim "$@"; fi; } # Git diff --git a/default/hypr/autostart.conf b/default/hypr/autostart.conf index f34aa49f..984b55a3 100644 --- a/default/hypr/autostart.conf +++ b/default/hypr/autostart.conf @@ -7,7 +7,6 @@ exec-once = uwsm-app -- swayosd-server exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 exec-once = omarchy-cmd-first-run exec-once = omarchy-powerprofiles-init -exec-once = omarchy-hyprland-monitor-internal recover exec-once = uwsm-app -- omarchy-hyprland-monitor-watch # Slow app launch fix -- set systemd vars diff --git a/default/snapper/root b/default/snapper/root new file mode 100644 index 00000000..96d0ef23 --- /dev/null +++ b/default/snapper/root @@ -0,0 +1,8 @@ +# Omarchy snapshots root only for pre-update recovery — kept to 5, no timeline +SUBVOLUME="/" +FSTYPE="btrfs" + +NUMBER_LIMIT="5" +NUMBER_LIMIT_IMPORTANT="5" + +TIMELINE_CREATE="no" diff --git a/install/first-run/recover-internal-monitor.sh b/install/first-run/recover-internal-monitor.sh new file mode 100644 index 00000000..c1e84fc0 --- /dev/null +++ b/install/first-run/recover-internal-monitor.sh @@ -0,0 +1 @@ +systemctl --user enable omarchy-recover-internal-monitor.service diff --git a/install/login/limine-snapper.sh b/install/login/limine-snapper.sh index c91815dc..d588f43f 100644 --- a/install/login/limine-snapper.sh +++ b/install/login/limine-snapper.sh @@ -50,26 +50,14 @@ EOF # We overwrite the whole thing knowing the limine-update will add the entries for us sudo cp $OMARCHY_PATH/default/limine/limine.conf /boot/limine.conf - # Match Snapper configs if not installing from the ISO - if [[ -z ${OMARCHY_CHROOT_INSTALL:-} ]]; then - if ! sudo snapper list-configs 2>/dev/null | grep -q "root"; then - sudo snapper -c root create-config / - fi - - if ! sudo snapper list-configs 2>/dev/null | grep -q "home"; then - sudo snapper -c home create-config /home - fi + # Only snapshot root — /home is user data; rolling it back loses user work + if ! sudo snapper list-configs 2>/dev/null | grep -q "root"; then + sudo snapper -c root create-config / fi + sudo cp $OMARCHY_PATH/default/snapper/root /etc/snapper/configs/root - # Enable quota to allow space-aware algorithms to work - sudo btrfs quota enable / - - # Tweak default Snapper configs - sudo sed -i 's/^TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/{root,home} - sudo sed -i 's/^NUMBER_LIMIT="50"/NUMBER_LIMIT="5"/' /etc/snapper/configs/{root,home} - sudo sed -i 's/^NUMBER_LIMIT_IMPORTANT="10"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/{root,home} - sudo sed -i 's/^SPACE_LIMIT="0.5"/SPACE_LIMIT="0.3"/' /etc/snapper/configs/{root,home} - sudo sed -i 's/^FREE_LIMIT="0.2"/FREE_LIMIT="0.3"/' /etc/snapper/configs/{root,home} + # Disable btrfs quotas — full qgroup accounting is a major performance drag + sudo btrfs quota disable / 2>/dev/null || true chrootable_systemctl_enable limine-snapper-sync.service fi diff --git a/migrations/1776927490.sh b/migrations/1776927490.sh new file mode 100644 index 00000000..d2c743d9 --- /dev/null +++ b/migrations/1776927490.sh @@ -0,0 +1,50 @@ +echo "Drop /home snapshots, btrfs quotas, and timeline snapshots (keep 5 root snapshots, never more)" + +if ! omarchy-cmd-present snapper btrfs; then + exit 0 +fi + +# Disable btrfs quotas first — every subvolume delete below would otherwise update +# all qgroups, which is exactly the performance drag we're removing. +sudo btrfs quota disable / 2>/dev/null || true + +# Remove the home config, all its snapshots, and the .snapshots subvolume. +# If the snapshots look like someone's been using them manually (pre/post pairs, +# userdata tags, freeform descriptions), ask before destroying. +if sudo snapper list-configs 2>/dev/null | grep -q "home"; then + drop_home="yes" + + if sudo snapper -c home --csvout list 2>/dev/null | \ + awk -F, 'NR>1 && ($6=="pre" || $6=="post" || $13!="" || ($12!="current" && $12!="timeline" && $12 !~ /^[0-9]+\.[0-9]+\.[0-9]+/))' | \ + grep -q .; then + gum confirm "Drop unused /home snapshots for better performance?" || drop_home="no" + fi + + if [[ $drop_home == "yes" ]]; then + sudo snapper -c home list --columns number 2>/dev/null | awk 'NR>2 && $1 != "0" {print $1}' | \ + xargs -r sudo snapper -c home delete 2>/dev/null + sudo snapper -c home delete-config 2>/dev/null + + if [[ -d /home/.snapshots ]]; then + for snap in /home/.snapshots/*/snapshot; do + [[ -d $snap ]] && sudo btrfs subvolume delete "$snap" 2>/dev/null + done + sudo rm -rf /home/.snapshots/* 2>/dev/null + sudo btrfs subvolume delete /home/.snapshots 2>/dev/null + fi + fi +fi + +# Ensure root config exists and matches our shipped defaults +if ! sudo snapper list-configs 2>/dev/null | grep -q "root"; then + sudo snapper -c root create-config / +fi +sudo cp $OMARCHY_PATH/default/snapper/root /etc/snapper/configs/root + +# Delete all timeline snapshots — we only want pre-update (cleanup=number) snapshots +sudo snapper -c root list --columns number,cleanup 2>/dev/null | \ + awk '$3 == "timeline" {print $1}' | \ + xargs -r sudo snapper -c root delete 2>/dev/null + +# Enforce NUMBER_LIMIT=5 on any existing number snapshots beyond the new cap +sudo snapper -c root cleanup number 2>/dev/null diff --git a/migrations/1776935686.sh b/migrations/1776935686.sh new file mode 100644 index 00000000..303db7b8 --- /dev/null +++ b/migrations/1776935686.sh @@ -0,0 +1,9 @@ +echo "Recover the internal monitor at login when no external display is connected" + +SERVICE=omarchy-recover-internal-monitor.service + +mkdir -p ~/.config/systemd/user +cp $OMARCHY_PATH/config/systemd/user/$SERVICE ~/.config/systemd/user/$SERVICE + +systemctl --user daemon-reload +systemctl --user enable $SERVICE