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:
@@ -0,0 +1 @@
|
|||||||
|
/target/
|
||||||
Generated
+3794
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
@@ -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