Files
gridcraft/apps/sheetcraft/src/control_server.rs
T
Brandon ThomasandClaude Opus 5.5 beb353943e M1: Excel-style egui UI, desktop app, chart renderer, MCP server, CLI, release packaging
- ui-egui: title bar with QAT, full ribbon (Home/Insert/Draw/Page Layout/Formulas/Data/
  Review/View/Automate + Table/Chart Design), formula bar with Name Box, virtualised grid
  (frozen panes, overflow, borders, CF data bars/scales/icons, tables, sparklines, filter
  buttons, charts/pictures/shapes), in-cell editor with point mode, reference colouring,
  autocomplete and argument hints, sheet tabs, status bar, dialogs, control channel
- apps/sheetcraft: desktop app (control server, file dialogs, prefs, drag & drop)
- chart: toolkit-free chart layout/rendering for 25 chart kinds + sparklines + rasteriser
- mcp + sheetcraft-cli: MCP server (headless or bridged) and CLI (info/convert/eval/cat/run/mcp)
- xtask (ci/layers/assets/wasm/version/parity), release workflows, packaging, placeholder icon
- automatic row heights

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:00:15 -04:00

68 lines
2.6 KiB
Rust

//! Localhost JSON-lines control server. Each connection may send many requests; each line
//! `{"id":…, "method":…, "params":…}` gets one reply line `{"id":…, "ok":…, …}`.
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use serde_json::{Value as Json, json};
use sheetcraft_ui_egui::ControlRequest;
const MAX_LINE: usize = 64 * 1024 * 1024;
pub fn start(port: u16, ctx: egui::Context) -> std::io::Result<Receiver<ControlRequest>> {
let listener = TcpListener::bind(("127.0.0.1", port))?;
let (tx, rx) = channel();
std::thread::Builder::new().name("control".into()).spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
let ctx = ctx.clone();
let _ = std::thread::Builder::new().name("control-conn".into()).spawn(move || serve(stream, tx, ctx));
}
})?;
eprintln!("SheetCraft control channel listening on 127.0.0.1:{port}");
Ok(rx)
}
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
let Ok(read) = stream.try_clone() else { return };
let mut out = stream;
let reader = BufReader::new(read);
for line in reader.lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
if line.len() > MAX_LINE {
let _ = writeln!(out, "{}", json!({"ok": false, "error": "request too large"}));
continue;
}
let msg: Json = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
let _ = writeln!(out, "{}", json!({"ok": false, "error": format!("invalid JSON: {e}")}));
continue;
}
};
let id = msg.get("id").cloned().unwrap_or(Json::Null);
let method = msg.get("method").and_then(Json::as_str).unwrap_or("").to_string();
let params = msg.get("params").cloned().unwrap_or(json!({}));
let (req, rx) = ControlRequest::new(method, params);
if tx.send(req).is_err() {
break;
}
// Wake the UI thread: egui only runs frames when something happens.
ctx.request_repaint();
let mut reply = rx
.recv_timeout(Duration::from_secs(60))
.unwrap_or_else(|_| json!({"ok": false, "error": "timed out waiting for the UI (is the window minimised?)"}));
if let Some(o) = reply.as_object_mut() {
o.insert("id".into(), id);
}
if writeln!(out, "{reply}").is_err() {
break;
}
}
}