//! Minimal local web UI for the query-engine frontend language. use std::io::{self, BufRead, BufReader, Read, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::{Arc, Mutex}; use super::Session; pub fn serve_gui(address: &str) -> io::Result<()> { let listener = TcpListener::bind(address)?; let session = Arc::new(Mutex::new(Session::new())); println!("GUI available at http://{}", address); for stream in listener.incoming() { match stream { Ok(stream) => { let shared = Arc::clone(&session); if let Err(err) = handle_connection(stream, &shared) { eprintln!("gui error: {}", err); } } Err(err) => eprintln!("gui accept error: {}", err), } } Ok(()) } fn handle_connection(mut stream: TcpStream, session: &Arc>) -> io::Result<()> { let mut reader = BufReader::new(stream.try_clone()?); let mut request_line = String::new(); reader.read_line(&mut request_line)?; if request_line.trim().is_empty() { return Ok(()); } let mut parts = request_line.split_whitespace(); let method = parts.next().unwrap_or_default(); let path = parts.next().unwrap_or("/"); let mut content_length = 0usize; loop { let mut header = String::new(); reader.read_line(&mut header)?; if header == "\r\n" || header.is_empty() { break; } if let Some((name, value)) = header.split_once(':') && name.eq_ignore_ascii_case("content-length") { let parsed = value.trim().parse::().ok(); if let Some(length) = parsed { content_length = length; } } } let mut body = vec![0u8; content_length]; if content_length > 0 { reader.read_exact(&mut body)?; } let response = match (method, path) { ("GET", "/") => http_response("200 OK", "text/html; charset=utf-8", INDEX_HTML), ("POST", "/execute") => { let script = String::from_utf8_lossy(&body); let output = { let mut locked = session .lock() .map_err(|_| io::Error::other("session lock poisoned"))?; match locked.execute_script(script.as_ref()) { Ok(output) => output, Err(err) => format!("error: {}", err), } }; http_response("200 OK", "text/plain; charset=utf-8", &output) } ("POST", "/reset") => { let mut locked = session .lock() .map_err(|_| io::Error::other("session lock poisoned"))?; locked.reset(); http_response("200 OK", "text/plain; charset=utf-8", "Session reset.") } _ => http_response("404 Not Found", "text/plain; charset=utf-8", "Not found"), }; stream.write_all(response.as_bytes())?; stream.flush()?; Ok(()) } fn http_response(status: &str, content_type: &str, body: &str) -> String { format!( "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", status, content_type, body.len(), body ) } const INDEX_HTML: &str = r#" query-engine GUI

query-engine

Minimal local workbench for rule-driven query experiments.

Try query Ancestor(?X, ?Y)?, explain Ancestor(alice, carol)?, or boolean queries like query Parent(alice, bob)?.

Session ready.
"#;