Portty
Portty routes XDG Desktop Portal D-Bus requests into plain files and Unix sockets. Anything that can read and write files can respond to portal requests — a terminal, a shell script, a keybinding daemon, or echo.
Implemented Portals
| Portal | Operations | Description |
|---|---|---|
| FileChooser | open-file, save-file, save-files |
File open/save dialogs |
| Screenshot | screenshot, pick-color |
Screen capture and color picking |
Architecture
graph TD
App[Application] -->|D-Bus| Daemon[porttyd]
Daemon --> Socket[daemon.sock<br>Unix socket]
Daemon --> FIFO[daemon.ctl<br>FIFO]
Daemon --> Session[Session<br>file-based state]
Session -->|exec| Process[Spawned process]
Process -->|exit triggers submit| Session
Session --- Files[/"submission (file)<br>options.json (file)<br>portal (file)"/]
CLI[portty CLI] -->|control commands| Socket
CLI -->|read/write| Files
Script[Shell scripts<br>echo, cat, ...] -->|read/write| Files
Script -->|"echo submit >"| FIFO
Shims["Shims (sel, submit, ...)<br>= portty CLI wrappers"] --> CLI
Workspace
| Crate | Binary | Description |
|---|---|---|
crates/lib (libportty) |
— | Shared library: protocol, codec, client, paths, files, portal validation |
crates/daemon (porttyd) |
porttyd |
D-Bus service, session management, daemon socket + FIFO |
crates/cli (portty) |
portty |
CLI for interacting with sessions and the daemon |
Data Flow
- An application requests a portal action via D-Bus (e.g. open file dialog)
- The daemon checks for a queued submission — if one exists, it auto-applies and returns immediately
- Otherwise, the daemon creates a session directory with file-based state (
options.json,submission,portal) - The configured
execcommand is run (typically a terminal emulator, but can be any program — evensubmitfor instant auto-confirm). If the process exits, the session submits automatically - The session's
submissionfile can be edited by anything — theporttyCLI, shell shims on$PATH, raw file I/O, or commands piped into the FIFO - On submit/cancel, the daemon reads the submission file, validates it against portal constraints, and returns results via D-Bus
Session Directory
/tmp/portty/<uid>/
├── daemon.sock # Unix socket (CLI <-> daemon, bidirectional)
├── daemon.ctl # FIFO (fire-and-forget commands)
├── pending/submission # Entries queued before any session exists
├── submissions/<ts>-<portal>/ # Queued submissions (auto-applied on next dialog)
│ └── submission
└── <session-id>/
├── portal # "<portal>\n<operation>" (e.g. "file-chooser\nopen-file")
├── options.json # Session options (from D-Bus request)
├── submission # Current entries, one per line
└── bin/ # Shell shims prepended to $PATH
├── sel # -> portty edit "$@"
├── desel # -> portty edit --remove "$@"
├── reset # -> portty edit --reset
├── submit # -> portty submit
├── cancel # -> portty cancel
├── info # -> portty info
└── <custom> # From config [portal.bin] section
All data operations (editing submissions) are file-based. The daemon socket handles control commands only (submit, cancel, verify, reset, list).
Interaction
There are multiple ways to interact with a session — they all do the same thing (edit files, send control commands):
portty CLI / shims
The shims in bin/ are one-line wrappers around portty. They're the same thing.
# These are equivalent:
# Edit submission
# Control
# Management (context-independent)
# Target a specific session
The CLI auto-detects context via PORTTY_SESSION env var — inside a session terminal it operates on the session directory, outside it operates on pending entries.
Raw file I/O
Since state is just files, you can skip the CLI entirely:
# Read options
# Write submission directly
# Clear submission
> /tmp/portty//<session-id>/submission
FIFO
Fire-and-forget commands — useful for scripting and keybindings:
Unix socket
For bidirectional communication (when you need the response):
|
Submission Queue
Pre-queue entries before a dialog opens. When the next dialog arrives, the queued submission is auto-applied without running exec:
Daemon Control Protocol
Plain text, newline-terminated. Shared by the socket and FIFO.
Request (single line)
submit [session_id]
cancel [session_id]
verify [session_id]
reset [session_id]
list
When session_id is omitted, the earliest active session is targeted.
Response (socket only — FIFO discards responses)
ok
error: <message>
<id>\t<portal>\t<operation>\t<created>\t<dir>\t<title>\n
...
ok
Session listing emits one tab-separated line per session, terminated by ok.
Configuration
~/.config/portty/config.toml — see misc/config.toml.example for a full annotated example.
Config resolution priority: operation-specific > portal-specific > root default.
= "foot" # root default (auto-detected if not set)
[]
= "foot" # portal default
[]
= "submit" # operation override: auto-confirm saves
[]
= "fzf --multi | sel --stdin" # custom shim on $PATH
Set exec = "" for headless mode (no process spawned, interact via CLI only).
Session Environment
| Variable | Description |
|---|---|
PORTTY_SESSION |
Session ID |
PORTTY_DIR |
Session directory path |
PORTTY_PORTAL |
Portal name (e.g. file-chooser) |
PORTTY_OPERATION |
Operation name (e.g. open-file) |
The session bin/ directory is prepended to $PATH.
Adding a New Portal
1. Define validation logic in libportty
Create crates/lib/src/portal/<portal>.rs:
use ;
/// Validate and transform submission entries.
/// Called at submit time — check constraints and produce final output.
/// Smart add behavior (single-select replace vs multi-select append).
Register in crates/lib/src/portal/mod.rs:
Add the feature to crates/lib/Cargo.toml:
[]
= ["portal"]
Wire it into SessionContext::add_entries() and validate() in crates/lib/src/portal/mod.rs.
2. Define D-Bus types in porttyd
Create crates/daemon/src/dbus/<portal>.rs implementing the org.freedesktop.impl.portal.* interface using zbus. See dbus/file_chooser.rs or dbus/screenshot.rs as examples.
The key pattern: define a Handler trait that the portal implementation calls, and a D-Bus proxy struct that implements the zbus interface and delegates to the handler.
3. Implement the portal handler
Create crates/daemon/src/portal/<portal>.rs:
run_session handles the entire lifecycle: queued submission check -> session creation -> exec spawn -> wait -> unregister -> validate.
4. Register in the server
In crates/daemon/src/server.rs, add to register_portals():
let my_portal = new;
let builder = builder.serve_at?;
5. Update the portal file
In misc/tty.portal, add the interface:
Interfaces=...;org.freedesktop.impl.portal.MyPortal
Building
Requires nightly Rust (uses linux_pidfd and unix_mkfifo features).
Installation
# Install binaries
# Install portal file
# Install systemd service (optional)
License
MIT