Skip to main content

newter_compiler/
serve.rs

1//! Canvas IDE server: HTTP + WebSocket for live-reload canvas.
2//!
3//! `cargo run -p newter-compiler -- serve path/to/file.newt`
4//! Opens http://localhost:3333 with the canvas IDE.
5
6use 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/// Shared state across handlers.
24#[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
34/// Compile a .newt source to layout JSON for the given screen (None = first screen).
35/// Payload includes "screens" list so the IDE can show a screen selector.
36/// Accepts state overrides and returns (json_string, effective_state).
37fn 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    // Serialize state for the client
52    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
68/// Find the position of `=` in an assignment expression, skipping `==`, `!=`, `<=`, `>=`.
69fn 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            // Skip ==
75            if i + 1 < len && bytes[i + 1] == b'=' {
76                continue;
77            }
78            // Skip !=, <=, >=
79            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
88/// Evaluate a state action expression (e.g., "count = count + 1; label = \"hi\"").
89/// Returns the updated state map.
90fn 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    // Apply current state overrides
109    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    // Process each semicolon-separated statement
116    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            // Parse and evaluate RHS using a dummy program wrapper
127            let wrapper = format!("let __result = {}; screen __X {{ box {{}} }}", rhs);
128            match crate::parse(&wrapper, None) {
129                Ok(wrapped_program) => {
130                    // The first item should be the let binding — evaluate it
131                    let rhs_ctx = EvalContext {
132                        variables: ctx.variables.clone(),
133                        components: ctx.components.clone(),
134                    };
135                    // Find the let binding and eval its value
136                    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
158/// Start the canvas IDE server.
159pub 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    // File watcher
180    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                            // Preserve current state on file save
200                            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
243/// Serve the canvas IDE HTML.
244async fn index_handler() -> Html<&'static str> {
245    Html(include_str!("canvas/index.html"))
246}
247
248/// Return current source.
249async 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
259/// Return layout JSON. ?screen=Name to get a specific screen (for multi-screen apps).
260async 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
275/// Compile source posted from the editor. Optional JSON body: { "code": "...", "screen": "Name" }.
276async 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    // Code changed — reset state
289    *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
303/// WebSocket handler for live updates.
304async 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    // Send current layout immediately
310    {
311        let json = state.layout_json.read().await;
312        let _ = socket.send(Message::Text(json.clone())).await;
313    }
314    // Send current source + filename
315    {
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                        // Check for state_action message first
347                        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                        // Client sent source code (or JSON { code, screen }) — compile it
377                        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                        // Code changed — reset state
386                        *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}