graphix_shell/
lib.rs

1use anyhow::{anyhow, bail, Context, Result};
2use arcstr::{literal, ArcStr};
3use derive_builder::Builder;
4use enumflags2::BitFlags;
5use graphix_compiler::{
6    expr::{ExprId, ModPath, ModuleResolver},
7    format_with_flags,
8    typ::{TVal, Type},
9    ExecCtx, PrintFlag,
10};
11use graphix_rt::{CompExp, CouldNotResolve, GXConfig, GXEvent, GXExt, GXHandle, GXRt};
12use graphix_stdlib::Module;
13use input::InputReader;
14use netidx::{
15    path::Path,
16    pool::Pooled,
17    publisher::{Publisher, Value},
18    subscriber::Subscriber,
19};
20use reedline::Signal;
21use std::{collections::HashMap, path::PathBuf, sync::LazyLock, time::Duration};
22use tokio::{select, sync::mpsc};
23use triomphe::Arc;
24use tui::Tui;
25
26mod completion;
27mod input;
28mod tui;
29
30type Env<X> = graphix_compiler::env::Env<GXRt<X>, <X as GXExt>::UserEvent>;
31
32const TUITYP: LazyLock<Type> = LazyLock::new(|| Type::Ref {
33    scope: ModPath::root(),
34    name: ModPath::from(["tui", "Tui"]),
35    params: Arc::from_iter([]),
36});
37
38enum Output<X: GXExt> {
39    None,
40    Tui(Tui<X>),
41    Text(CompExp<X>),
42}
43
44impl<X: GXExt> Output<X> {
45    fn from_expr(gx: &GXHandle<X>, env: &Env<X>, e: CompExp<X>) -> Self {
46        if TUITYP.contains(env, &e.typ).unwrap()
47            && e.typ != Type::Bottom
48            && e.typ != Type::Any
49        {
50            Self::Tui(Tui::start(gx, env.clone(), e))
51        } else {
52            Self::Text(e)
53        }
54    }
55
56    async fn clear(&mut self) {
57        match self {
58            Self::None | Self::Text(_) => (),
59            Self::Tui(tui) => tui.stop().await,
60        }
61        *self = Self::None
62    }
63
64    async fn process_update(&mut self, env: &Env<X>, id: ExprId, v: Value) {
65        match self {
66            Self::None => (),
67            Self::Tui(tui) => tui.update(id, v).await,
68            Self::Text(e) => {
69                if e.id == id {
70                    println!("{}", TVal { env: &env, typ: &e.typ, v: &v })
71                }
72            }
73        }
74    }
75}
76
77fn tui_mods() -> ModuleResolver {
78    ModuleResolver::VFS(HashMap::from_iter([
79        (Path::from("/tui"), literal!(include_str!("tui/mod.gx"))),
80        (
81            Path::from("/tui/input_handler"),
82            literal!(include_str!("tui/input_handler.gx")),
83        ),
84        (Path::from("/tui/text"), literal!(include_str!("tui/text.gx"))),
85        (Path::from("/tui/paragraph"), literal!(include_str!("tui/paragraph.gx"))),
86        (Path::from("/tui/block"), literal!(include_str!("tui/block.gx"))),
87        (Path::from("/tui/scrollbar"), literal!(include_str!("tui/scrollbar.gx"))),
88        (Path::from("/tui/layout"), literal!(include_str!("tui/layout.gx"))),
89        (Path::from("/tui/tabs"), literal!(include_str!("tui/tabs.gx"))),
90        (Path::from("/tui/barchart"), literal!(include_str!("tui/barchart.gx"))),
91        (Path::from("/tui/chart"), literal!(include_str!("tui/chart.gx"))),
92        (Path::from("/tui/sparkline"), literal!(include_str!("tui/sparkline.gx"))),
93        (Path::from("/tui/line_gauge"), literal!(include_str!("tui/line_gauge.gx"))),
94        (Path::from("/tui/gauge"), literal!(include_str!("tui/gauge.gx"))),
95        (Path::from("/tui/list"), literal!(include_str!("tui/list.gx"))),
96        (Path::from("/tui/table"), literal!(include_str!("tui/table.gx"))),
97        (Path::from("/tui/calendar"), literal!(include_str!("tui/calendar.gx"))),
98        (Path::from("/tui/canvas"), literal!(include_str!("tui/canvas.gx"))),
99        (Path::from("/tui/browser"), literal!(include_str!("tui/browser.gx"))),
100    ]))
101}
102
103#[derive(Debug, Clone)]
104pub enum Mode {
105    /// Read input line by line from the user and compile/execute it.
106    /// provide completion and print the value of the last expression
107    /// as it executes. Ctrl-C cancel's execution of the last
108    /// expression and Ctrl-D exits the shell.
109    Repl,
110    /// Load compile and execute the specified file. Print the value
111    /// of the last expression in the file to stdout. Ctrl-C exits the
112    /// shell.
113    File(PathBuf),
114    /// Compile and execute the code in the specified string. Besides
115    /// not loading from a file this mode behaves exactly like File.
116    Static(ArcStr),
117}
118
119impl Mode {
120    fn file_mode(&self) -> bool {
121        match self {
122            Self::Repl => false,
123            Self::File(_) | Self::Static(_) => true,
124        }
125    }
126}
127
128#[derive(Builder)]
129#[builder(pattern = "owned")]
130pub struct Shell<X: GXExt> {
131    /// do not run the users init module
132    #[builder(default = "false")]
133    no_init: bool,
134    /// drop subscribers if they don't consume updates after this timeout
135    #[builder(setter(strip_option), default)]
136    publish_timeout: Option<Duration>,
137    /// module resolution from netidx will fail if it can't subscribe
138    /// before this time elapses
139    #[builder(setter(strip_option), default)]
140    resolve_timeout: Option<Duration>,
141    /// define module resolvers to append to the default list
142    #[builder(default)]
143    module_resolvers: Vec<ModuleResolver>,
144    /// enable or disable features of the standard library
145    #[builder(default = "BitFlags::all()")]
146    stdlib_modules: BitFlags<Module>,
147    /// set the shell's mode
148    #[builder(default = "Mode::Repl")]
149    mode: Mode,
150    /// The netidx publisher to use. If you do not wish to use netidx
151    /// you can use netidx::InternalOnly to create an internal netidx
152    /// environment
153    publisher: Publisher,
154    /// The netidx subscriber to use. If you do not wish to use netidx
155    /// you can use netidx::InternalOnly to create an internal netidx
156    /// environment
157    subscriber: Subscriber,
158    /// Provide a closure to register any built-ins you wish to use.
159    ///
160    /// Your closure should register the builtins with the context and return a
161    /// string specifiying any modules you need to load in order to use them.
162    /// For example if you wish to implement a module called m containing
163    /// builtins foo and bar, then you would first implement foo and bar in rust
164    /// and register them with the context. You would add a VFS module resolver
165    /// to the set of resolvers containing prototypes that reference your rust
166    /// builtins. e.g.
167    ///
168    /// ``` ignore
169    /// pub let foo = |x, y| 'foo_builtin;
170    /// pub let bar = |x| 'bar_builtin
171    /// ```
172    ///
173    /// Your VFS resolver would map "/m" -> the above stubs. Your register
174    /// function would then return "mod m\n" to force loading the module at
175    /// startup. Then your user only needs to `use m`
176    #[builder(setter(strip_option), default)]
177    register: Option<Arc<dyn Fn(&mut ExecCtx<GXRt<X>, X::UserEvent>) -> ArcStr>>,
178}
179
180impl<X: GXExt> Shell<X> {
181    async fn init(
182        &mut self,
183        sub: mpsc::Sender<Pooled<Vec<GXEvent<X>>>>,
184    ) -> Result<GXHandle<X>> {
185        let publisher = self.publisher.clone();
186        let subscriber = self.subscriber.clone();
187        let mut ctx = ExecCtx::new(GXRt::<X>::new(publisher, subscriber));
188        let (root, mods) = graphix_stdlib::register(&mut ctx, self.stdlib_modules)?;
189        let usermods = self.register.as_mut().map(|f| f(&mut ctx));
190        let root = match usermods {
191            Some(m) => ArcStr::from(format!("{root};\nmod tui;\n{m}")),
192            None => ArcStr::from(format!("{root};\nmod tui")),
193        };
194        let mut mods = vec![mods, tui_mods()];
195        for res in self.module_resolvers.drain(..) {
196            mods.push(res);
197        }
198        let mut gx = GXConfig::builder(ctx, sub);
199        if let Some(s) = self.publish_timeout {
200            gx = gx.publish_timeout(s);
201        }
202        if let Some(s) = self.resolve_timeout {
203            gx = gx.resolve_timeout(s);
204        }
205        Ok(gx
206            .root(root)
207            .resolvers(mods)
208            .build()
209            .context("building rt config")?
210            .start()
211            .await
212            .context("loading initial modules")?)
213    }
214
215    async fn load_env(
216        &mut self,
217        gx: &GXHandle<X>,
218        newenv: &mut Option<Env<X>>,
219        output: &mut Output<X>,
220        exprs: &mut Vec<CompExp<X>>,
221    ) -> Result<Env<X>> {
222        let env;
223        macro_rules! file_mode {
224            ($r:expr) => {{
225                exprs.extend($r.exprs);
226                env = gx.get_env().await?;
227                if let Some(e) = exprs.pop() {
228                    *output = Output::from_expr(&gx, &env, e);
229                }
230                *newenv = None
231            }};
232        }
233        match &self.mode {
234            Mode::File(file) => {
235                let r = gx.load(file.clone()).await?;
236                file_mode!(r)
237            }
238            Mode::Static(s) => {
239                let r = gx.compile(s.clone()).await?;
240                file_mode!(r)
241            }
242            Mode::Repl if !self.no_init => match gx.compile("mod init".into()).await {
243                Ok(res) => {
244                    env = res.env;
245                    exprs.extend(res.exprs);
246                    *newenv = Some(env.clone())
247                }
248                Err(e) if e.is::<CouldNotResolve>() => {
249                    env = gx.get_env().await?;
250                    *newenv = Some(env.clone())
251                }
252                Err(e) => {
253                    eprintln!("error in init module: {e:?}");
254                    env = gx.get_env().await?;
255                    *newenv = Some(env.clone())
256                }
257            },
258            Mode::Repl => {
259                env = gx.get_env().await?;
260                *newenv = Some(env.clone());
261            }
262        }
263        Ok(env)
264    }
265
266    pub async fn run(mut self) -> Result<()> {
267        let (tx, mut from_gx) = mpsc::channel(100);
268        let gx = self.init(tx).await?;
269        let script = self.mode.file_mode();
270        let mut input = InputReader::new();
271        let mut output = Output::None;
272        let mut newenv = None;
273        let mut exprs = vec![];
274        let mut env = self.load_env(&gx, &mut newenv, &mut output, &mut exprs).await?;
275        let unhandled = env
276            .lookup_bind(&ModPath::root(), &["errors"].into())
277            .ok_or_else(|| anyhow!("no toplevel errors bind found"))?
278            .1
279            .id;
280        let unhandled = gx.compile_ref(unhandled).await?;
281        if !script {
282            println!("Welcome to the graphix shell");
283            println!("Press ctrl-c to cancel, ctrl-d to exit, and tab for help")
284        }
285        loop {
286            select! {
287                batch = from_gx.recv() => match batch {
288                    None => bail!("graphix runtime is dead"),
289                    Some(mut batch) => {
290                        for e in batch.drain(..) {
291                            match e {
292                                GXEvent::Updated(id, v) => {
293                                    if unhandled.id == id {
294                                        eprintln!("unhandled error: {v}")
295                                    }
296                                    output.process_update(&env, id, v).await
297                                },
298                                GXEvent::Env(e) => {
299                                    env = e;
300                                    newenv = Some(env.clone());
301                                }
302                            }
303                        }
304                    }
305                },
306                input = input.read_line(&mut output, &mut newenv) => {
307                    match input {
308                        Err(e) => eprintln!("error reading line {e:?}"),
309                        Ok(Signal::CtrlC) if script => break Ok(()),
310                        Ok(Signal::CtrlC) => output.clear().await,
311                        Ok(Signal::CtrlD) => break Ok(()),
312                        Ok(Signal::Success(line)) => {
313                            match gx.compile(ArcStr::from(line)).await {
314                                Err(e) => eprintln!("error: {e:?}"),
315                                Ok(res) => {
316                                    env = res.env;
317                                    newenv = Some(env.clone());
318                                    exprs.extend(res.exprs);
319                                    if exprs.last().map(|e| e.output).unwrap_or(false) {
320                                        let e = exprs.pop().unwrap();
321                                        let typ = e.typ
322                                            .with_deref(|t| t.cloned())
323                                            .unwrap_or_else(|| e.typ.clone());
324                                        format_with_flags(
325                                            PrintFlag::DerefTVars | PrintFlag::ReplacePrims,
326                                            || println!("-: {}", typ)
327                                        );
328                                        output = Output::from_expr(&gx, &env, e);
329                                    } else {
330                                        output.clear().await
331                                    }
332                                }
333                            }
334                        }
335                    }
336                },
337            }
338        }
339    }
340}