Skip to content
jtwolfePublic

About

Household mesh for Omarchy — pair kids' systems over Iroh, track screen time, and push updates from the top bar

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Hearth

Household mesh for Omarchy.

A top-bar widget (like Network and Display) plus a Rust daemon that pairs kids' systems over Iroh — no cloud account, no box you have to run.

Pair at home once. The mesh follows them to school.

What it does

Role Capabilities
Parent See kids online/home/school, focused app, minutes used, grant +30m, lock/unlock, push Omarchy updates, short messages, per-system schedule, override codes
Child Remaining time, bedtime, ask for more minutes, redeem an override, messages

Presence is the Hyprland window class and title — never a screenshot or a keylog.

Hearth is a household norm, not MDM. hearthd is a user systemd unit with no sudo. A determined kid who stops the daemon, switches TTY, or edits household.json wins. Stopping hearthd drops the lock overlay (fail-open).

Architecture

┌─────────────────────┐     Unix socket      ┌──────────────────┐
│  Omarchy shell      │ ◄──────────────────► │  hearthd         │
│  (QML bar plugin)   │   hearth status      │  (Rust daemon)   │
│  wolfe.hearth       │                      │                  │
└─────────────────────┘                      │  • Iroh mesh     │
                                             │  • screen time   │
                                             │  • Hyprland IPC  │
                                             │  • policy/lock   │
                                             └────────┬─────────┘
                                                      │ Iroh QUIC
                                                      │ (hole-punch / relay)
                                             ┌────────▼─────────┐
                                             │  kids' hearthd   │
                                             └──────────────────┘

Omarchy plugins are git clones of QML + manifest.json. The plugin manager never runs install hooks and never asks for sudo. So Hearth is two pieces, same as Tailscale:

  • wolfe.hearth plugin — bar icon + dropdown. Files live at the repo root (manifest.json, HearthBar.qml, Panel.qml, …).
  • hearth / hearthd binaries — shipped as GitHub Release assets (not cargo on the machine). Installed by ./install.sh.

Plugin id is wolfe.hearth (omarchy.* is reserved for first-party). A PR upstream would rename it to omarchy.hearth.

omarchy plugin add only clones this repo. It does not run install.sh — that is Omarchy's contract. Use ./install.sh on each system so the daemon, user unit, and bar widget all land together.

Install (one script)

Needs Omarchy. The repo is public; curl fetches the release tarball. GitHub CLI is only a fallback.

# once per system
git clone https://github.com/jtwolfe/hearth.git ~/src/hearth
cd ~/src/hearth

# parent
./install.sh

# each child
./install.sh --child

If this directory already exists, git pull then run ./install.sh again.

That will:

  1. Download hearth + hearthd from the latest GitHub Release for your CPU (x86_64 or aarch64)
  2. Put them in ~/.local/bin
  3. Enable hearthd.service as a user unit (systemctl --user enable --now hearthd.service — not a system unit; WantedBy=default.target)
  4. Copy this checkout's QML + manifest.json into ~/.config/omarchy/plugins/wolfe.hearth/ and pin the house icon on the right of the bar

omarchy plugin add https://github.com/jtwolfe/hearth.git --enable only clones the bar files. It does not install hearthd. Prefer ./install.sh so the daemon, user unit, and widget land together. If you already used plugin add, still run ./install.sh (or ./install.sh --skip-plugin after the widget is in place).

Do not cargo build inside ~/.config/omarchy/plugins/wolfe.hearth/ — Omarchy hot-reloads that folder on every file change.

The user unit puts /usr/share/omarchy/bin on PATH so omarchy-update-available, omarchy-update, omarchy-shell, and omarchy-notification-send resolve. install.sh does not change UFW.

Configure / remove the widget with the usual Omarchy commands:

omarchy bar move wolfe.hearth --section right
omarchy plugin remove wolfe.hearth

./install.sh --uninstall also removes the user unit and binaries.

Check it:

systemctl --user status hearthd.service
hearth status

The house mark should appear on the right of the top bar (next to the tray). Click it. Parent: Pair a system. Child: Join household.

From source (only if you want to)

./install.sh --from-source

Needs a recent Rust toolchain. Prefer the release binaries; that is the supported path. --from-source still installs the plugin from this checkout.

Pairing (words + session code)

You do not open firewall ports. Iroh hole-punches and falls back to a relay.

  1. Parent opens Hearth → Pair a system. The panel shows a 24-word grid (the parent’s Iroh id) and a short session code. Read those across the desk — no shared clipboard.
  2. Child: Join household, type the words and the session code.
  3. Both sides confirm (Trust this system). Device ids stay in ~/.local/share/hearth/.

More details on either side still has the QR and hearth1:… ticket if you can copy-paste. CLI hearth pair create / hearth pair join --words '…' --code XXX-XXX matches the bar.

The six-character code by itself is only a LAN multicast shortcut. Stock Omarchy UFW denies incoming, so that code alone usually fails. install.sh does not touch UFW.

After that, reconnect is endpoint-id → hole punch. School NATs often need a relay for the first packets; traffic upgrades to direct when it can.

Lock overlay

Parental lock is a Hearth overlay, not omarchy.lock / the child's login password.

  • One overlay per bar instance, on that screen only (Omarchy instantiates the widget once per monitor).
  • Card uses the same popup tokens as the bar menu (Color.popups, Border.surfaceSpec).
  • Visibility is polled status.self.locked and only while the child is paired.
  • If hearthd dies, the next failed status poll drops the overlay (fail-open).
  • Exclusive layer-shell does not eat compositor binds: Super+Ctrl+L, Super+Return, and the Omarchy menu still fire (bind passthrough). The overlay cannot be dismissed with the child's login password.

Override codes

Parent mints a one-time code (shown on the parent panel). The hash is pre-synced to the child so it works with the parent offline. Redeem is a timed bypass (override_until) — it does not clear parent lock. Copy: bypasses lock for 30 minutes. Default TTL 24h. When the timer ends, parent lock and the schedule re-apply. Grant is extra cap, not unlock.

Schedule

Each child system has a daily plan: allowed windows, seven weekdays, bedtime/wake that wrap midnight, and a daily cap. The child enforces the last policy locally (the parent can be at school NAT or asleep). Parent edits from the bar (per-day chips, Weekdays / Weekend presets, copy-to-all-days).

Messaging

Short 1:1 notes parent ↔ each child over the mesh (280 characters, last 50 kept). Shown as omarchy-notification-send plus the panel. Not a chat app: no cloud, no groups, no media.

Omarchy updates

The parent Update button (hearth update push <id>) sends Wire::UpdatePush to that child. The child runs omarchy-update -y and replies UpdateResult. The parent does not run omarchy-update -y itself. Checks use omarchy-update-available every 30 minutes (never omarchy-update --check). Push is best-effort; polkit may prompt on the child.

CLI

hearth status --json          # full snapshot for the bar
hearth --json rpc             # one JSON object on stdin (bar actions; secrets stay off argv)
hearth pair create            # parent: prints code + ticket + ASCII QR
hearth pair join K7M-P2Q      # child: join with code (LAN) or hearth1:… ticket
hearth pair confirm           # mutual Trust this system
hearth grant <id> --minutes 30
hearth lock <id>
hearth unlock <id>
hearth update push <id>       # child applies omarchy-update -y
hearth ask --minutes 30       # child: request more time
hearth leave                  # child: ask the parent to allow leave (does not leave yet)
hearth forget <id>            # parent: remove a system (they are released)
hearth policy set <id> --limit 120 --bedtime 20:30

Layout

Quattro bar-widget shape: the manifest loads HearthBar.qml (a qs.Ui.BarWidget); that file loads Panel.qml. One kind (bar-widget). Nested Service.qml and LockOverlay.qml are not extra plugin kinds.

The entry file is not named BarWidget.qml because a third-party plugin directory is an implicit QML import, and that filename collides with qs.Ui.BarWidget (File name case mismatch).

manifest.json              Omarchy plugin contract (must stay at repo root)
HearthBar.qml              Bar slot, icon, IPC, lock overlay host
Panel.qml                  Dropdown (pairing, schedule, messages)
HearthIcon.qml             House mark with ember badge
LockOverlay.qml            Per-monitor parental lock surface
Model.js                   Status parsing helpers
Service.qml                Polls `hearth status --json`; actions via `hearth rpc` on stdin
preview.png                Optional marketplace card
install.sh                 Downloads release bins + copies plugin + enables systemd user unit
daemon/                    Rust sources for hearthd + hearth
systemd/hearthd.service    Template (install.sh rewrites ExecStart)
.github/workflows/         CI + optional tagged builds of prebuilt bins

Releases

./install.sh (without --from-source) downloads the latest GitHub Release tarball for your CPU:

  • hearth-<ver>-x86_64-unknown-linux-gnu.tar.gz
  • hearth-<ver>-aarch64-unknown-linux-gnu.tar.gz

Each tarball contains hearth, hearthd, and hearthd.service.

.github/workflows/release.yml can build those on a v* tag. If GitHub Actions is unavailable, pack locally and attach with gh release create:

(cd daemon && cargo build --release --locked --bins)
stage=$(mktemp -d)
install -m 0755 daemon/target/release/hearth daemon/target/release/hearthd "$stage/"
install -m 0644 systemd/hearthd.service "$stage/"
ver=0.2.9
tar -C "$stage" -czf hearth-${ver}-x86_64-unknown-linux-gnu.tar.gz hearth hearthd hearthd.service
gh release create v${ver} ./hearth-${ver}-x86_64-unknown-linux-gnu.tar.gz

Pin a release with ./install.sh --version v0.2.9.

Upgrade: git pull && ./install.sh (restarts the user unit, recopies QML). Schema 2 daemons keep a one-time household.json.bak-v1 before the first schema-2 save; a rolled-back v1 daemon does not understand schema 2, so keep that backup if you might downgrade.

Uninstall

Same script, no sudo, no install hook:

./install.sh --uninstall            # parent: keeps pairing under ~/.local/share/hearth
./install.sh --uninstall --child    # child: tells the parent, then wipes household.json

On a child, --uninstall also detects role: child in household.json if you omit --child. That asks the parent to allow leave (if hearthd is up), then deletes household.json. The parent still lists the system until they Allow that request or Remove it. endpoint.key stays so a later join is the same Iroh identity.

A child cannot drop out on their own from the bar. Ask to leave notifies the parent; Allow releases them. Remove on the parent is the same release without a request.

Omarchy may leave plugin backups at ~/.config/omarchy/plugins/.wolfe.hearth.bak.*. The source checkout is separate.

Incorporating into Omarchy

Same contract as the Quattro bar-widget tutorial / omarchy.clock:

  • kinds: ["bar-widget"] only
  • entryPoints.barWidget: "HearthBar.qml" (a BarWidget that loads Panel.qml internally)
  • BarIconButton + KeyboardPanel + IpcHandler target wolfe.hearth
  • hostWidget / switchPanel / closeForPopoutSwitch forwarded from the slot widget
  • Process { command: ["hearth", "status", "--json"] } for polls; writes go through hearth --json rpc on stdin
  • no install hook, no sudo, no reserved omarchy.* id

A first-party port would move the QML into shell/plugins/, rename the id to omarchy.hearth, and package hearthd like other Omarchy user services.

Privacy and security

Hearth is local household software. It does not open a cloud account, take screenshots, or keylog.

What is stored (mode 0600, under ~/.local/share/hearth/)

  • endpoint.key — this system's Iroh identity
  • household.json — pairing, schedules, override hashes, last-50 message threads, window class/title presence
  • Pairing QR matrices and the 24-word list are not written to disk; they are derived for hearth status only

What crosses the mesh (Iroh QUIC, hole-punch or n0 relay)

  • Device id, hostname, username label (user@host), online/home/school
  • Focused window class and title (never pixels)
  • Minutes used, lock/schedule/override hashes, short messages (≤280 characters)

n0 relays, when used, only see encrypted QUIC. They are not a household database.

What stays off logs and process lists

  • The bar sends pairing words, tickets, override codes, and message bodies to hearth --json rpc on stdin, not argv
  • hearthd logs endpoint ids, grant minutes, and override hash prefixes — not message text, not override plaintext, not tickets
  • wl-copy --sensitive is used when copying a ticket or override code
  • hearthd.sock is 0600 in XDG_RUNTIME_DIR (already user-only)

Threat model (unchanged)

Hearth is a household norm, not MDM. A determined person on the child system who stops hearthd, switches TTY, or edits household.json wins. Stopping the daemon drops the lock overlay (fail-open). CLI hearth pair join --words '…' still puts words on argv; use the bar join form when you care.

Desktop notifications show message text on the receiving machine by design. Do not put secrets in Hearth messages.

License

MIT — see LICENSE.

About

Household mesh for Omarchy — pair kids' systems over Iroh, track screen time, and push updates from the top bar

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages