use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::RwLock;
use std::time::{Duration, Instant};
use tungstenite::{accept, Message};
static FIGMA_ZOOM: RwLock<Option<(f64, Instant)>> = RwLock::new(None);
const FRESHNESS: Duration = Duration::from_secs(2);
pub fn current_figma_zoom() -> Option<f64> {
let guard = FIGMA_ZOOM.read().ok()?;
let (zoom, when) = guard.as_ref()?;
if when.elapsed() <= FRESHNESS {
Some(*zoom)
} else {
None
}
}
pub fn spawn(port: u16) {
std::thread::Builder::new()
.name("vernier-figma-bridge".into())
.spawn(move || run(port))
.ok();
}
fn run(port: u16) {
let bind = format!("127.0.0.1:{port}");
let listener = match TcpListener::bind(&bind) {
Ok(l) => l,
Err(e) => {
log::warn!("figma bridge: bind {bind}: {e}");
return;
}
};
log::info!("figma bridge: listening on {bind}");
for stream in listener.incoming() {
match stream {
Ok(s) => {
std::thread::Builder::new()
.name("vernier-figma-conn".into())
.spawn(move || handle(s))
.ok();
}
Err(e) => log::warn!("figma bridge: accept: {e}"),
}
}
}
fn handle(stream: TcpStream) {
let peer = stream
.peer_addr()
.map(|a| a.to_string())
.unwrap_or_else(|_| "?".into());
let mut ws = match accept(stream) {
Ok(w) => w,
Err(e) => {
log::debug!("figma bridge: handshake from {peer}: {e}");
return;
}
};
log::info!("figma bridge: plugin connected ({peer})");
loop {
match ws.read() {
Ok(Message::Text(t)) => parse_and_cache(&t),
Ok(Message::Close(_)) | Err(_) => break,
Ok(_) => {}
}
}
if let Ok(mut g) = FIGMA_ZOOM.write() {
*g = None;
}
log::info!("figma bridge: plugin disconnected ({peer})");
}
pub fn manifest_path() -> Option<PathBuf> {
if let Some(dir) = std::env::var_os("VERNIER_FIGMA_PLUGIN_DIR") {
let p = PathBuf::from(dir).join("manifest.json");
if p.exists() {
return canonicalize(p);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
let portable = parent.join("figma-plugin").join("manifest.json");
if portable.exists() {
return canonicalize(portable);
}
let fhs = parent.join("../share/vernier/figma-plugin/manifest.json");
if fhs.exists() {
return canonicalize(fhs);
}
}
}
#[cfg(debug_assertions)]
{
let dev = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../figma-plugin/manifest.json");
if dev.exists() {
return canonicalize(dev);
}
}
None
}
fn canonicalize(p: PathBuf) -> Option<PathBuf> {
std::fs::canonicalize(&p).ok().or(Some(p))
}
fn parse_and_cache(text: &str) {
let v: serde_json::Value = match serde_json::from_str(text) {
Ok(v) => v,
Err(e) => {
log::debug!("figma bridge: bad json: {e}: {text}");
return;
}
};
let zoom = match v.get("value").and_then(|v| v.as_f64()) {
Some(z) if z > 0.0 && z.is_finite() => z,
_ => return,
};
if let Ok(mut g) = FIGMA_ZOOM.write() {
*g = Some((zoom, Instant::now()));
}
}