zernel/commands/
notebook.rs

1// Copyright (C) 2026 Dyber, Inc. — Proprietary
2
3//! zernel notebook — Terminal notebook
4
5use anyhow::{Context, Result};
6use clap::Subcommand;
7use std::process::Command;
8
9#[derive(Subcommand)]
10pub enum NotebookCommands {
11    /// Start Jupyter Lab
12    Start {
13        /// Port
14        #[arg(long, default_value = "8888")]
15        port: u16,
16        /// Don't open browser
17        #[arg(long)]
18        no_browser: bool,
19    },
20    /// Open an existing notebook
21    Open {
22        /// Path to .ipynb file
23        path: String,
24    },
25    /// Convert notebook to Python script
26    Convert {
27        /// Input .ipynb file
28        input: String,
29        /// Output format (py, html, pdf, md)
30        #[arg(long, default_value = "py")]
31        to: String,
32    },
33    /// List running notebook servers
34    List,
35    /// Stop a notebook server
36    Stop,
37}
38
39pub async fn run(cmd: NotebookCommands) -> Result<()> {
40    match cmd {
41        NotebookCommands::Start { port, no_browser } => {
42            println!("Starting Jupyter Lab on port {port}...");
43
44            let mut args = vec![
45                "lab".to_string(),
46                format!("--port={port}"),
47                "--ip=0.0.0.0".into(),
48            ];
49            if no_browser {
50                args.push("--no-browser".into());
51            }
52
53            let status = tokio::process::Command::new("jupyter")
54                .args(&args)
55                .status()
56                .await
57                .with_context(|| "jupyter not found — install with: zernel install jupyter")?;
58
59            if !status.success() {
60                anyhow::bail!("Jupyter exited with code {}", status.code().unwrap_or(-1));
61            }
62        }
63
64        NotebookCommands::Open { path } => {
65            println!("Opening {path}...");
66            let status = tokio::process::Command::new("jupyter")
67                .args(["lab", &path])
68                .status()
69                .await
70                .with_context(|| "jupyter not found")?;
71            let _ = status;
72        }
73
74        NotebookCommands::Convert { input, to } => {
75            let output = input.replace(".ipynb", &format!(".{to}"));
76            println!("Converting {input} → {output}");
77
78            let to_format = match to.as_str() {
79                "py" => "script",
80                "html" => "html",
81                "pdf" => "pdf",
82                "md" => "markdown",
83                other => other,
84            };
85
86            let status = Command::new("jupyter")
87                .args(["nbconvert", "--to", to_format, &input])
88                .status()
89                .with_context(|| "jupyter nbconvert not found")?;
90
91            if status.success() {
92                println!("Converted: {output}");
93            } else {
94                println!("Conversion failed.");
95            }
96        }
97
98        NotebookCommands::List => {
99            println!("Running Jupyter Servers:");
100            let output = Command::new("jupyter")
101                .args(["lab", "list"])
102                .output()
103                .with_context(|| "jupyter not found")?;
104            print!("{}", String::from_utf8_lossy(&output.stdout));
105        }
106
107        NotebookCommands::Stop => {
108            println!("Stopping Jupyter servers...");
109            let status = Command::new("jupyter")
110                .args(["lab", "stop"])
111                .status()
112                .with_context(|| "jupyter not found")?;
113            if status.success() {
114                println!("All servers stopped.");
115            }
116        }
117    }
118    Ok(())
119}