A Matrix client for Linux built with Rust + Qt 6 / QML, focused on maximum visual customization and the everyday chat feature set.
- Token-based auto-login: the last session's access token is stored on
disk under
~/.local/share/Rustrix/session.jsonand reused on the next launch — no password is ever stored. - Manual login with username + password (against the homeserver's
/loginendpoint). - Manual token entry for users who already have a working access token from another client (Element, FluffyChat, Cinny, etc.).
- IPv6-only transport toggle: when enabled, the underlying reqwest client is rebuilt with a custom resolver that issues only AAAA queries and refuses to dial IPv4 endpoints. Useful on IPv6-only / CGNAT-bypass networks and for testing dual-stack homeservers.
- End-to-end encryption via
matrix-sdk-crypto(Olm/Megolm). Keys are persisted in a SQLite store under~/.local/share/Rustrix/sqlite/. - Automatic migration of the old
matrix-client/data directory toRustrix/on first launch of the new version (session, crypto store, avatars, theme — all preserved).
- Send and receive text messages (Markdown supported via the SDK).
- Receive formatted messages (HTML).
- Send and receive images — inline preview in the bubble, click to open full-size.
- Send and receive videos — inline player with controls.
- Send and receive audio and arbitrary files — tiles with name, size, mime, and Download button.
- Multi-file attachments — queue multiple files + text, send them all together on Enter (Discord-style).
- Hidden files (dotfiles) are visible in the file picker.
- Download any attachment with a single click — files land in
~/Downloads/Rustrix/with collision-safe naming. - Live unread badges & highlight counts on the room list.
- Per-room last-event preview.
- Hierarchical Spaces → Rooms view on the left sidebar (indented children, space/room iconography, unread counts).
- Flat Rooms list view for direct messages and standalone rooms.
- Each entry shows avatar, name, last event, and unread counter.
- View & edit display name.
- Upload & set avatar (any image format supported by Qt).
- Set presence (online / unavailable / offline) with a status message.
The dedicated Appearance page exposes ~60 knobs, all saved to
~/.config/Rustrix/theme.json and restored on the next launch:
| Group | Knobs |
|---|---|
| Presets | Material Dark, Solarized Dark, Tokyo Night, Nordic, Dracula, Gruvbox, Catppuccin Mocha, Sunset, Matrix Green |
| Colors | window bg/fg, sidebar bg/fg, accent, accent-fg, danger, success, warning, muted, border, bubble bg/fg (own + other) |
| Typography | font family, monospace family, 5 sizes (XS/SM/MD/LG/XL) |
| Geometry | 3 radii, 4 paddings, 4 spacings |
| Bubbles | radius, padding H/V, max-width %, tail on/off |
| Avatars | 3 sizes, corner radius, shape (circle / rounded / square) |
| Scrollbars | width, radius |
| Behavior | compact mode, show timestamps, show avatars, animate bubbles, animation duration (ms) |
| Import / Export | full JSON export and import for theme sharing |
A ColorDialog is wired to every color picker, and a Slider plus +/−
buttons to every integer field — change anything and the entire UI
updates live, no restart required.
Rustrix/
├── Cargo.toml # dependencies & build profile
├── build.rs # Qt discovery helper
├── src/
│ ├── main.rs # entry point, registers QML types & singletons
│ ├── matrix_client.rs # central QML-facing MatrixClient singleton
│ ├── auth.rs # client construction + IPv6-only transport
│ ├── room_model.rs # QAbstractListModel for joined rooms
│ ├── message_model.rs # QAbstractListModel for a room's timeline
│ ├── spaces.rs # SpaceModel: spaces → rooms tree
│ ├── profile.rs # ProfileManager: display name / avatar / presence
│ ├── file_transfer.rs # upload & download of files/images/videos/audio
│ ├── theme.rs # Theme singleton with all visual knobs
│ ├── avatar_cache.rs # on-disk caches and downloads dir helpers
│ └── errors.rs # shared error type
├── qml/
│ ├── main.qml # root window + MainView (3-pane layout)
│ ├── LoginPage.qml # password + token login forms
│ ├── SpacesPage.qml # spaces → rooms tree
│ ├── RoomsSidebar.qml # flat room list (alternative)
│ ├── ChatPage.qml # header + message list + composer
│ ├── MessageBubble.qml # themed bubble for every event kind
│ ├── ProfilePage.qml # display name / avatar / presence editor
│ ├── SettingsPage.qml # network, IPv6, logout, diagnostics
│ ├── AppearancePage.qml# full theme editor with live preview
│ ├── Theme.qml # color helper (mix / lighten / darken / alpha)
│ ├── Components.qml # shared widget stubs
│ └── icons.qml # Canvas-drawn vector icons
├── assets/
│ ├── logo.svg
│ └── default-avatar.svg
└── scripts/
├── build.sh # release build helper
└── run.sh # debug run helper
sudo apt-get install -y \
build-essential pkg-config \
rustc cargo \
qt6-base-dev qt6-declarative-dev qt6-svg-dev \
libssl-dev libsqlite3-dev \
qmake6sudo dnf install -y \
rust cargo \
qt6-qtbase-devel qt6-qtdeclarative-devel qt6-qtsvg-devel \
openssl-devel sqlite-devel \
qt6-qtbase-devel-guisudo pacman -S --needed \
rust \
qt6-base qt6-declarative qt6-svg \
openssl sqlitenix-shell -p rustc cargo qt6.full pkg-config openssl sqlite# From the project root:
cargo run # debug build + run
cargo build --release # optimized binary at target/release/rustrixIf qmake is not on PATH, point the build at it explicitly:
QMAKE=/usr/bin/qmake6 cargo build --releaseThe scripts/build.sh and scripts/run.sh helpers wrap these for convenience.
- Launch the binary.
- The login window appears. Either:
- enter homeserver + username + password, or
- switch to the Token tab and paste your access token (and user ID).
- Optionally tick Force IPv6 to restrict all Matrix traffic to IPv6 endpoints only.
- Click Sign in.
- The session is stored on disk; subsequent launches auto-login.
| Path | Contents |
|---|---|
~/.local/share/Rustrix/session.json |
Homeserver URL, user ID, device ID, access token |
~/.local/share/Rustrix/sqlite/ |
Matrix SDK state (E2E keys, room state) |
~/.local/share/Rustrix/avatars/ |
Downloaded avatar thumbnails |
~/.config/Rustrix/theme.json |
Custom appearance settings |
~/Downloads/Rustrix/ |
All downloads from chats |
(If you previously used matrix-client, the old ~/.local/share/matrix-client/
directory is automatically renamed to ~/.local/share/Rustrix/ on the first
launch — your session, E2E keys, and theme are preserved.)
Either click Logout in the Settings page, or simply delete
~/.local/share/Rustrix/session.json. The SQLite store remains, so
E2E keys survive a re-login.
qmetaobject provides pure-Rust Qt bindings
(no C++ glue). All #[derive(QObject)] structs are exposed to QML via
register_type (instantiable) or register_singleton_type
(globally-available).
A single Tokio runtime is owned by the Backend singleton. Every QML-callable
method on MatrixClient spawns a future onto it; results are returned via
Qt signals (logged_in, sync_done, file_downloaded, last_error_changed,
…). The UI thread never blocks.
When Force IPv6 is on, build_client() constructs a reqwest client with
a custom resolver callback that:
- Issues only
ipv6_lookup(AAAA) queries viahickory-resolver. - Returns
Errif no AAAA records exist (forcing the request to fail rather than silently falling back to IPv4). - Sorts results to prefer ULA / global addresses over link-local.
This is the only change vs. the default transport — TLS, HTTP/2, etc. behave identically.
The Theme singleton is a #[derive(QObject)] Rust struct whose state is
mirrored to a ThemeState (serde). Every setter writes through to
~/.config/Rustrix/theme.json. QML reads properties via the
standard property binding, so changes propagate instantly.
The image://matrix/<media_source_json> QML image provider fetches media
bytes through the Matrix SDK (which transparently decrypts E2EE files),
caches them under <cache_dir>/Rustrix/media/, and serves them to QML
Image components. Videos are played inline with MediaPlayer +
VideoOutput.
- Sliding Sync is not wired up; we use plain
/sync. For large accounts, switching tomatrix_sdk_ui::sync_serviceis recommended. - No voice / video calls (MSC3401).
- No reply / edit / reactions UI (events are rendered as-is; SDK supports them, the QML side just needs the controls).
GPL-3.0-or-later.