Shared SSH remote playback library for Rust TUI media apps
  • Rust 51.5%
  • Shell 29.7%
  • HTML 18.8%
Find a file
2026-03-09 02:00:03 -06:00
assets *Initially* 2026-03-09 02:00:03 -06:00
src *Initially* 2026-03-09 02:00:03 -06:00
.gitignore *Initially* 2026-03-09 02:00:03 -06:00
Cargo.lock *Initially* 2026-03-09 02:00:03 -06:00
Cargo.toml *Initially* 2026-03-09 02:00:03 -06:00
README.md *Initially* 2026-03-09 02:00:03 -06:00

tui-remote

Shared SSH remote playback library for Rust TUI media apps.


Table of Contents

Prerequisites

  • ssh binary in PATH — for script deployment and emergency process kill
  • python3 on the remote host — for mpv/VLC IPC control (pause, seek, chapters)
  • SSH key-based auth configured in ~/.ssh/config

Installation

[dependencies]
tui-remote = { git = "https://git.kvndl.xyz/kvndl/tui-remote.git" }
use tui_remote::{DaemonConnection, DaemonStatus};

API

Module Exports
ssh SshHostConfig, resolve_ssh_config, authenticate, ssh_worker, ssh_kill, shell_escape
daemon DaemonConnection, DaemonStatus

DaemonConnection

// Connect and start the daemon on the remote host
let mut conn = DaemonConnection::start("pihost", "mpv")?;

// false when another TUI session already holds the playback lock on this host
if !conn.is_primary {
    eprintln!("read-only: another session controls playback");
}
Method Description
start(host, player) Deploy daemon and open persistent SSH channel
play(args) Start remote player with given arguments
stop() Kill remote player
status() Query playback state (DaemonStatus)
getinfo() Get current player arguments if playing
pause() Toggle pause (requires python3 on remote)
seek(secs) Seek ±N seconds (requires python3 on remote)
subtitle_toggle() Toggle subtitle visibility (mpv only)
chapter_prev() Previous chapter (mpv only)
chapter_next() Next chapter (mpv only)
taillog(n) Fetch last N lines from remote player log
quit() Shut down daemon and stop player
send_keepalive() Send SSH keepalive packet — call every ~10s from idle event loop

DaemonStatus

pub enum DaemonStatus {
    Playing,
    Stopped,
    Error(String),
}

Daemon Protocol

The daemon script is deployed to /tmp/tui-daemon.sh on first connection and executed over a persistent SSH channel. It reads newline-delimited commands from stdin and writes responses to stdout. The player process survives SSH disconnects — reconnecting resumes the existing session.

Command Arguments Response
PLAY player args string OK / ERR <msg>
STOP OK
STATUS PLAYING / STOPPED
GETINFO PLAYING <args> / STOPPED
PAUSE OK / ERR <msg>
SEEK seconds (integer) OK / ERR <msg>
SUBTITLE OK / ERR <msg>
CHAPTER_PREV OK / ERR <msg>
CHAPTER_NEXT OK / ERR <msg>
TAILLOG N (default 50) LOG:<line> × N, then LOG_END
QUIT OK

On startup the daemon responds READY (primary controller) or READY_RO (read-only — another session holds the lock).

Multi-Session Behaviour

Multiple TUI instances connecting to the same host share a single player via a lock file on the remote. Only the first connection becomes primary and may issue PLAY, STOP, PAUSE, and other write commands. Subsequent connections are read-only (is_primary = false) and limited to STATUS, GETINFO, and TAILLOG. Write commands from a read-only connection return ERR locked: primary session controls playback.

The lock is released automatically when the primary session exits. If the primary crashes without cleaning up, the stale lock is detected on the next connection attempt and the incoming session takes over.

Example

use tui_remote::{DaemonConnection, DaemonStatus};

fn main() -> anyhow::Result<()> {
    let mut conn = DaemonConnection::start("pihost", "mpv")?;

    if !conn.is_primary {
        // Another TUI session is already playing — observe only
        match conn.status()? {
            DaemonStatus::Playing => println!("remote is playing"),
            _ => {}
        }
        return Ok(());
    }

    conn.play("https://example.com/stream.m3u8 --vo=drm")?;

    match conn.status()? {
        DaemonStatus::Playing => println!("playing"),
        DaemonStatus::Stopped => println!("stopped"),
        DaemonStatus::Error(e) => eprintln!("error: {}", e),
    }

    // In the TUI event loop — keep the connection alive between commands
    conn.send_keepalive()?;

    conn.stop()?;
    Ok(())
}

License

MIT