English · Français
iPhone and Android texts and calls on a Linux PC, over Bluetooth only: no third-party server, no Apple or Google account, no app to install on the phone. Written in Rust, egui interface.
Unlike KDE Connect / GSConnect, nothing runs on the phone: the PC talks to it through standard Bluetooth profiles (MAP, PBAP, HFP). Works with iPhone as well as Android.
- Setup assistant: turns the Bluetooth adapter on, finds and pairs the phone (code confirmed on both sides), then tells you what to enable on it; unpair a device from the assistant; several devices, each with its own history.
- Texts: read and send through the Bluetooth MAP profile (via
bluez-obex), colour emojis, reactions, clickable links with optional previews (title and image), resend on failure. - Conversations: SMS and Calls tabs, search in names and in the text of every message, mute, archive, local deletion, export to a JSON file.
- Contacts: synced over PBAP every 6 h (names in conversations, search).
- Calls: ringing, answer, decline, hang up, mute, hold, call waiting, outgoing calls, quick replies by text after declining. Audio (mSBC) goes through PipeWire/WirePlumber in the HFP hands-free role.
- Desktop notifications (texts with Reply, incoming call with Answer / Decline / Quick reply), even with the window closed; iPhone battery level and link lights in the header.
- Privacy and robustness: local SQLite history (backed up before each migration), logs without numbers, names, texts or addresses, bounded waits on every Bluetooth exchange, sandboxed systemd service, translatable interface (English, French), no system font needed.
- Compatibility
- Components
- Requirements
- Installation
- Getting started
- Usage
- Files
- Colours (matugen)
- Translations
- Code layout
- Development
- About this project
- License
| Status | |
|---|---|
| iPhone (iOS) | Texts (SMS/MMS), calls, contacts. No iMessage (not exposed over MAP). |
| Android | Texts, calls, contacts. Allow the PC to access messages and contacts when the phone asks (the MAP service only appears once sharing is allowed). |
| Tested on | Arch Linux, Hyprland, iPhone 11 and one Android phone |
| Other distributions / desktops | Signed .deb / .rpm / Arch packages provided; community testing welcome |
- iOS does not expose iMessage over MAP (only SMS/MMS text).
- The MAP
sent/outboxfolders are empty on the iPhone, so messages sent from the app are recorded locally. - Reactions (tapbacks) are sent as plain texts ("Liked “…”").
- If a phone forgets the PC, unpair it from the assistant too before pairing again.
| Binary | Role |
|---|---|
bluez-messaged |
Daemon (systemd user service): MAP, PBAP, calls, notifications, history, socket for clients. |
bluez-message |
Main window (menu entry: bmessage), client of the daemon. |
bluez-msg-call |
Small ongoing-call window, started by the daemon when a call is answered. |
bluez-msg |
Command-line tool (MAP/HFP tests, translation files). |
- Linux with BlueZ,
bluez-obex, PipeWire and WirePlumber (≥ 0.5,org.pipewire.Telephonyservice). - A notification server (mako, dunst, swaync…) for notifications.
- Vulkan recommended (lighter rendering), OpenGL fallback otherwise.
- No font to install: icons (Material Design Icons) and emojis (Twemoji) are embedded.
- To build: Rust ≥ 1.91 (2024 edition).
Prebuilt package (x86_64) from the releases,
signed with key 0FC9047F715A691A8C0F1BFDCB996B67A2CEA2E0.
Compatible with:
— any systemd-based Arch derivative. (On non-systemd Arch systems such as Artix, use build from source with your own init's service manager.)
VER=1.1.3
base="https://github.com/morganorix/bluez-message/releases/download/v$VER"
# Once: trust the signing key
curl -sL "$base/bluez-message.asc" | sudo pacman-key --add -
sudo pacman-key --lsign-key 0FC9047F715A691A8C0F1BFDCB996B67A2CEA2E0
# Install
sudo pacman -U "$base/bluez-message-${VER}-1-x86_64.pkg.tar.zst"Or build it from a clone of the repository:
cd packaging/arch
makepkg -siThe package installs the four binaries, the bluez-messaged service (enabled for every user
session), the bmessage menu entry, the icon and the WirePlumber configuration. Details (French):
packaging/arch/README.md.
Prebuilt .deb (x86_64) from the releases,
GPG-signed with key 0FC9047F715A691A8C0F1BFDCB996B67A2CEA2E0. Download the package, its
signature and the public key, verify, then install:
Compatible with:
— and other Debian/Ubuntu derivatives (amd64).
VER=1.1.3
base="https://github.com/morganorix/bluez-message/releases/download/v$VER"
curl -LO "$base/bluez-message_${VER}-1_amd64.deb"
curl -LO "$base/bluez-message_${VER}-1_amd64.deb.sig"
curl -LO "$base/bluez-message.asc"
# Verify (once: import the key)
gpg --import bluez-message.asc
gpg --verify "bluez-message_${VER}-1_amd64.deb.sig" "bluez-message_${VER}-1_amd64.deb"
# Install (apt pulls in bluez, bluez-obexd, pipewire, wireplumber…)
sudo apt install "./bluez-message_${VER}-1_amd64.deb"Prebuilt .rpm (x86_64) from the releases,
GPG-signed with the same key:
Compatible with:
— RHEL clones likely need EPEL for some dependencies.
VER=1.1.3
base="https://github.com/morganorix/bluez-message/releases/download/v$VER"
curl -LO "$base/bluez-message-${VER}-1.x86_64.rpm"
curl -LO "$base/bluez-message-${VER}-1.x86_64.rpm.sig"
curl -LO "$base/bluez-message.asc"
# Verify (once: import the key)
gpg --import bluez-message.asc
gpg --verify "bluez-message-${VER}-1.x86_64.rpm.sig" "bluez-message-${VER}-1.x86_64.rpm"
# Install (dnf resolves bluez, bluez-obexd, pipewire, wireplumber…)
sudo dnf install "./bluez-message-${VER}-1.x86_64.rpm"Like the Arch package, the .deb/.rpm install the four binaries, enable the
bluez-messaged user service for every user session, and add the bmessage menu
entry, the icon and the WirePlumber configuration.
Supported baselines (for PipeWire Telephony / HFP calls): Ubuntu 24.04+, Debian 13+, Fedora 40+. SMS (MAP) works on older releases; only the call features need the newer PipeWire.
Requires a recent Rust via rustup (the project needs the 2024 edition):
# .deb — on Debian/Ubuntu
sudo apt-get install -y pkg-config libsqlite3-dev libxkbcommon-dev libwayland-dev
cargo install cargo-deb && cargo deb # -> target/debian/*.deb
# .rpm — on Fedora
sudo dnf install -y gcc pkgconf-pkg-config sqlite-devel libxkbcommon-devel wayland-devel
cargo install cargo-generate-rpm
cargo build --release --features gui && cargo generate-rpm # -> target/generate-rpm/*.rpmBuild distributable packages on the target distribution (or via the CI Packages workflow), not on a rolling-release system: a binary linked against a very recent glibc will not run on stable Debian/Ubuntu/Fedora.
Any distribution can run bmessage by building from source — needs Rust (2024
edition, via rustup) plus the sqlite, wayland and
libxkbcommon development files, and the bluez + bluez-obex, pipewire and
wireplumber runtime services.
cargo build --release --all-features
install -Dm755 -t ~/.local/bin target/release/{bluez-msg,bluez-messaged,bluez-message,bluez-msg-call}
# User service (ExecStart pointed at ~/.local/bin/bluez-messaged)
install -Dm644 packaging/arch/bluez-messaged.service ~/.config/systemd/user/bluez-messaged.service
sed -i "s#/usr/bin/#$HOME/.local/bin/#" ~/.config/systemd/user/bluez-messaged.service
# Menu entry and icon
install -Dm644 packaging/arch/bluez-message.desktop ~/.local/share/applications/bluez-message.desktop
install -Dm644 packaging/arch/bluez-message.svg ~/.local/share/icons/hicolor/scalable/apps/bluez-message.svg
# Call audio (HFP hands-free role + gain on received audio)
install -Dm644 -t ~/.config/wireplumber/wireplumber.conf.d packaging/arch/5{2,3}-*.conf
install -Dm644 -t ~/.local/share/wireplumber/scripts packaging/arch/bluez-message-call-gain.lua
systemctl --user daemon-reload
systemctl --user restart wireplumber
systemctl --user enable --now bluez-messagedAfter restarting WirePlumber, reconnect the iPhone (bluetoothctl disconnect then connect).
Dependency names and notes per distribution:
—
dev-db/sqlite,dev-libs/wayland,x11-libs/libxkbcommon,net-wireless/bluez(with theobexUSE flag),media-video/pipewire,media-video/wireplumber; Rust fromdev-lang/rustor rustup. Follow the steps above.—
apk add rust cargo sqlite-dev wayland-dev libxkbcommon-dev bluez bluez-obexd pipewire wireplumber. Alpine is musl-based: the CLI (bluez-msg) builds cleanly; the GUI (wgpu/winit) may need extra work, andgcompatif a dependency expects glibc. Alpine uses OpenRC, so run the daemon with your own supervisor instead of the systemd user service.— the immutable layout does not fit the
install -Dsteps. Package it as a Nix derivation (rustPlatform.buildRustPackagewithsqlite,wayland,libxkbcommoninbuildInputs), enablehardware.bluetooth,services.pipewireand WirePlumber at the system level, and runbluez-messagedas a Home Manager user service. (A flake may be provided later.)
Received call audio is amplified by +18 dB (linear gain 8.0), because the iPhone sends it very low
over HFP (around -34 dBFS at peak). To change it, copy the script to
~/.local/share/wireplumber/scripts/bluez-message-call-gain.lua (it takes precedence over
/usr/share/wireplumber/scripts/), edit local GAIN = 8.0 (1.0 = no gain, 4.0 ≈ +12 dB), then
systemctl --user restart wireplumber and reconnect the iPhone.
-
Open bmessage from the menu. Without a configured device, the setup assistant opens (also available from Settings > Devices > Set up a device):
- Bluetooth adapter: turn it on if needed.
- Device: search, then pair the phone (confirm the same code on both sides), or pick an already paired one, or type its address.
- Phone settings: on iPhone, in Settings > Bluetooth > (i) next to the PC, enable Show Notifications (MAP) and Sync Contacts (PBAP); on Android, allow access to messages and contacts for the PC in the pairing prompt or the Bluetooth device settings; the Calls (HFP), Messages (MAP) and Contacts (PBAP) lights turn green. iOS also asks for permission on first access.
- Done.
Manual equivalent: pair with
bluetoothctl(pair,trust,connect <address>), write the addressAA:BB:CC:DD:EE:FFto~/.config/bluez-message/device, thensystemctl --user restart bluez-messaged.
- Left column, "Contacts": SMS and Calls tabs (half width each). Landlines only appear in Calls,
under the contact name when they are in the address book. "Search" field (accents ignored, local or
+33…format; Enter opens the first result). From 2 characters it also searches the text of every message ("Messages" section, accents and case ignored): a click opens the conversation on that message. - Right-click on a conversation: Mute (no SMS notification, calls still ring), Archive (moved to the collapsible "Archived (N)" section until the next incoming SMS), Delete.
- Thread: send, emoji button, call button in the header; "Older messages" at the top loads the rest of the history. Web links (http/https) are clickable.
- Right-click on a bubble: reaction, Resend, Copy, Delete (local deletion only, never on the iPhone).
- A pending message shows a progress bar; if sending fails, a button resends it.
- Header: Calls (HFP), Messages (MAP) and Contacts (PBAP) lights, and the iPhone battery level (red below 20 %).
- New SMS notification: Reply opens the window on the conversation (started if closed).
Shortcuts: ↑/↓ + Enter in the list, Esc, Ctrl+F (search), Ctrl+, (settings).
When the phone rings, a notification offers Answer / Decline, plus, for a mobile number,
QUICK REPLY: it declines and opens the conversation (the window is started if needed), with the quick
replies as chips above the input: a click fills it, nothing is sent automatically. In the window, the call
banner offers the same choices. Once the call is up, the
bluez-msg-call window shows the duration and the Mute, Hold, Hang up buttons (plus Answer / Decline /
Swap during call waiting). It closes by itself when the call ends.
Tabs:
- General: language (applied live), phone number country (Automatic: from the language or the
locale; used to recognise landlines and to group a number written as
06…or+33…), Link previews (off by default: title and image under a link, fetched by the window over https, which reveals your IP address to the linked site; private addresses refused). - Notifications: new SMS, incoming calls, hide message text.
- Devices: Bluetooth device, Connect automatically, Set up a device.
- History: retention (30 / 90 / 365 days), Export history (JSON file in the Downloads folder, or the home folder, readable by you only, the only export in the window; deleted messages left out), purge deleted messages, Clear history (double confirmation, contacts are kept).
- Replies: quick replies offered as chips after QUICK REPLY (5 at most, 160 characters each).
bluez-msg list <address> [inbox|sent|outbox|deleted] # JSON, one message per line
bluez-msg send <address> <number> "<text>"
bluez-msg watch <address> [--interval 5]
bluez-msg serve <address> # engine driven by JSON over stdin/stdout
bluez-msg hfp <address> [--answer] # raw RFCOMM HFP, no audio (hfp_hf disabled)
bluez-msg export [--key <number>] # via the daemon: whole history or one conversation
bluez-msg locale template | check <file>
bluez-message --open <number> # window on that conversation| Path | Content |
|---|---|
~/.config/bluez-message/device |
iPhone Bluetooth address (0600). |
~/.config/bluez-message/settings.json |
Settings (written by the daemon only). |
~/.config/bluez-message/locales/<lang>.json |
Your own translations. |
~/.local/share/bluez-message/history.db |
SQLite history (texts, calls, contacts, read marks). Numbers are keyed in international format; an older database is migrated at startup, after a copy to history.db.bak (key format) or history.db.v<N>.bak (schema). Export: bluez-message-<date>[-<n>].json in the Downloads folder (-2…-9 if the name is taken). |
$XDG_RUNTIME_DIR/bluez-message.sock |
Daemon socket (JSON lines). |
~/.config/bluez-message/colors.plm |
Optional: colour palette (matugen template), reloaded live. |
BLUEZ_MESSAGE_LANG variable: forces the language (fr, en…).
BLUEZ_MESSAGE_LOG variable: log level (info by default, without numbers, names, texts or addresses;
debug for troubleshooting, e.g. systemctl --user edit bluez-messaged → Environment=BLUEZ_MESSAGE_LOG=debug).
Window classes: bluez-message (main; suggested: floating, centred, 900×640) and bluez-msg-call
(call; suggested: floating, pinned, 420×150, top of the screen).
When a notification opens a conversation, the window is focused with hyprctl dispatch (Lua syntax).
The main window and the call popup read their palette from
~/.config/bluez-message/colors.plm and reload it live (within ~5 s) whenever it
changes — so it can follow your matugen Material You
scheme (generated from your wallpaper).
The file is just a list of let <name> = #RRGGBB lines. Recognised names (anything else
is ignored; a missing one keeps its default):
| Name | Role | Default |
|---|---|---|
accent |
primary accent | #b0d18b |
on_accent |
text/icons on the accent | #1e3702 |
accent2 |
secondary accent | #bfcbad |
fg |
foreground text | #e2e3d8 |
bg |
background | #12140e |
danger |
errors / destructive | #ffb4ab |
To drive it from matugen, add a template and point its output at that file.
~/.config/matugen/templates/bluez-message.plm:
library Colors {
let accent = {{colors.primary.default.hex}}
let on_accent = {{colors.on_primary.default.hex}}
let accent2 = {{colors.secondary.default.hex}}
let fg = {{colors.on_surface.default.hex}}
let bg = {{colors.surface.default.hex}}
let danger = {{colors.error.default.hex}}
}
~/.config/matugen/config.toml:
[templates.bluez-message]
input_path = "~/.config/matugen/templates/bluez-message.plm"
output_path = "~/.config/bluez-message/colors.plm"Then matugen image /path/to/wallpaper.jpg (re-run on each wallpaper change): bmessage
repaints itself with the new colours, no restart.
Texts are written in English in the code and translated at run time from JSON files.
To add a language, see locales/README.md.
src/
├── main.rs bluez-msg CLI
├── bin/
│ ├── bluez-messaged.rs daemon: engine, notifications, history, socket, link watch
│ ├── bluez-message/ main window (ui, convs, thread, composer, prefs, setup assistant)
│ └── bluez-msg-call/ call window
├── engine.rs shared engine: MAP polling, sending, calls (Cmd commands / Out events)
├── map.rs, obex.rs MAP client over bluez-obex (D-Bus)
├── bmsg.rs bMessage encoding/decoding (SMS)
├── pbap.rs contacts (vCard)
├── telephony.rs org.pipewire.Telephony D-Bus client (calls, HFP audio held by PipeWire)
├── hfp.rs raw RFCOMM hands-free unit (AT commands, CLI only)
├── bluez.rs device state and reconnection (org.bluez)
├── ipc.rs JSON-lines protocol daemon ↔ clients
├── history.rs, reads.rs SQLite database (user_version migrations), slow reads (search, pages, export)
├── setup.rs pairing agent and adapter control (setup assistant)
├── serve.rs, log.rs JSON-lines engine on stdin/stdout, logs with masked addresses
├── settings.rs, config.rs, i18n.rs, phone.rs, numbering.rs, stamp.rs, addr.rs, error.rs
├── app/ shared state (conversations, call), notifications, reactions
└── gui/ theme, widgets, icons (MDI), emojis (Twemoji), link previews, egui startup
assets/mdi/ Material Design Icons SVG (white), LICENSE, NOTICE
locales/ fr.json (embedded), template.json, README
packaging/arch/ PKGBUILD, systemd service, .desktop, icon, WirePlumber config
packaging/aur/ AUR PKGBUILD (sources from the GitHub tag)
packaging/debian/ .deb maintainer scripts (enable the user service)
.github/workflows/ CI (clippy/tests) and Packages (signed .deb/.rpm/Arch on tags)
cargo build --all-features
cargo test --all-features
cargo clippy --all-features --all-targets -- -D warningsEvery new t!("…") string must be added to locales/template.json (checked by the
template_matches_sources test).
"Vibe coded": the code was written by an AI (Claude Code) under the direction of the author, who set its structural, optimisation and security guidelines and tested it on his own hardware (iPhone 11 and an Android phone, Arch Linux, Hyprland). Bug reports and feedback are welcome in issues.
Embedded assets:
- Material Design Icons (Apache-2.0), recoloured, see assets/mdi/NOTICE.
- Twemoji graphics (CC-BY 4.0) through the
twemoji-assetscrate.


