graphix_shell/
lib.rs

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