1use crate::{compile_with_state, get_screen, screen_names, value_to_json, Source};
7use crate::value::{eval_expr, EvalContext, Value};
8use axum::{
9 extract::ws::{Message, WebSocket, WebSocketUpgrade},
10 extract::{Query, State},
11 response::{Html, IntoResponse},
12 routing::get,
13 Router,
14};
15use serde::Deserialize;
16use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind};
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::sync::Arc;
20use tokio::sync::{broadcast, RwLock};
21use tower_http::cors::CorsLayer;
22
23#[derive(Clone)]
25pub struct AppState {
26 pub file_path: PathBuf,
27 pub source: Arc<RwLock<String>>,
28 pub layout_json: Arc<RwLock<String>>,
29 pub default_screen: Option<String>,
30 pub tx: broadcast::Sender<String>,
31 pub current_state: Arc<std::sync::Mutex<HashMap<String, Value>>>,
32}
33
34fn compile_to_json(
38 source: &str,
39 path: Option<&str>,
40 screen_name: Option<&str>,
41 state_overrides: &HashMap<String, Value>,
42) -> Result<(String, HashMap<String, Value>), String> {
43 let path_obj = path.map(std::path::Path::new);
44 let (program, layout, effective_state) = compile_with_state(source, path_obj, screen_name, state_overrides).map_err(|e| {
45 let src = Source::new(source.to_string(), path.map(String::from));
46 crate::format_error(&src, &e)
47 })?;
48 let screens = screen_names(&program);
49 let screen = get_screen(&program, screen_name).expect("compile guarantees a screen");
50
51 let state_json: serde_json::Map<String, serde_json::Value> = effective_state
53 .iter()
54 .map(|(k, v)| (k.clone(), value_to_json(v)))
55 .collect();
56
57 let payload = serde_json::json!({
58 "type": "layout",
59 "screens": screens,
60 "screen": screen.name,
61 "viewport": { "w": crate::DEFAULT_VIEWPORT_W, "h": crate::DEFAULT_VIEWPORT_H },
62 "root": layout,
63 "state": state_json,
64 });
65 Ok((serde_json::to_string(&payload).unwrap(), effective_state))
66}
67
68fn find_assignment_split(expr: &str) -> Option<usize> {
70 let bytes = expr.as_bytes();
71 let len = bytes.len();
72 for i in 0..len {
73 if bytes[i] == b'=' {
74 if i + 1 < len && bytes[i + 1] == b'=' {
76 continue;
77 }
78 if i > 0 && (bytes[i - 1] == b'!' || bytes[i - 1] == b'<' || bytes[i - 1] == b'>') {
80 continue;
81 }
82 return Some(i);
83 }
84 }
85 None
86}
87
88fn evaluate_state_action(
91 source: &str,
92 path: Option<&str>,
93 current_state: &HashMap<String, Value>,
94 action_expr: &str,
95) -> Result<HashMap<String, Value>, String> {
96 let path_obj = path.map(std::path::Path::new);
97 let trimmed = source.trim();
98 let program = crate::parse(trimmed, path.map(|s| s)).map_err(|e| format!("{:?}", e))?;
99 let program = if let Some(p) = path_obj {
100 let base = p.parent().unwrap_or_else(|| std::path::Path::new("."));
101 crate::resolve_imports(program, base).map_err(|e| format!("{:?}", e))?
102 } else {
103 program
104 };
105
106 let mut ctx = EvalContext::from_program(&program);
107
108 for (name, val) in current_state {
110 ctx.variables.insert(name.clone(), val.clone());
111 }
112
113 let mut new_state = current_state.clone();
114
115 for stmt in action_expr.split(';') {
117 let stmt = stmt.trim();
118 if stmt.is_empty() {
119 continue;
120 }
121
122 if let Some(eq_pos) = find_assignment_split(stmt) {
123 let var_name = stmt[..eq_pos].trim().to_string();
124 let rhs = stmt[eq_pos + 1..].trim();
125
126 let wrapper = format!("let __result = {}; screen __X {{ box {{}} }}", rhs);
128 match crate::parse(&wrapper, None) {
129 Ok(wrapped_program) => {
130 let rhs_ctx = EvalContext {
132 variables: ctx.variables.clone(),
133 components: ctx.components.clone(),
134 };
135 for item in &wrapped_program.items {
137 if let crate::ProgramItem::Variable(v) = item {
138 if v.name == "__result" {
139 if let Ok(val) = eval_expr(&rhs_ctx, &v.value) {
140 new_state.insert(var_name.clone(), val.clone());
141 ctx.variables.insert(var_name.clone(), val);
142 }
143 break;
144 }
145 }
146 }
147 }
148 Err(e) => {
149 eprintln!("state_action parse error for '{}': {:?}", stmt, e);
150 }
151 }
152 }
153 }
154
155 Ok(new_state)
156}
157
158pub async fn serve(file_path: PathBuf, port: u16, host: String, screen_name: Option<String>) -> anyhow::Result<()> {
160 let source_code = std::fs::read_to_string(&file_path)?;
161 let path_str = file_path.to_str().map(|s| s.to_string());
162
163 let (layout_json, initial_state) = match compile_to_json(&source_code, path_str.as_deref(), screen_name.as_deref(), &HashMap::new()) {
164 Ok((j, s)) => (j, s),
165 Err(e) => (serde_json::json!({ "type": "error", "message": e }).to_string(), HashMap::new()),
166 };
167
168 let (tx, _rx) = broadcast::channel::<String>(64);
169
170 let state = AppState {
171 file_path: file_path.clone(),
172 source: Arc::new(RwLock::new(source_code)),
173 layout_json: Arc::new(RwLock::new(layout_json)),
174 default_screen: screen_name,
175 tx: tx.clone(),
176 current_state: Arc::new(std::sync::Mutex::new(initial_state)),
177 };
178
179 let watch_state = state.clone();
181 let watch_path = file_path.clone();
182 tokio::task::spawn_blocking(move || {
183 let rt = tokio::runtime::Handle::current();
184 let (notify_tx, notify_rx) = std::sync::mpsc::channel::<notify::Result<Event>>();
185 let mut watcher = RecommendedWatcher::new(
186 move |res| { let _ = notify_tx.send(res); },
187 notify::Config::default(),
188 ).expect("watcher");
189 watcher.watch(watch_path.as_ref(), RecursiveMode::NonRecursive).expect("watch");
190
191 loop {
192 match notify_rx.recv() {
193 Ok(Ok(event)) => {
194 if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
195 std::thread::sleep(std::time::Duration::from_millis(50));
196 if let Ok(new_source) = std::fs::read_to_string(&watch_state.file_path) {
197 let path_str = watch_state.file_path.to_str().map(|s| s.to_string());
198 let screen = watch_state.default_screen.as_deref();
199 let overrides = watch_state.current_state.lock().unwrap().clone();
201 let json = match compile_to_json(&new_source, path_str.as_deref(), screen, &overrides) {
202 Ok((j, new_state)) => {
203 *watch_state.current_state.lock().unwrap() = new_state;
204 j
205 }
206 Err(e) => serde_json::json!({ "type": "error", "message": e }).to_string(),
207 };
208 rt.block_on(async {
209 *watch_state.source.write().await = new_source;
210 *watch_state.layout_json.write().await = json.clone();
211 });
212 let _ = watch_state.tx.send(json);
213 }
214 }
215 }
216 Ok(Err(e)) => eprintln!("watch error: {:?}", e),
217 Err(_) => break,
218 }
219 }
220 });
221
222 let app = Router::new()
223 .route("/", get(index_handler))
224 .route("/ws", get(ws_handler))
225 .route("/api/source", get(source_handler))
226 .route("/api/layout", get(layout_handler))
227 .route("/api/compile", axum::routing::post(compile_handler))
228 .layer(CorsLayer::permissive())
229 .with_state(state);
230
231 let addr = format!("{}:{}", host, port);
232 println!("Newt Canvas IDE running at http://localhost:{}", port);
233 println!(" File: {}", file_path.display());
234 println!(" Press Ctrl+C to stop.\n");
235
236 let _ = open::that(format!("http://localhost:{}", port));
237
238 let listener = tokio::net::TcpListener::bind(&addr).await?;
239 axum::serve(listener, app).await?;
240 Ok(())
241}
242
243async fn index_handler() -> Html<&'static str> {
245 Html(include_str!("canvas/index.html"))
246}
247
248async fn source_handler(State(state): State<AppState>) -> impl IntoResponse {
250 let src = state.source.read().await;
251 src.clone()
252}
253
254#[derive(Deserialize)]
255struct LayoutQuery {
256 screen: Option<String>,
257}
258
259async fn layout_handler(State(state): State<AppState>, Query(q): Query<LayoutQuery>) -> impl IntoResponse {
261 let json = if let Some(ref name) = q.screen {
262 let src = state.source.read().await;
263 let path_str = state.file_path.to_str().map(|s| s.to_string());
264 let overrides = state.current_state.lock().unwrap().clone();
265 match compile_to_json(&src, path_str.as_deref(), Some(name), &overrides) {
266 Ok((j, _)) => j,
267 Err(e) => serde_json::json!({ "type": "error", "message": e }).to_string(),
268 }
269 } else {
270 state.layout_json.read().await.clone()
271 };
272 ([("content-type", "application/json")], json)
273}
274
275async fn compile_handler(
277 State(state): State<AppState>,
278 body: String,
279) -> impl IntoResponse {
280 let path_str = state.file_path.to_str().map(|s| s.to_string());
281 let (code, screen) = if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
282 let code = v.get("code").and_then(|c| c.as_str()).unwrap_or(body.as_str()).to_string();
283 let screen = v.get("screen").and_then(|s| s.as_str()).map(String::from);
284 (code, screen.or(state.default_screen.clone()))
285 } else {
286 (body.clone(), state.default_screen.clone())
287 };
288 *state.current_state.lock().unwrap() = HashMap::new();
290 let json = match compile_to_json(&code, path_str.as_deref(), screen.as_deref(), &HashMap::new()) {
291 Ok((j, new_state)) => {
292 *state.current_state.lock().unwrap() = new_state;
293 j
294 }
295 Err(e) => serde_json::json!({ "type": "error", "message": e }).to_string(),
296 };
297 *state.source.write().await = code;
298 *state.layout_json.write().await = json.clone();
299 let _ = state.tx.send(json.clone());
300 ([("content-type", "application/json")], json)
301}
302
303async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
305 ws.on_upgrade(move |socket| handle_ws(socket, state))
306}
307
308async fn handle_ws(mut socket: WebSocket, state: AppState) {
309 {
311 let json = state.layout_json.read().await;
312 let _ = socket.send(Message::Text(json.clone())).await;
313 }
314 {
316 let src = state.source.read().await;
317 let filename = state.file_path.file_name()
318 .and_then(|f| f.to_str())
319 .unwrap_or("untitled.newt");
320 let msg = serde_json::json!({
321 "type": "source",
322 "code": *src,
323 "filename": filename,
324 "path": state.file_path.to_str().unwrap_or(""),
325 });
326 let _ = socket.send(Message::Text(msg.to_string())).await;
327 }
328
329 let mut rx = state.tx.subscribe();
330
331 loop {
332 tokio::select! {
333 msg = rx.recv() => {
334 match msg {
335 Ok(json) => {
336 if socket.send(Message::Text(json)).await.is_err() {
337 break;
338 }
339 }
340 Err(_) => break,
341 }
342 }
343 msg = socket.recv() => {
344 match msg {
345 Some(Ok(Message::Text(text))) => {
346 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
348 if v.get("type").and_then(|t| t.as_str()) == Some("state_action") {
349 if let Some(expr) = v.get("expr").and_then(|e| e.as_str()) {
350 let path_str = state.file_path.to_str().map(|s| s.to_string());
351 let src = state.source.read().await.clone();
352 let cur_state = state.current_state.lock().unwrap().clone();
353 match evaluate_state_action(&src, path_str.as_deref(), &cur_state, expr) {
354 Ok(new_state) => {
355 *state.current_state.lock().unwrap() = new_state.clone();
356 let screen = state.default_screen.as_deref();
357 let json = match compile_to_json(&src, path_str.as_deref(), screen, &new_state) {
358 Ok((j, effective)) => {
359 *state.current_state.lock().unwrap() = effective;
360 j
361 }
362 Err(e) => serde_json::json!({ "type": "error", "message": e }).to_string(),
363 };
364 *state.layout_json.write().await = json.clone();
365 let _ = state.tx.send(json);
366 }
367 Err(e) => {
368 eprintln!("state_action error: {}", e);
369 }
370 }
371 }
372 continue;
373 }
374 }
375
376 let path_str = state.file_path.to_str().map(|s| s.to_string());
378 let (code, screen) = if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
379 let code = v.get("code").and_then(|c| c.as_str()).unwrap_or(text.as_str()).to_string();
380 let screen = v.get("screen").and_then(|s| s.as_str()).map(String::from);
381 (code, screen.or(state.default_screen.clone()))
382 } else {
383 (text.clone(), state.default_screen.clone())
384 };
385 *state.current_state.lock().unwrap() = HashMap::new();
387 let json = match compile_to_json(&code, path_str.as_deref(), screen.as_deref(), &HashMap::new()) {
388 Ok((j, new_state)) => {
389 *state.current_state.lock().unwrap() = new_state;
390 j
391 }
392 Err(e) => serde_json::json!({ "type": "error", "message": e }).to_string(),
393 };
394 *state.source.write().await = code;
395 *state.layout_json.write().await = json.clone();
396 let _ = state.tx.send(json);
397 }
398 Some(Ok(Message::Close(_))) | None => break,
399 _ => {}
400 }
401 }
402 }
403 }
404}