Documentation Index
Fetch the complete documentation index at: https://mintlify.com/sxyazi/yazi/llms.txt
Use this file to discover all available pages before exploring further.
Yazi’s Data Distribution Service (DDS) provides a built-in publish-subscribe system that enables communication between multiple Yazi instances and plugins. It’s built on a client-server architecture with no additional server process required.
Overview
DDS enables:
- Cross-instance communication - Share data between multiple Yazi instances
- State persistence - Maintain state across all instances
- Plugin messaging - Plugins can publish and subscribe to events
- Lua integration - Full Lua API for pub/sub operations
- Automatic discovery - Instances find each other automatically
Architecture
DDS uses a hybrid client-server model:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Yazi #1 │◄────►│ DDS Server │◄────►│ Yazi #2 │
│ (Client) │ │ (in Yazi #1)│ │ (Client) │
└─────────────┘ └─────────────┘ └─────────────┘
▲ ▲
│ │
└──────────────────────────────────────────┘
Direct communication via Unix socket
- First instance - Starts DDS server automatically
- Additional instances - Connect as clients to existing server
- No separate daemon - Server embedded in first Yazi process
- Unix socket - Communication via filesystem socket
Message Types (Embers)
DDS messages are called “embers” and include:
System Events
hi - Client announces capabilities
hey - Server broadcasts peer list
bye - Client disconnects gracefully
File Events
cd - Directory change
load - Folder loading complete
hover - File hover/selection
rename - File renamed
move - Files moved
duplicate - Files duplicated
trash - Files moved to trash
delete - Files permanently deleted
download - Remote files downloaded
Special Events
yank - Files yanked to clipboard (cut/copy)
bulk - Bulk file changes
tab - Tab switched
mount - Filesystem mounted/unmounted
custom - User-defined events
Client-Server Implementation
The DDS system is implemented in yazi-dds/:
Server
The server (yazi-dds/src/server.rs:17) manages connections:
pub(super) struct Server;
impl Server {
pub(super) async fn make() -> Result<JoinHandle<()>> {
CLIENTS.write().clear();
let listener = Stream::bind().await?;
Ok(tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let (reader, mut writer) = tokio::io::split(stream);
// Handle connection...
}
}))
}
}
Client
Clients (yazi-dds/src/client.rs:34) connect and communicate:
impl Client {
pub(super) fn serve() {
tokio::spawn(async move {
let mut server = None;
let (mut lines, mut writer) = Self::connect(&mut server).await;
loop {
select! {
Some(payload) = rx.recv() => {
writer.write_all(payload.as_bytes()).await;
}
Ok(next) = lines.next_line() => {
// Process received message
}
}
}
});
}
}
Publish-Subscribe API
The pub/sub system (yazi-dds/src/pubsub.rs) provides:
Subscribe (Local)
Subscribe to events in the current instance:
local function on_tab_switch(id)
ya.err("Switched to tab: " .. id)
end
ps.sub("my-plugin", "tab", on_tab_switch)
Subscribe (Remote)
Subscribe to events from all instances:
local function on_remote_cd(tab, url)
ya.err("Another instance changed to: " .. tostring(url))
end
ps.sub_remote("my-plugin", "cd", on_remote_cd)
Publish
Send events to subscribers:
-- Publish to local subscribers
ps.pub("custom", "my-event-data")
-- Publish to specific instance
ps.pub_to(receiver_id, "custom", "my-event-data")
Unsubscribe
ps.unsub("my-plugin", "tab")
ps.unsub_remote("my-plugin", "cd")
Built-in Event Publishing
Yazi automatically publishes events using the pub_after_* pattern:
impl Pubsub {
pub fn pub_after_cd(tab: Id, url: &UrlBuf) -> Result<()> {
if BOOT.local_events.contains("cd") {
EmberCd::borrowed(tab, url).with_receiver(*ID).flush()?;
}
if PEERS.read().values().any(|p| p.able("cd")) {
Client::push(EmberCd::borrowed(tab, url))?;
}
if LOCAL.read().contains_key("cd") {
Self::pub(EmberCd::owned(tab, url))?;
}
Ok(())
}
}
This ensures events are:
- Delivered locally if subscribed
- Broadcast to remote peers if they subscribe
- Persisted if static event
State Persistence
Static events (prefixed with @) are persisted:
if receiver == 0 && kind.starts_with('@') {
let Some(body) = parts.next() else { continue };
if !STATE.set(kind, sender, body) { continue }
}
When new clients connect, they receive all persisted state:
if let Some(state) = &*STATE.read() {
state.values().for_each(|s| _ = tx.send(s.clone()));
}
This enables:
- Shared clipboard - Yank state synced across instances
- Plugin state - Persistent data between restarts
- Configuration sync - Runtime config changes propagated
Peer Discovery
Instances track each other via hey messages:
fn handle_hey(clients: &HashMap<Id, Client>) {
let payload = Payload::new(EmberHey::owned(
clients.values().map(|c| (c.id, Peer::new(&c.abilities))).collect(),
));
if let Ok(s) = try_format!("{payload}\n") {
clients.values().for_each(|c| _ = c.tx.send(s.clone()));
}
}
Each instance knows:
- Peer IDs - Unique identifier for each instance
- Capabilities - What events each peer can receive
- Connection status - When peers join/leave
Ya CLI Integration
The ya command-line tool uses DDS to communicate with running instances:
Send Single Message
ya pub static quit --json '{"args": []}'
Implemented in yazi-dds/src/client.rs:81:
pub async fn shot(kind: &str, receiver: Id, body: &str) -> Result<()> {
Ember::validate(kind)?;
let payload = try_format!(
"{}\n{kind},{receiver},{ID},{body}\n{}\n",
Payload::new(EmberHi::borrowed(iter::empty())),
Payload::new(EmberBye::borrowed())
)?;
let (mut lines, mut writer) = Stream::connect().await?;
writer.write_all(payload.as_bytes()).await?;
writer.flush().await?;
drop(writer);
// Wait for response...
}
Listen to Events
Implemented in yazi-dds/src/client.rs:141:
pub async fn draw(kinds: HashSet<&str>) -> Result<()> {
async fn make(kinds: &HashSet<&str>) -> Result<ClientReader> {
let (lines, mut writer) = Stream::connect().await?;
let hi = Payload::new(EmberHi::borrowed(kinds.iter().copied()));
writer.write_all(try_format!("{hi}\n")?.as_bytes()).await?;
writer.flush().await?;
Ok(lines)
}
let mut lines = make(&kinds).await
.context("No running Yazi instance found")?;
loop {
match lines.next_line().await? {
Some(s) => {
let kind = s.split(',').next();
if matches!(kind, Some(kind) if kinds.contains(kind)) {
println!("{s}");
}
}
None => /* reconnect */,
}
}
}
Connection Management
Automatic Reconnection
Clients automatically reconnect if connection lost:
select! {
Some(payload) = rx.recv() => {
if writer.write_all(payload.as_bytes()).await.is_err() {
(lines, writer) = Self::reconnect(&mut server).await;
writer.write_all(payload.as_bytes()).await.ok(); // Retry once
}
}
Ok(next) = lines.next_line() => {
let Some(line) = next else {
(lines, writer) = Self::reconnect(&mut server).await;
continue;
};
// Process line...
}
}
Heartbeat
Server sends periodic heartbeats to detect dead connections:
_ = time::sleep(Duration::from_secs(5)) => {
if writer.write_u8(b'\n').await.is_err() {
break; // Connection dead
}
}
Graceful Shutdown
Clients send bye message before disconnecting:
async fn handle_bye(id: Id, mut rx: UnboundedReceiver<String>, mut writer: ClientWriter) {
// Flush pending messages
while let Ok(payload) = rx.try_recv() {
writer.write_all(payload.as_bytes()).await.ok();
}
// Send bye
let bye = EmberBye::borrowed().with_receiver(id).with_sender(Id(0));
if let Ok(s) = try_format!("{bye}") {
writer.write_all(s.as_bytes()).await.ok();
writer.flush().await.ok();
}
}
Environment Variables
DDS sets environment variables for child processes:
pub fn init() {
// ...
unsafe {
if let Some(s) = std::env::var("YAZI_ID").ok().filter(|s| !s.is_empty()) {
std::env::set_var("YAZI_PID", s); // Parent ID
}
std::env::set_var("YAZI_ID", ID.to_string()); // Current ID
std::env::set_var(
"YAZI_LEVEL",
(std::env::var("YAZI_LEVEL").unwrap_or_default().parse().unwrap_or(0u16) + 1).to_string(),
);
}
}
This enables:
- Nested detection - Know if running inside another Yazi
- Parent tracking - Find parent Yazi instance
- Nesting level - Track recursion depth
Version Compatibility
DDS checks version compatibility:
if version.as_deref() != Some(EmberHi::version()) {
bail!(
"Incompatible version (Ya {}, Yazi {}). Restart all `ya` and `yazi` processes if you upgrade either one.",
EmberHi::version(),
version.as_deref().unwrap_or("Unknown")
);
}
This prevents communication errors between different Yazi versions.
Plugin Examples
Cross-Instance Notification
-- In plugin init
ps.sub_remote("my-plugin", "custom", function(data)
ya.notify({
title = "Remote Event",
content = "Another instance sent: " .. data,
})
end)
-- In another instance
ps.pub("custom", "Hello from instance A!")
Synchronized State
local state = {}
ps.sub_remote("my-plugin", "custom", function(data)
state = data
-- Update UI
end)
function update_state(new_state)
state = new_state
ps.pub("custom", state)
end
File Synchronization
ps.sub_remote("sync-plugin", "cd", function(tab, url)
-- When any instance changes directory, sync others
if should_sync(url) then
ya.manager_emit("cd", { url = tostring(url) })
}
end)
DDS is designed for efficiency:
- Async I/O - Non-blocking communication
- Unix sockets - Fast local IPC
- Binary format - Compact message encoding
- Connection pooling - Reuse connections
- Selective delivery - Only send to interested peers
Debugging
Monitor DDS activity:
# Watch all events
ya sub cd,hover,load,tab,yank,bulk,move,trash,delete
# Send test message
ya pub custom --str "test message"
# Check connected instances
ya pub hey # Triggers peer list update
Security Considerations
- Local only - Unix socket accessible only to same user
- No authentication - Processes under same UID trusted
- No encryption - Data not encrypted (local socket)
See Also