agent: headless desktop clipboard agent on tethercore (macOS/Linux/Windows)
A single Rust binary: the shared tethercore engine (SSE + RTC) wired to the OS clipboard via arboard. Inbound room clipboard → local clipboard; local changes → published to the room, with echo suppression. No UI — meant to run as a login/service daemon. One codebase covers all three desktops. tether-agent --server http://host:8765 --room <id> [--source label] Verified on macOS against the live server: RECEIVE (bus → clipboard via pbpaste) and SEND (pbcopy → bus) both confirmed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1
agent/.gitignore
vendored
Normal file
1
agent/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target/
|
||||
3794
agent/Cargo.lock
generated
Normal file
3794
agent/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
agent/Cargo.toml
Normal file
13
agent/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "tether-agent"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Headless tether clipboard agent: shared tethercore engine + native clipboard (arboard). Runs on macOS, Linux, Windows."
|
||||
|
||||
[[bin]]
|
||||
name = "tether-agent"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tethercore = { path = "../core" }
|
||||
arboard = "3"
|
||||
86
agent/src/main.rs
Normal file
86
agent/src/main.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! tether-agent — headless clipboard sync for desktops.
|
||||
//!
|
||||
//! The same shared `tethercore` engine (SSE + RTC) that backs the native apps,
|
||||
//! wired to the OS clipboard via `arboard`. One Rust binary covers macOS, Linux,
|
||||
//! and Windows. No UI; meant to run as a login/service daemon.
|
||||
//!
|
||||
//! tether-agent --server http://host:8765 --room <id> [--source label]
|
||||
//!
|
||||
//! Inbound clipboard from the room is written to the local clipboard; local
|
||||
//! clipboard changes are published to the room. Echo is suppressed by tracking
|
||||
//! the last value we set or sent.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use arboard::Clipboard;
|
||||
use tethercore::{Engine, Message, MessageHandler};
|
||||
|
||||
/// Inbound sink: the engine calls this on its runtime thread; we just queue the
|
||||
/// text for the main loop, which owns the (non-Send) clipboard handle.
|
||||
struct Inbox(Arc<Mutex<Vec<String>>>);
|
||||
|
||||
impl MessageHandler for Inbox {
|
||||
fn on_message(&self, msg: Message) {
|
||||
eprintln!("← [{}] {}", msg.source, preview(&msg.text));
|
||||
self.0.lock().unwrap().push(msg.text);
|
||||
}
|
||||
fn on_status(&self, connected: bool) {
|
||||
eprintln!("• {}", if connected { "connected" } else { "disconnected" });
|
||||
}
|
||||
}
|
||||
|
||||
fn preview(s: &str) -> String {
|
||||
let t = s.replace('\n', " ");
|
||||
if t.chars().count() > 60 {
|
||||
format!("{}…", t.chars().take(60).collect::<String>())
|
||||
} else {
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
fn arg(flag: &str, default: &str) -> String {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
a.iter()
|
||||
.position(|x| x == flag)
|
||||
.and_then(|i| a.get(i + 1))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let server = arg("--server", "http://localhost:8765");
|
||||
let room = arg("--room", "default");
|
||||
let source = arg("--source", "agent");
|
||||
let from = arg("--from", &format!("agent-{source}"));
|
||||
|
||||
eprintln!("tether-agent → {server} room={room} source={source}");
|
||||
|
||||
let inbox = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let engine = Engine::new(server, room, from, source);
|
||||
engine.start(Box::new(Inbox(inbox.clone())));
|
||||
|
||||
let mut clip = Clipboard::new().expect("open system clipboard");
|
||||
let mut last = clip.get_text().unwrap_or_default();
|
||||
|
||||
loop {
|
||||
// Apply inbound: received text → OS clipboard.
|
||||
let pending: Vec<String> = std::mem::take(&mut *inbox.lock().unwrap());
|
||||
for text in pending {
|
||||
if text != last {
|
||||
let _ = clip.set_text(text.clone());
|
||||
last = text;
|
||||
}
|
||||
}
|
||||
// Detect a local change → publish to the room.
|
||||
if let Ok(cur) = clip.get_text() {
|
||||
if !cur.is_empty() && cur != last {
|
||||
last = cur.clone();
|
||||
eprintln!("→ {}", preview(&cur));
|
||||
engine.send(cur);
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user