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
# Root level = default for all portals
# Auto-detects terminal if not set (foot, alacritty, kitty, etc.)
exec = "foot"
# File chooser portal configuration
[file-chooser]
exec = "foot" # default for all file-chooser operations
# Custom commands available in sessions
# Added to $PATH alongside default shims (sel, submit, cancel)
[file-chooser.bin]
pick = "fzf --multi | sel --stdin"
preview = "bat \"$@\""
# Per-operation overrides
# Priority: operation-specific -> file-chooser -> root default
# SaveFile: auto-confirm with proposed filename
[file-chooser.save-file]
exec = "submit" # uses submit shim for instant confirmation
# SaveFiles: auto-confirm with proposed directory
[file-chooser.save-files]
exec = "submit"
# Headless mode (no terminal, CLI only):
# Set exec = "" at any level, then use `portty` CLI to interact
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>/
├── daemon.sock # Daemon control socket (CLI <-> daemon)
└── <session-id>/
├── bin/
│ ├── sel # Shell shim -> portty select
│ ├── submit # Shell shim -> portty submit
│ └── cancel # Shell shim -> portty cancel
├── sock # Session Unix socket for IPC
└── portal # Portal type identifier
Session Commands
Commands are generated per-session as shell shims. For the file chooser portal:
sel
Manage file selection.
# Add files to selection
sel file1.txt file2.txt
# Select files from stdin
find . -name "*.rs" | sel --stdin
# Show current selection (no args)
sel
submit
Confirm selection and complete the dialog.
submit
cancel
Cancel the current operation.
cancel
CLI Usage
The portty CLI can control sessions from outside the spawned terminal:
# List active sessions
portty --list
# Add files to selection
portty select file1.txt file2.txt
# Submit the current session
portty submit
# Target a specific session
portty --session <id> select file.txt
When multiple sessions are active, commands target the earliest (oldest) session by default.
IPC Protocol
Session shims 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 (shim -> session):
enum Request {
GetOptions, // Get session options
GetSelection, // Get current selection
Select(Vec<String>), // Select files (URIs)
Cancel, // Cancel operation
}
Response (session -> shim):
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/ipc/src/ipc/<portal>.rsRegister commands in
crates/daemon/src/session.rs:
fn default_commands(portal: &str) -> &'static [(&'static str, &'static str)] {
match portal {
"file-chooser" => &[("sel", "select"), ("submit", "submit"), ("cancel", "cancel")],
"my-portal" => &[("my_cmd", "my_cmd"), ("submit", "submit"), ("cancel", "cancel")],
_ => &[],
}
}
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 CLI
install -Dm755 target/release/portty /usr/bin/portty
# 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