Skip to main content

graphix_shell/
lib.rs

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