commit 016d335484f2e3f48ed3874ebd152be4263b6f07 Author: Gavin Nugent Date: Sun Apr 26 09:18:28 2026 +0100 Initial commit: battery tooltip installer + docs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51ab1bc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.bak.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5acd172 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e08723 --- /dev/null +++ b/README.md @@ -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. +~/.config/waybar/style.css.bak. +``` + +## 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 ``, 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. diff --git a/install-battery-tooltip.NOTES.md b/install-battery-tooltip.NOTES.md new file mode 100755 index 0000000..494c302 --- /dev/null +++ b/install-battery-tooltip.NOTES.md @@ -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 `` 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:** + - `...` around the dynamic fields so + digit-count transitions (5W → 10W, etc.) don't rewidth the tooltip. + `` 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.`) 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. # timestamped backup per run +├── style.css # walker.css import + marker block +└── style.css.bak. # timestamped backup per run +``` diff --git a/install-battery-tooltip.sh b/install-battery-tooltip.sh new file mode 100755 index 0000000..e2019d9 --- /dev/null +++ b/install-battery-tooltip.sh @@ -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": "󰁿 Battery{{capacity:>3}}% · {{time}} left · ↓{{power:>4.1f}}W/{wh}Wh"'), + ('tooltip-format-charging', + f'"tooltip-format-charging": "󰂄 Battery{{capacity:>3}}% · {{time}} to full · ↑{{power:>4.1f}}W/{wh}Wh"'), + ('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