1#[cfg(feature = "embedded-frontend")]
18pub mod embedded_frontend;
19pub mod error;
20pub mod lsp;
21pub mod protocol;
22pub mod routes;
23pub mod rust_analyzer;
24pub mod session;
25pub mod undo;
26pub mod watcher;
27
28use std::net::SocketAddr;
29use std::path::Path;
30use std::sync::Arc;
31use std::sync::atomic::AtomicBool;
32
33use tokio::sync::{Mutex as TokioMutex, RwLock};
34
35pub use error::{ServerError, ServerResult};
36pub use protocol::{ClientMessage, ServerMessage};
37pub use routes::{AppState, create_router};
38pub use session::{NotebookSession, SessionHandle};
39pub use watcher::{FileEvent, FileWatcher};
40
41#[derive(Debug, Clone)]
43pub struct ServerConfig {
44 pub host: String,
46 pub port: u16,
48 pub open_browser: bool,
50}
51
52impl Default for ServerConfig {
53 fn default() -> Self {
54 Self {
55 host: "127.0.0.1".to_string(),
56 port: 3000,
57 open_browser: false,
58 }
59 }
60}
61
62pub async fn serve(notebook_path: impl AsRef<Path>, config: ServerConfig) -> ServerResult<()> {
64 let path = notebook_path.as_ref();
65
66 let interrupted = Arc::new(AtomicBool::new(false));
68
69 let (session, _rx) = NotebookSession::new(path, interrupted.clone())?;
71 let session = Arc::new(RwLock::new(session));
72
73 let state = Arc::new(AppState {
75 session: session.clone(),
76 kill_handle: Arc::new(TokioMutex::new(None)),
77 interrupted,
78 });
79
80 let app = create_router(state);
82
83 let mut watcher = FileWatcher::new(path)?;
85
86 let session_clone = session.clone();
88 tokio::spawn(async move {
89 while let Some(event) = watcher.recv().await {
90 match event {
91 FileEvent::Modified(_) => {
92 tracing::info!("Notebook file changed, reloading...");
93 let mut session = session_clone.write().await;
94 if let Err(e) = session.reload() {
95 tracing::error!("Failed to reload notebook: {}", e);
96 }
97 }
98 FileEvent::Removed(path) => {
99 tracing::warn!("Notebook file removed: {}", path.display());
100 }
101 FileEvent::Created(_) => {}
102 }
103 }
104 });
105
106 let addr: SocketAddr = format!("{}:{}", config.host, config.port)
108 .parse()
109 .map_err(|_| ServerError::Io {
110 path: std::path::PathBuf::new(),
111 message: format!("Invalid address: {}:{}", config.host, config.port),
112 })?;
113
114 tracing::info!("Starting Venus server at http://{}", addr);
115
116 if config.open_browser {
118 tracing::info!("Open http://{} in your browser", addr);
119 }
120
121 let listener = tokio::net::TcpListener::bind(addr).await?;
123 axum::serve(listener, app).await?;
124
125 Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_default_config() {
134 let config = ServerConfig::default();
135 assert_eq!(config.host, "127.0.0.1");
136 assert_eq!(config.port, 3000);
137 assert!(!config.open_browser);
138 }
139}