Initial commit: battery tooltip installer + docs

This commit is contained in:
Gavin Nugent 2026-04-26 09:18:28 +01:00
commit 016d335484
5 changed files with 475 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.bak.*

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Gavin Nugent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

92
README.md Normal file
View file

@ -0,0 +1,92 @@
# omarchy-battery-tooltip
Replace Omarchy's minimal waybar battery tooltip with a richer one:
```
󰁿 Battery 69% · 03h:49m left · ↓10.3W/57Wh
```
…wrapped in an accent-coloured border that follows the active Omarchy theme.
The default tooltip on a stock Omarchy install shows just `5W↓ 69%`. This installer adds time remaining, power direction, formatted power draw, and your battery's capacity in Wh — auto-detected from `/sys/class/power_supply/`.
## Requirements
- [Omarchy](https://omarchy.org) (waybar set up at `~/.config/waybar/`)
- A laptop. The script auto-detects via `hostnamectl` / SMBIOS chassis type and exits cleanly on desktops, VMs, and servers (use `--force` to install anyway).
- `bash`, `awk`, `find`, `python3` — all in Omarchy's base install.
## Install
One-liner:
```bash
curl -fsSL https://git.no-signal.uk/nosignal/omarchy-battery-tooltip/raw/branch/main/install-battery-tooltip.sh | bash
```
GitHub mirror:
```bash
curl -fsSL https://raw.githubusercontent.com/28allday/omarchy-battery-tooltip/main/install-battery-tooltip.sh | bash
```
Or clone and run:
```bash
git clone https://git.no-signal.uk/nosignal/omarchy-battery-tooltip.git
cd omarchy-battery-tooltip
./install-battery-tooltip.sh
```
Hover the battery icon in waybar to see the new tooltip.
## Options
| Flag | Effect |
| ------------ | -------------------------------------------------------------------------------------------- |
| *(none)* | Use the battery's **design capacity** (rated spec, never changes). |
| `--current` | Use the **current full-charge capacity** instead — reflects degradation. Re-run to refresh. |
| `--force` | Install even if the script doesn't think this is a laptop. |
| `--help` | Print the script's header. |
## Safe to re-run
The script is idempotent:
- `config.jsonc` — finds the existing `"battery"` block and replaces the tooltip keys in place (or inserts them if missing). Stock or already-patched configs both work.
- `style.css` — uses a `/* >>> battery-tooltip */` marker block, so re-runs replace what's between the markers rather than accumulating.
A timestamped backup of each file is written next to it on every run:
```
~/.config/waybar/config.jsonc.bak.<unix-ts>
~/.config/waybar/style.css.bak.<unix-ts>
```
## Theme follows automatically
- Border colour pulls `@selected-text` from the active theme's `walker.css`. Switching themes with `omarchy-theme-set` updates the border on the next waybar reload.
- Font follows because the tooltip wraps dynamic fields in `<span face='monospace'>`, which picks up whatever font `omarchy-font-set` configured.
## Uninstall / revert
Restore the most recent backups:
```bash
cd ~/.config/waybar
ls -t config.jsonc.bak.* | head -1 | xargs -I{} cp {} config.jsonc
ls -t style.css.bak.* | head -1 | xargs -I{} cp {} style.css
omarchy-restart-waybar
```
## Design notes
See [`install-battery-tooltip.NOTES.md`](./install-battery-tooltip.NOTES.md) for the why-it-works-this-way: laptop detection layering, idempotency strategy, capacity-mode trade-off, the GTK-tooltip monospace-wrapper workaround, and known edge cases.
## Tested on
Acer Nitro laptop with Realtek BAT1 (Li-ion, ~57Wh design, charge-based sysfs), fresh stock Omarchy configs and already-patched configs, plus simulated desktop / unknown-flag / missing-battery paths.
## License
MIT.

124
install-battery-tooltip.NOTES.md Executable file
View file

@ -0,0 +1,124 @@
# install-battery-tooltip.sh — design notes
Notes to travel with the script when copying it to another machine for
the omarchy PR.
## What it does
Enriches Omarchy's stock waybar battery tooltip from a minimal
`5W↓ 69%` into a full readout:
󰁿 Battery 69% · 03h:49m left · ↓10.3W/57Wh
Wrapped in an accent-colored border that matches the active theme.
Two files are patched:
| File | What changes |
| --------------------------------- | --------------------------------------------------- |
| `~/.config/waybar/config.jsonc` | 5 keys in the `"battery"` block |
| `~/.config/waybar/style.css` | `@import` of walker.css + tooltip rules (markered) |
## Design decisions
1. **Laptop detection (layered, skip cleanly on desktops):**
- `hostnamectl chassis` (primary — systemd's DMI + heuristics)
- `/sys/class/dmi/id/chassis_type` (SMBIOS fallback)
- Battery presence (last resort)
- `--force` to override
2. **Idempotent by construction:**
- `config.jsonc`: Python regex + brace-depth parser finds the
`"battery"` block, replaces known keys in-place, inserts missing
ones. Safe on stock omarchy AND on already-patched systems.
- `style.css`: `/* >>> battery-tooltip */` marker block — any re-run
replaces between markers instead of accumulating. The walker.css
`@import` is only added if not already present.
3. **Capacity modes:**
- Default `design` — rated spec from `energy_full_design`
(or `charge_full_design × voltage_min_design` if only mAh files exist).
Never changes, never needs refreshing.
- `--current` — uses `energy_full` / `charge_full`, reflects battery
degradation. Drifts over time; user re-runs when they want a
refreshed number.
4. **Theme following:**
- Border color uses `@selected-text` from the theme's walker.css.
`omarchy-theme-set` swaps the `~/.config/omarchy/current/theme`
symlink → border updates automatically on next waybar reload.
- Font follows because `omarchy-font-set` rewrites both waybar's
`style.css` font-family AND fontconfig's `monospace` alias, which
our `<span face='monospace'>` wrapper picks up.
5. **Zero-padded time (`03h:49m` not ` 3h 49m`):**
- `format-time: "{H:02}h:{M:02}m"` keeps the tooltip a constant width
with no leading-space that would compound with template spacing.
6. **Monospace wrapper forces stable width:**
- `<span face='monospace'>...</span>` around the dynamic fields so
digit-count transitions (5W → 10W, etc.) don't rewidth the tooltip.
`<tt>` was unreliable in GTK tooltips; `face='monospace'` is honored
consistently.
## Output matrix
| Scenario | Result | Exit |
| ------------------------------ | ------------------------- | ---- |
| Laptop | Installs | 0 |
| Desktop / VM / server | Skips with info message | 0 |
| Desktop with `--force` | Installs anyway | 0 |
| `--current` | Uses degraded capacity | 0 |
| `--help` | Prints header comment | 0 |
| Missing waybar / no python3 | Errors with hint | 1 |
| Laptop but unreadable sysfs | Errors with path | 1 |
| Laptop but no `"battery"` block| Errors with hint | 1 |
## Dependencies
- `bash`, `awk`, `find`, `python3` — all in omarchy's base install.
- `omarchy-restart-waybar` — optional; if present, waybar is restarted
automatically. If absent, the installer just reports completion.
## Tested on
- Acer laptop, BAT1 (charge-based sysfs, Li-ion, ~57Wh design, 18 cycles).
- Fresh stock omarchy configs (both config.jsonc and style.css).
- Re-runs (3× identical checksums → idempotent).
- Simulated desktop via `hostnamectl` override.
- Unknown-flag and missing-battery paths.
## PR suggestions for the omarchy distro
- If adding to the install flow, call the installer non-interactively.
It handles "not a laptop" internally as a zero-exit skip, so no guards
needed at the caller.
- If omarchy adds a global install-time hook mechanism, this could live
there. Otherwise place it in the laptop-specific install path.
- Backup files (`*.bak.<timestamp>`) accumulate on each re-run.
Consider adding the installer to a path where backups are either
swept, or add a `--no-backup` flag if the install flow doesn't want
them.
## Caveats / known edge cases
- Capacity padding `{capacity:>3}` means `Battery 5%` at single-digit
%, `Battery100%` at 100%. Width stays constant; visible gap varies.
Zero-padding (`005%`) was rejected as uglier.
- Similarly `↓{power:>4.1f}` shows `↓ 5.2W` vs `↓10.3W`.
- Full/plugged tooltips don't have `{time}` available so they use a
simpler format without the monospace span.
- `{icon}` is NOT supported in `tooltip-format-*` for the battery
module (waybar errors "argument not found"). We use static state
glyphs instead — still theme-consistent since all omarchy-managed
fonts are Nerd Fonts.
## File layout after install
```
~/.config/waybar/
├── config.jsonc # battery block patched
├── config.jsonc.bak.<ts> # timestamped backup per run
├── style.css # walker.css import + marker block
└── style.css.bak.<ts> # timestamped backup per run
```

237
install-battery-tooltip.sh Executable file
View file

@ -0,0 +1,237 @@
#!/usr/bin/env bash
# install-battery-tooltip.sh
#
# Extends Omarchy's waybar battery tooltip with a full readout:
# icon, percentage, time remaining, power draw, and auto-detected
# battery capacity in Wh. Also adds an accent-colored border and
# padding to the tooltip box so it matches the active theme.
#
# Idempotent — safe to re-run any time (e.g. to refresh the Wh
# number as the battery degrades).
#
# Usage:
# ./install-battery-tooltip.sh # uses design capacity
# ./install-battery-tooltip.sh --current # uses current full-charge capacity
set -euo pipefail
CONFIG="${HOME}/.config/waybar/config.jsonc"
STYLE="${HOME}/.config/waybar/style.css"
die() { printf '✗ %s\n' "$*" >&2; exit 1; }
# --- Laptop detection (skip cleanly on desktops/servers) ---
#
# Strategy: try the most reliable source first, fall back through less reliable
# signals. Any positive match → laptop. All negatives / no data → skip.
# Override with --force for testing or unusual hardware.
is_laptop() {
# 1. systemd's hostnamectl — merges DMI with heuristics, most reliable.
if command -v hostnamectl >/dev/null 2>&1; then
case "$(hostnamectl chassis 2>/dev/null)" in
laptop|portable|convertible|tablet|handset) return 0 ;;
desktop|server|embedded|vm|container) return 1 ;;
# empty / unknown → fall through to next method
esac
fi
# 2. Raw SMBIOS chassis_type. Mobile form factors:
# 8=Portable 9=Laptop 10=Notebook 11=Handheld 14=Sub-Notebook
# 30=Tablet 31=Convertible 32=Detachable
if [[ -r /sys/class/dmi/id/chassis_type ]]; then
case "$(cat /sys/class/dmi/id/chassis_type)" in
8|9|10|11|14|30|31|32) return 0 ;;
3|4|5|6|7|15|16|17|23|24|25|28) return 1 ;;
esac
fi
# 3. Last resort: presence of a battery.
compgen -G '/sys/class/power_supply/BAT*' >/dev/null && return 0
return 1
}
# --- Argument parsing ---
FORCE=false
MODE=design
for arg in "$@"; do
case "$arg" in
--force) FORCE=true ;;
--current|current) MODE=current ;;
design) MODE=design ;;
-h|--help)
sed -n '2,10p' "$0" | sed 's/^# \?//'
exit 0 ;;
*) die "Unknown argument: $arg (use: --current, --force)" ;;
esac
done
if ! $FORCE && ! is_laptop; then
echo " Not a laptop (per hostnamectl/DMI) — skipping battery tooltip install."
echo " Use --force to install anyway."
exit 0
fi
# --- Pre-flight ---
[[ -f "$CONFIG" ]] || die "No $CONFIG — Omarchy/Waybar not set up?"
[[ -f "$STYLE" ]] || die "No $STYLE — Omarchy/Waybar not set up?"
command -v python3 >/dev/null || die "python3 required."
# --- Detect battery capacity (Wh) ---
BAT=$(find /sys/class/power_supply -maxdepth 1 -name 'BAT*' -printf '%f\n' 2>/dev/null | sort | head -n1)
[[ -n "$BAT" ]] || die "No battery found under /sys/class/power_supply/."
P="/sys/class/power_supply/$BAT"
read_sysfs() { [[ -r "$1" ]] || die "Can't read $1"; cat "$1"; }
# Design mode (default) = rated spec, never changes.
# Current mode = current full-charge capacity, reflects degradation.
if [[ "$MODE" == "design" ]]; then
if [[ -r "$P/energy_full_design" ]]; then
WH=$(awk -v e="$(read_sysfs "$P/energy_full_design")" 'BEGIN { printf "%.0f", e/1000000 }')
else
charge=$(read_sysfs "$P/charge_full_design")
voltage=$(cat "$P/voltage_min_design" 2>/dev/null || read_sysfs "$P/voltage_now")
WH=$(awk -v c="$charge" -v v="$voltage" 'BEGIN { printf "%.0f", (c*v)/1e12 }')
fi
else
if [[ -r "$P/energy_full" ]]; then
WH=$(awk -v e="$(read_sysfs "$P/energy_full")" 'BEGIN { printf "%.0f", e/1000000 }')
else
charge=$(read_sysfs "$P/charge_full")
voltage=$(cat "$P/voltage_min_design" 2>/dev/null || read_sysfs "$P/voltage_now")
WH=$(awk -v c="$charge" -v v="$voltage" 'BEGIN { printf "%.0f", (c*v)/1e12 }')
fi
fi
[[ "$WH" =~ ^[0-9]+$ && "$WH" -gt 0 ]] || die "Couldn't compute a sensible Wh value (got '$WH')."
echo "$BAT: ${WH}Wh (${MODE} capacity)"
# --- Patch config.jsonc and style.css ---
TS=$(date +%s)
cp "$CONFIG" "${CONFIG}.bak.${TS}"
cp "$STYLE" "${STYLE}.bak.${TS}"
python3 - "$CONFIG" "$STYLE" "$WH" << 'PY'
import re, sys
from pathlib import Path
config_path = Path(sys.argv[1])
style_path = Path(sys.argv[2])
wh = int(sys.argv[3])
# =========================================================================
# config.jsonc — battery module tooltip keys
# =========================================================================
src = config_path.read_text()
m = re.search(r'"battery"\s*:\s*\{', src)
if not m:
sys.exit('✗ No battery block in config.jsonc')
start = m.end()
depth, j = 1, start
while j < len(src) and depth > 0:
if src[j] == '{': depth += 1
elif src[j] == '}': depth -= 1
j += 1
end = j - 1
block = src[start:end]
indent_m = re.search(r'\n([ \t]+)"', block)
indent = indent_m.group(1) if indent_m else ' '
desired = [
('format-time',
'"format-time": "{H:02}h:{M:02}m"'),
('tooltip-format-discharging',
f'"tooltip-format-discharging": "<span face=\'monospace\'>󰁿 Battery{{capacity:>3}}% · {{time}} left · ↓{{power:>4.1f}}W/{wh}Wh</span>"'),
('tooltip-format-charging',
f'"tooltip-format-charging": "<span face=\'monospace\'>󰂄 Battery{{capacity:>3}}% · {{time}} to full · ↑{{power:>4.1f}}W/{wh}Wh</span>"'),
('tooltip-format-full',
f'"tooltip-format-full": "󰂅 Battery {{capacity}}% · Full · {wh}Wh"'),
('tooltip-format-plugged',
f'"tooltip-format-plugged": "󰚥 Battery {{capacity}}% · Plugged in · {wh}Wh"'),
]
for key, new_line in desired:
key_re = re.compile(rf'^([ \t]*)"{re.escape(key)}"\s*:\s*"[^"]*"(,?)\s*$',
re.MULTILINE)
if key_re.search(block):
block = key_re.sub(lambda m: f'{m.group(1)}{new_line}{m.group(2) or ","}', block, count=1)
else:
trailing_m = re.search(r'\s*$', block)
ip = trailing_m.start() if trailing_m else len(block)
block = (block[:ip].rstrip() + '\n' + indent + new_line + ',' + block[ip:])
config_path.write_text(src[:start] + block + src[end:])
print(f' · config.jsonc: patched {len(desired)} battery keys')
# =========================================================================
# style.css — walker.css import + tooltip rules in a marker block
# =========================================================================
style = style_path.read_text()
# 1. Ensure walker.css is imported (for @selected-text)
if 'walker.css' not in style:
new_style, n = re.subn(
r'(@import\s+"[^"]*/waybar\.css";)(\s*\n)',
r'\1\2@import "../omarchy/current/theme/walker.css";\n',
style,
count=1,
)
if n == 0:
# No existing waybar.css import — prepend both.
new_style = ('@import "../omarchy/current/theme/waybar.css";\n'
'@import "../omarchy/current/theme/walker.css";\n\n' + style)
style = new_style
# 2. Tooltip rules inside a marker block (idempotent replace)
MARKER_START = '/* >>> battery-tooltip */'
MARKER_END = '/* <<< battery-tooltip */'
tooltip_block = (
f'{MARKER_START}\n'
'tooltip,\n'
'tooltip box,\n'
'tooltip label,\n'
'tooltip * {\n'
' margin: 0;\n'
' padding: 0;\n'
'}\n\n'
'tooltip {\n'
' padding: 7px;\n'
' border: 2px solid @selected-text;\n'
'}\n'
f'{MARKER_END}\n'
)
block_re = re.compile(re.escape(MARKER_START) + r'.*?' + re.escape(MARKER_END) + r'\n?', re.DOTALL)
if block_re.search(style):
style = block_re.sub(tooltip_block, style)
else:
style = style.rstrip() + '\n\n' + tooltip_block
style_path.write_text(style)
print(' · style.css: walker.css import + tooltip rules in place')
PY
# --- Reload ---
if command -v omarchy-restart-waybar >/dev/null; then
if omarchy-restart-waybar >/dev/null 2>&1; then
echo "→ Waybar restarted."
else
echo "⚠ omarchy-restart-waybar failed — reload manually."
fi
fi
cat << EOF
✓ Done. Hover the battery icon to see:
󰁿 Battery XX% · HHh:MMm left · ↓X.XW/${WH}Wh
Inside an accent-colored border matching the active theme.
Backups (this run): ${CONFIG}.bak.${TS}
${STYLE}.bak.${TS}
Re-run with --current to use the degraded (current full-charge) capacity
instead of design capacity.
EOF