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