mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-30 13:46:47 -06:00
- 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>
68 lines
2.6 KiB
Rust
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;
|
|
}
|
|
}
|
|
}
|