Desktop and mobile UI for the pim proximity mesh daemon. A Tauri 2 application — the same React codebase targets macOS, Windows, Linux, iOS, and Android.
┌────────────────────────────────────────────┐
│ █ pim · status --verbose [OK] │
├────────────────────────────────────────────┤
│ node client-a │
│ interface pim0 ◆ up │
│ peers 3 connected │
│ gateway 10.77.0.1 via tcp ◆ active│
│ relay-b 10.77.0.22 via tcp ◆ active│
│ forwarded 4.2 MB · 3,847 packets │
│ uptime 4h 22m │
└────────────────────────────────────────────┘
This repo contains the UI only. The daemon, CLI, and protocol spec live in Astervia/proximity-internet-mesh.
On desktop, pim-ui spawns pim-daemon as a sidecar child process.
On mobile, pim-ui connects to a remote daemon over TCP (embedded daemon is
planned but requires platform-native VPN plugins — see ROADMAP.md).
| Shell | Tauri 2 |
| Frontend | React 19 · Vite 6 · TypeScript |
| Styling | Tailwind v4 · shadcn/ui (new-york) |
| Type | Geist Mono · Geist · JetBrains Mono |
| Icons | Lucide · Unicode-first |
The brand spec is authored in the kernel repo at
.design/branding/pim/patterns/ (pim.yml source of truth, STYLE.md the
agent contract, guidelines.html the visual reference). src/globals.css
mirrors those tokens — run pnpm sync-brand when the brand evolves.
No separate kernel install is required. The desktop bundle ships with a matching
pim-daemonbaked in as a Tauri sidecar — installing pim-ui is the only step. (scripts/fetch-daemon.shpulls the binary from Astervia/proximity-internet-mesh's release matchingPIM_DAEMON_VERSIONat build time, and Tauri embeds it viabundle.externalBin.)
Published releases include native installer bundles named
pim-ui-<tag>-<label>.<ext> plus a matching .sha256 next to each:
| Platform | Label | Bundles |
|---|---|---|
| Linux x86_64 | linux-x86_64 |
.deb, .AppImage, .rpm |
| macOS Intel | macos-x86_64 |
.dmg |
| macOS Apple Silicon | macos-aarch64 |
.dmg |
| Windows x86_64 | windows-x86_64 |
.msi, .exe |
| Android arm64-v8a | android-aarch64 |
.apk |
| Android armeabi-v7a | android-armv7 |
.apk |
Pick the bundle that matches your host:
VERSION="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
https://github.com/Astervia/pim-ui/releases/latest \
| sed 's:.*/::')"
if [ -z "${VERSION}" ]; then
echo "Failed to determine the latest GitHub release version" >&2
exit 1
fi
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) LABEL="linux-x86_64" ; EXT="deb" ;; # rpm-based distros: set EXT=rpm
Darwin-x86_64) LABEL="macos-x86_64" ; EXT="dmg" ;;
Darwin-arm64) LABEL="macos-aarch64" ; EXT="dmg" ;;
*)
echo "No published release artifact for $(uname -s)-$(uname -m)" >&2
exit 1
;;
esac
ASSET="pim-ui-${VERSION}-${LABEL}.${EXT}"
BASE="https://github.com/Astervia/pim-ui/releases/download/${VERSION}"
curl -LO "${BASE}/${ASSET}"
curl -LO "${BASE}/${ASSET}.sha256"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c "${ASSET}.sha256"
else
shasum -a 256 -c "${ASSET}.sha256"
fiThen install the bundle the platform-native way. Use a dependency-aware
front-end (apt, dnf, zypper) rather than the raw dpkg/rpm
commands — the .deb/.rpm declare GTK runtime deps
(libayatana-appindicator3-1, libwebkit2gtk-4.1-0, libgtk-3-0 on
Debian/Ubuntu) that need to be pulled in.
- Debian / Ubuntu (
.deb):sudo apt install ./pim-ui-${VERSION}-linux-x86_64.deb - Fedora / RHEL (
.rpm):sudo dnf install ./pim-ui-${VERSION}-linux-x86_64.rpm - openSUSE (
.rpm):sudo zypper install ./pim-ui-${VERSION}-linux-x86_64.rpm - macOS (
.dmg): open the.dmgand dragpim.appinto/Applications - Windows (
.msi): double-click to launch the installer - Windows (
.exe): NSIS setup — double-click to install
Linux
.AppImageis not recommended. The AppImage bundler currently mangles the bundledpim-daemon(same ELFBuildIDbut different SHA256 than the.deb/.rpmpayload), causing the daemon toSIGSEGVbeforemain()and the UI to surfacepim-daemon exited in ~2000 ms during startup. Use.debor.rpmuntil the AppImage build is fixed.
The .deb / .rpm declare the GTK webview deps and the package manager will pull them in. A few runtime requirements are not declared and need to already be present:
- polkit + a polkit auth agent — required so the UI can
pkexecthe daemon (it needs root for TUN, NAT, etc.). Full KDE / GNOME / XFCE / MATE / LXQt desktops ship one. On minimal i3 / sway / headless setups install one explicitly (e.g.polkit-gnome,polkit-kde-agent-1,lxqt-policykit); without it the password dialog never appears, the daemon never starts, and the UI surfaces the samepim-daemon exited in ~2000 mserror. iproute2(ip) andiptables— used by the daemon to bring up the TUN interface and set up forwarding. Installed by default on most desktop distros.- Bluetooth path only — if
[bluetooth]or[bluetooth_rfcomm]is enabled in~/.config/pim/pim.toml, the daemon shells out tobluetoothctl,bt-network,dnsmasq,dhclient. Installbluez,bluez-tools,dnsmasq, andisc-dhcp-clienton Debian/Ubuntu (or the equivalents on other distros) before enabling those features. The UI starts fine without them; only the BT bridge setup logs errors. - Wi-Fi Direct path only — if
[wifi_direct]is enabled, the daemon driveswpa_supplicantdirectly. Most desktop distros include it.
- Linux (
.deb/.rpm): after installation, search forpimin your desktop app launcher and open it from there. - macOS (
.dmg): after draggingpim.appinto/Applications, open it from/Applications, Spotlight, or Launchpad.
The bundle ships with the matching pim-daemon sidecar baked in; no
separate daemon download is required for desktop use.
Mobile is preview / debug-signed. The published APKs are built with
tauri android build --apk --debugso they are auto-signed with a debug keystore and install directly after download — noapksignerdance required. They are not Play-Store-grade builds and the embedded daemon path (libpim_daemon.so) on Android is still stabilising. The phone connects to a remotepim-daemonover TCP.
Two split APKs ship per release — one per ABI. Pick aarch64 unless
your phone is genuinely 32-bit only:
| ABI | Asset | Phones |
|---|---|---|
| arm64-v8a | pim-ui-<tag>-android-aarch64.apk |
All 64-bit Android (most modern phones) |
| armeabi-v7a | pim-ui-<tag>-android-armv7.apk |
Older 32-bit phones |
If you are not sure, check on the phone with
adb shell getprop ro.product.cpu.abi (returns e.g. arm64-v8a).
- Open the latest release in the phone's browser: https://github.com/Astervia/pim-ui/releases/latest
- Tap the
pim-ui-<tag>-android-aarch64.apk(or-armv7) asset to download it. - Tap the downloaded file. The first time, Android prompts you to allow installs from your browser / file manager — grant it, then confirm the install.
- Open
pimfrom the launcher.
VERSION="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
https://github.com/Astervia/pim-ui/releases/latest \
| sed 's:.*/::')"
ABI=aarch64 # or armv7 for legacy 32-bit phones
ASSET="pim-ui-${VERSION}-android-${ABI}.apk"
BASE="https://github.com/Astervia/pim-ui/releases/download/${VERSION}"
curl -LO "${BASE}/${ASSET}"
curl -LO "${BASE}/${ASSET}.sha256"
sha256sum -c "${ASSET}.sha256"
adb install -r "${ASSET}"-r reinstalls over a previous install while preserving app data; drop
it for a first-time install.
Run this before installing or upgrading on Linux. It is idempotent —
safe to run when nothing is installed. Preserves ~/.config/pim/pim.toml
(your authored daemon config); to wipe that too, rm -rf ~/.config/pim
explicitly.
# 1. Stop the UI (the daemon is killed by the UI on close).
pkill -x pim-ui 2>/dev/null || true
sudo pkill -x pim-daemon 2>/dev/null || true
# 2. Uninstall whichever package format is installed (silences "not installed").
sudo dpkg -r pim 2>/dev/null || true
sudo rpm -e pim 2>/dev/null || true
# 3. Remove user-level launcher remnants from any earlier AppImage recipe.
rm -rf "${HOME}/.local/share/pim-ui"
rm -f "${HOME}/.local/share/applications/pim-ui.desktop"
rm -f "${HOME}/.local/share/icons/hicolor/512x512/apps/pim-ui.png"
update-desktop-database "${HOME}/.local/share/applications" 2>/dev/null || true
gtk-update-icon-cache "${HOME}/.local/share/icons/hicolor" 2>/dev/null || true
# 4. Clear stale root-owned daemon runtime state.
sudo rm -f \
"/run/user/$(id -u)/pim.sock" \
"/run/user/$(id -u)/pim.pid" \
"/run/user/$(id -u)/pim-daemon.log"If you would rather produce your own installer bundle than download a
published one, build it locally. The output is the same
.deb / .rpm / .AppImage / .dmg / .msi / .exe formats listed in
Install From GitHub Releases — install them
the same platform-native way once built.
-
git
-
Node ≥ 20
-
pnpm 10 (
npm install -g pnpm@10) -
Rust (stable) — install via rustup
-
Platform build deps — see Tauri prerequisites for your OS. On Debian / Ubuntu:
sudo apt-get install -y libwebkit2gtk-4.1-dev libxdo-dev libssl-dev \ libayatana-appindicator3-dev librsvg2-dev patchelf
-
macOS only — Xcode command-line tools (Swift toolchain) are required to build the
pim-bt-rfcomm-macBluetooth-RFCOMM bridge declared insrc-tauri/tauri.macos.conf.json. -
Android only — install JDK 17 (Temurin recommended), the Android SDK + NDK, and a second pinned Rust toolchain for the kernel cdylib (matches
src-tauri/gen/android/app/build.gradle.kts):# SDK + NDK r26b. On Linux, sdkmanager lives under # $ANDROID_HOME/cmdline-tools/latest/bin/. sdkmanager "platform-tools" "platforms;android-36" \ "build-tools;36.0.0" "ndk;26.1.10909125" # Tauri shell crate compiles with stable; kernel cdylib uses 1.94.0. rustup target add aarch64-linux-android armv7-linux-androideabi rustup toolchain install 1.94.0 --profile minimal rustup target add --toolchain 1.94.0 \ aarch64-linux-android armv7-linux-androideabi cargo install --locked cargo-ndk
Point cargo-ndk and the Tauri build at the NDK:
export ANDROID_HOME="$HOME/Android/Sdk" export NDK_HOME="$ANDROID_HOME/ndk/26.1.10909125" export ANDROID_NDK_HOME="$NDK_HOME" export ANDROID_NDK_ROOT="$NDK_HOME"
The kernel
pim-daemonis compiled in-process aslibpim_daemon.sofrom a sibling../kernelcheckout — clone Astervia/proximity-internet-mesh next topim-ui/:# Run from the parent of pim-ui/ git clone https://github.com/Astervia/proximity-internet-mesh.git kernel
git clone https://github.com/Astervia/pim-ui.git
cd pim-ui
pnpm install --frozen-lockfile
# Bake the matching pim-daemon sidecar into src-tauri/binaries/.
# Defaults to the latest proximity-internet-mesh release;
# see "Pinning the bundled daemon" below to override.
pnpm fetch-daemonOn macOS only, also build the Bluetooth-RFCOMM Swift sidecar that
tauri.macos.conf.json declares (mirrors the matching step in
.github/workflows/release.yml):
case "$(uname -m)" in
arm64) TRIPLE_IN=arm64-apple-macosx13.0 ; TRIPLE_OUT=aarch64-apple-darwin ;;
x86_64) TRIPLE_IN=x86_64-apple-macosx13.0 ; TRIPLE_OUT=x86_64-apple-darwin ;;
esac
(
cd tools/pim-bt-rfcomm-mac
swift build -c release --triple "$TRIPLE_IN"
src="$(find .build -name pim-bt-rfcomm-mac -type f -path '*/release/*' ! -path '*.dSYM*' | head -1)"
dst="../../src-tauri/binaries/pim-bt-rfcomm-mac-${TRIPLE_OUT}"
cp "$src" "$dst"
chmod +x "$dst"
codesign --force -s - \
--entitlements entitlements/pim-bt-rfcomm-mac.entitlements "$dst"
)Then produce the native installer bundles for the host:
pnpm tauri buildOutput lands under src-tauri/target/release/bundle/:
| Platform | Bundles |
|---|---|
| Linux | bundle/deb/*.deb, bundle/rpm/*.rpm, bundle/appimage/*.AppImage |
| macOS | bundle/dmg/*.dmg, bundle/macos/pim.app |
| Windows | bundle/msi/*.msi, bundle/nsis/*.exe |
For Android, build the APK directly. No pnpm fetch-daemon is
needed — the kernel daemon is linked in-process as
libpim_daemon.so, built from the sibling ../kernel checkout by
the gradle script:
pnpm install --frozen-lockfile
pnpm tauri android build --apk --debug --target aarch64 --target armv7--debug produces auto-signed (debug keystore) APKs that install
straight onto a phone. Drop --debug for an unsigned release APK
(then sign with apksigner before install). Output lands at:
src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
src-tauri/gen/android/app/build/outputs/apk/arm/debug/app-arm-debug.apk
Install on a connected phone with adb install -r <apk>.
Install the resulting bundle the same platform-native way described in Install From GitHub Releases. For example on Debian / Ubuntu:
sudo apt install ./src-tauri/target/release/bundle/deb/pim_*.debThe .AppImage warning above applies to locally-built AppImages too —
prefer .deb or .rpm on Linux until the AppImage daemon-mangling issue
is fixed. On Linux, the Runtime Requirements
(polkit auth agent, iproute2, iptables, …) apply to source-built
bundles exactly the same way they apply to released ones.
scripts/fetch-daemon.sh decides which pim-daemon ends up baked into the
bundle. Override the default-latest-release with:
pnpm fetch-daemon --version v0.1.16 # pin a kernel release tag
pnpm fetch-daemon --branch main # build pim-daemon from a kernel branch
PIM_DAEMON_VERSION=v0.1.16 pnpm fetch-daemon--branch builds the daemon from source: it reads a sibling ../kernel
checkout when present, otherwise pulls a tarball from
Astervia/proximity-internet-mesh.
Override the local checkout location with --repo-path <path> or
PIM_DAEMON_REPO_PATH=<path>.
pnpm tauri build defaults to the host triple. To target another triple,
fetch a daemon for it and pass --target:
# Apple Silicon host → also build the Intel bundle:
rustup target add x86_64-apple-darwin
pnpm fetch-daemon x86_64-apple-darwin
pnpm tauri build --target x86_64-apple-darwinFor multi-platform releases the
.github/workflows/release.yml matrix is
the easier path — it already runs each target on a matching host runner
and produces the same pim-ui-<tag>-<label>.<ext> artifact layout used in
Install From GitHub Releases.
pnpm install
pnpm tauri dev # desktop — requires Rust + platform build toolsFirst-time prerequisites:
- Node ≥ 20
- pnpm 10
- Rust (stable) — install via rustup
- Platform build deps — see Tauri prerequisites for your OS
pim-ui/
├── src/ React frontend
│ ├── components/
│ │ ├── brand/ Logo, CliPanel, StatusIndicator
│ │ └── ui/ shadcn primitives (overridden per pim.yml)
│ ├── lib/
│ │ ├── rpc.ts typed client — mirrors src-tauri/src/rpc
│ │ └── utils.ts cn() helper
│ ├── screens/ one file per page
│ ├── globals.css brand tokens + Tailwind v4 @theme
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/ Rust shell
│ ├── src/
│ │ ├── rpc/ Tauri commands, one module per domain
│ │ └── daemon/ sidecar + remote implementations
│ ├── binaries/ pre-built pim-daemon (not committed)
│ ├── capabilities/ Tauri 2 permission manifests
│ ├── Cargo.toml
│ └── tauri.conf.json
├── scripts/
│ ├── sync-brand.sh sync tokens from the kernel repo
│ ├── fetch-daemon.sh download pim-daemon for bundling
│ ├── prepare-release.sh bump versions across package.json + cargo + tauri.conf
│ ├── pre-pr.sh auto-fix + run all CI checks locally
│ └── pre-pr-check.sh check-only mirror of CI for PR validation
└── .github/workflows/ quality-and-security · codeql-analysis · release · sbom · secret_scanning · dependency-review
Run the full CI check suite locally before opening a PR:
scripts/pre-pr.sh # auto-fixes formatting, then runs all checks
scripts/pre-pr-check.sh # check-only — matches CI exactlyBoth scripts run rustfmt, clippy, cargo test, pnpm typecheck,
pnpm build, pnpm test, gitleaks, cargo audit, and a final
cargo build of src-tauri. They mirror
.github/workflows/quality-and-security.yml and
.github/workflows/secret_scanning.yml.
scripts/prepare-release.sh --bump patch # or minor/major
git diff # review version bumps + lockfiles
git commit -am "chore: release vX.Y.Z"
git tag vX.Y.Z
git push --tagsThe tag push triggers .github/workflows/release.yml, which builds
Tauri bundles for every supported target, generates a SHA-256 next to
each, and publishes a draft GitHub release. Review and publish the
draft once the matrix completes.
The wire between the UI and the daemon is defined in
proximity-internet-mesh/docs/RPC.md. Until that doc is authored, the types
are hand-kept in sync between src-tauri/src/rpc/ (Rust) and src/lib/rpc.ts
(TypeScript).
Roadmap: migrate to tauri-specta v2
for generated bindings so the TS types are always truthful to the Rust source.
The pim brand is instrument-grade and terminal-native. Every surface — hero screen, status panel, CLI output, error message — is designed to feel like a well-made CLI, not a SaaS dashboard.
- Canonical logo:
█ pim— cursor-block + wordmark. Hero variant animates the cursor blink and types the wordmark character-by-character on mount (src/components/brand/logo.tsx,<Logo animated size="hero" />). - Status glyphs: Unicode-first —
◆ active,◈ relayed,○ connecting,✗ failed. Seesrc/components/brand/status-indicator.tsx. - Palette: green-tinted near-black ground, pale phosphor text, signal
green (
#22c55e) as the one active color, amber (#e8a84a) for warnings. - Motion: instant (linear, 100ms). No easing curves. Respects
prefers-reduced-motion.
See .design/branding/pim/patterns/guidelines.html in the kernel repo for
the full visual reference.
MIT — see LICENSE.