portty
`xdg-desktop-portal-tty` desktop portal implemented in terminal environment
Portty
An XDG Desktop Portal backend for TTY environments. This allows terminal-based applications to handle portal requests like file chooser dialogs by spawning a terminal with helper utilities.
Implemented Portals
| Portal | Status | Description |
|---|---|---|
| FileChooser | ✅ | Open/save file dialogs |
How It Works
- An application requests a portal action (e.g., open file dialog)
- The daemon creates a session directory at
/tmp/portty/<uid>/<session-id>/ - Shell shims are generated in
<session-dir>/bin/for portal-specific commands - A terminal is spawned with the session bin directory prepended to
$PATH - The user interacts with the terminal to complete the action
- The result is sent back to the requesting application
Configuration
Configuration file: ~/.config/portty/config.toml
# Path to the portty-builtin binary
builtin_path = "/usr/lib/portty/portty-builtin"
[default]
# Default terminal command for all portals
exec = "foot"
[file-chooser]
# Override for file chooser portal
exec = "foot"
Session Environment
When a terminal is spawned for a portal action, these environment variables are set:
| Variable | Description |
|---|---|
PORTTY_SESSION |
Unique session identifier |
PORTTY_DIR |
Session directory path |
PORTTY_SOCK |
Path to the IPC socket |
PORTTY_PORTAL |
Portal type (e.g., file_chooser) |
The session bin directory ($PORTTY_DIR/bin) is prepended to $PATH.
Session Directory Structure
/tmp/portty/<uid>/<session-id>/
├── bin/
│ ├── sel # Shell shim -> portty-builtin file-chooser select
│ └── cancel # Shell shim -> portty-builtin file-chooser cancel
├── sock # Unix domain socket for IPC
└── portal # Portal type identifier
Builtin Commands
Commands are generated per-session as shell shims. For the file chooser portal:
sel
Manage file selection.
# Select files (completes the dialog)
sel file1.txt file2.txt
# Select files from stdin
find . -name "*.rs" | sel --stdin
# Show current selection
sel
# Show session options (filters, title, etc.)
sel --options
cancel
Cancel the current operation.
cancel
IPC Protocol
The daemon and builtins communicate via Unix domain socket using length-prefixed bincode messages.
Message Format
[4 bytes: message length (little-endian u32)]
[N bytes: bincode-serialized payload]
FileChooser Messages
Request (builtin -> daemon):
enum Request {
GetOptions, // Get session options
GetSelection, // Get current selection
Select(Vec<String>), // Select files (URIs)
Cancel, // Cancel operation
}
Response (daemon -> builtin):
enum Response {
Options(SessionOptions), // Session options
Selection(Vec<String>), // Current selection
Ok, // Success
Error(String), // Error message
}
struct SessionOptions {
title: String,
multiple: bool,
directory: bool,
save_mode: bool,
current_name: Option<String>,
current_folder: Option<String>,
filters: Vec<Filter>,
current_filter: Option<usize>,
}
Connecting to the Socket
From Rust:
use std::os::unix::net::UnixStream;
let mut stream = UnixStream::connect(std::env::var("PORTTY_SOCK")?)?;
// write length-prefixed bincode message
// read length-prefixed bincode response
Implementing a New Portal
- Define IPC types in
crates/types/src/ipc/<portal>.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Request {
// Portal-specific requests
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Response {
// Portal-specific responses
Ok,
Error(String),
}
- Register commands in
crates/daemon/src/session.rs:
// Returns (shim_name, internal_command) pairs
fn default_commands(portal: &str) -> &'static [(&'static str, &'static str)] {
match portal {
"file-chooser" => &[("sel", "select"), ("cancel", "cancel")],
"my-portal" => &[("my_cmd", "my_cmd"), ("cancel", "cancel")],
_ => &[],
}
}
- Add builtin handler in
crates/builtins/src/:
Create my_portal.rs with a dispatch(command, args) function.
- Register in main.rs:
// crates/builtins/src/main.rs
match portal.as_str() {
"file_chooser" => portty_builtins::file_chooser::dispatch(command, rest),
"my_portal" => portty_builtins::my_portal::dispatch(command, rest),
// ...
}
Implement the portal in
crates/daemon/src/portal/<portal>.rsUpdate the portal file in
misc/tty.portal:
[portal]
DBusName=org.freedesktop.impl.portal.desktop.tty
Interfaces=org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.MyPortal;
Building
cargo build --release
Installation
# Install the daemon
install -Dm755 target/release/porttyd /usr/lib/portty/porttyd
# Install the builtin binary
install -Dm755 target/release/portty-builtin /usr/lib/portty/portty-builtin
# Install portal file
install -Dm644 misc/tty.portal /usr/share/xdg-desktop-portal/portals/tty.portal
# Install systemd service (optional)
install -Dm644 misc/portty.service /usr/lib/systemd/user/portty.service
License
MIT