Skip to content

Instantly share code, notes, and snippets.

@tecnologer
Last active September 6, 2025 00:20
Show Gist options
  • Select an option

  • Save tecnologer/243ea9f6ce661137a2b9c021c95fc0e3 to your computer and use it in GitHub Desktop.

Select an option

Save tecnologer/243ea9f6ce661137a2b9c021c95fc0e3 to your computer and use it in GitHub Desktop.
Battery Alert Notifications on Pop!_OS 22.04 (GNOME/X11)

Battery Alerts (Cursor-Centered Popups) for Pop!_OS / GNOME (X11)

Get visible pop-up alerts when your laptop battery:

  • discharging and drops below a threshold
image
  • charging and reaches a target level.
image

Popups appear centered at your mouse cursor (X11 only).

⚠️ Cursor-centering uses xdotool/wmctrl which require Xorg (X11). On Wayland, notifications will work but won’t be repositioned at the cursor.

Features

  • Set low battery and charging reached thresholds.
  • Pops up near the cursor so you don’t miss it.
  • Runs automatically via systemd user timer.
  • Optional auto-dismiss timeout.

Requirements

Install these packages:

# Debian/Ubuntu/Pop!_OS
sudo apt install upower zenity xdotool wmctrl

1) Export GUI environment to systemd (IMPORTANT)

Systemd (user) doesn’t automatically inherit your desktop env. Add this line so your service can access your display & D-Bus:

  • zsh: add to ~/.zprofile
  • bash: add to ~/.profile
# Make desktop env vars available to systemd --user on login
systemctl --user import-environment DISPLAY WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR

Log out and back in (or run that command once in a terminal now).

Verify:

systemctl --user show-environment | egrep 'DISPLAY|DBUS|XDG_RUNTIME_DIR'

The variables should be visible. I.e.

DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus
DISPLAY=:1
XDG_RUNTIME_DIR=/run/user/1001

2) Files & Paths

Place the files like this (adjust paths if you prefer):

~/bin/battery-alert.sh
~/.config/systemd/user/battery-alert.service
~/.config/systemd/user/battery-alert.timer

Make the script executable:

chmod +x ~/bin/battery-alert.sh

3) Configure (optional)

Open ~/bin/battery-alert.sh and tweak:

LOW_BATTERY=20        # notify when discharging at/below this %
CHARGED_LEVEL=95      # notify when charging at/above this %
POPUP_TIMEOUT=8       # seconds; set 0 to require manual close
POPUP_WIDTH=280
POPUP_HEIGHT=120

4) Enable the systemd user timer

systemctl --user daemon-reload
systemctl --user enable --now battery-alert.timer
systemctl --user list-timers | grep battery-alert
Test once manually:
systemctl --user start battery-alert.service
journalctl --user -u battery-alert.service -n 50 --no-pager

You should see a popup when thresholds are met. (You can also temporarily set LOW_BATTERY=100 to force a low-battery alert for testing while on battery.)

Uninstall / Disable

systemctl --user disable --now battery-alert.timer
rm -f ~/.config/systemd/user/battery-alert.{service,timer}
rm -f ~/bin/battery-alert.sh
systemctl --user daemon-reload

Troubleshooting

Popup appears then vanishes immediately

  • Ensure the script waits for the popup PID (already implemented).
  • Avoid backgrounding zenity without waiting; systemd will end the cgroup when the oneshot service exits.

zenity: cannot open display

  • Your user manager lacks GUI env vars. Re-login after adding the line in Step 1 to ~/.zprofile or ~/.profile, or run:
     systemctl --user import-environment DISPLAY WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR

Cursor centering doesn’t work

  • You’re probably on Wayland. Use an Xorg session (login screen → gear icon → GNOME on Xorg), or accept non-centered popups.

Service ran repeatedly and then failed with start-limit-hit

  • Use the timer + oneshot setup in this repo (don’t set Restart=always on a oneshot that exits quickly).

Check logs

journalctl --user -u battery-alert.service -n 100 --no-pager

How it works (brief)

  • The timer runs battery-alert.service every 60s.
  • The script reads battery state/percentage via upower.
  • If thresholds are hit, it opens a zenity window and uses xdotool/wmctrl to center it at the cursor.
  • The script waits for the popup to close (or time out), keeping the systemd unit alive so the popup isn’t killed.

License

MIT

[Unit]
Description=Battery Alert (popup near cursor)
After=graphical-session.target
[Service]
Type=oneshot
# Let the script run zenity until it closes (we don't background it permanently)
# If you *do* background UI processes, KillMode=process prevents systemd from killing them:
# KillMode=process
ExecStart=/usr/bin/env bash -lc '/home/%u/bin/battery-alert.sh'
#!/usr/bin/env bash
set -euo pipefail
# ---- SETTINGS ----
LOW_BATTERY=20 # notify when discharging at/below this %
CHARGED_LEVEL=95 # notify when charging at/above this %
POPUP_WIDTH=280
POPUP_HEIGHT=120
POPUP_TIMEOUT=8 # seconds; set 0 to require manual close
TITLE_LOW="Battery Alert"
TITLE_HIGH="Charging Level"
# ---- REQUIREMENTS ----
need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; }; }
need upower
need zenity
need xdotool
need wmctrl
# ---- HELPERS ----
show_popup() {
local message="$1"
local title="$2"
# Get current mouse position (X,Y) on X11
eval "$(xdotool getmouselocation --shell)" # exports X and Y
# Launch zenity and capture its PID
zenity --info --title="$title" --text="$message" --no-wrap \
--width="$POPUP_WIDTH" --height="$POPUP_HEIGHT" &
local zpid=$!
# Give the window a moment to map, then place it centered on the cursor
sleep 0.2
local xpos=$(( X - POPUP_WIDTH / 2 ))
local ypos=$(( Y - POPUP_HEIGHT / 2 ))
wmctrl -r "$title" -e "0,${xpos},${ypos},${POPUP_WIDTH},${POPUP_HEIGHT}" || true
# Keep the service alive until the popup is dismissed (or timeout)
if (( POPUP_TIMEOUT > 0 )); then
( sleep "$POPUP_TIMEOUT"; kill -TERM "$zpid" 2>/dev/null || true ) &
fi
wait "$zpid" || true
}
read_battery() {
local bat dev
dev="$(upower -e | grep -m1 BAT || true)"
[[ -n "$dev" ]] || { echo "No battery device found via upower." >&2; exit 0; }
bat="$(upower -i "$dev")"
# State (charging/discharging/fully-charged/unknown)
state="$(awk -F': *' '/state/{print $2}' <<<"$bat")"
# Percentage as integer
percent="$(awk -F': *' '/percentage/{gsub(/%/,"",$2); print int($2)}' <<<"$bat")"
echo "$state" "$percent"
}
main() {
# Quick sanity: ensure we have a display/bus (when run by systemd)
: "${DISPLAY:=}"
: "${DBUS_SESSION_BUS_ADDRESS:=}"
if [[ -z "${DISPLAY}" || -z "${DBUS_SESSION_BUS_ADDRESS}" ]]; then
echo "Missing DISPLAY or DBUS_SESSION_BUS_ADDRESS; skipping popup." >&2
exit 0
fi
read -r STATE PERCENT < <(read_battery)
if [[ "$STATE" == "discharging" && $PERCENT -le $LOW_BATTERY ]]; then
show_popup "⚠️ Battery at ${PERCENT}% (discharging)" "$TITLE_LOW"
fi
if [[ "$STATE" == "charging" && $PERCENT -ge $CHARGED_LEVEL ]]; then
show_popup "🔋 Battery reached ${PERCENT}% (charging)" "$TITLE_HIGH"
fi
}
main "$@"
[Unit]
Description=Run battery alert periodically
[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
Unit=battery-alert.service
[Install]
WantedBy=timers.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment