graphix_shell/
lib.rs

1use anyhow::{bail, Context, Result};
2use arcstr::{literal, ArcStr};
3use derive_builder::Builder;
4use enumflags2::BitFlags;
5use fxhash::FxHashMap;
6use graphix_compiler::{
7    expr::{ExprId, ModPath, ModuleResolver},
8    typ::{format_with_flags, PrintFlag, TVal, Type},
9    ExecCtx, NoUserEvent,
10};
11use graphix_rt::{CompExp, CouldNotResolve, GXConfig, GXCtx, GXHandle, RtEvent};
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 = graphix_compiler::env::Env<GXCtx, NoUserEvent>;
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 {
39    None,
40    Tui(Tui),
41    Text(CompExp),
42}
43
44impl Output {
45    fn from_expr(gx: &GXHandle, env: &Env, e: CompExp) -> Self {
46        if TUITYP.contains(env, &e.typ).unwrap() {
47            Self::Tui(Tui::start(gx, env.clone(), e))
48        } else {
49            Self::Text(e)
50        }
51    }
52
53    async fn clear(&mut self) {
54        match self {
55            Self::None | Self::Text(_) => (),
56            Self::Tui(tui) => tui.stop().await,
57        }
58        *self = Self::None
59    }
60
61    async fn process_update(&mut self, env: &Env, id: ExprId, v: Value) {
62        match self {
63            Self::None => (),
64            Self::Tui(tui) => tui.update(id, v).await,
65            Self::Text(e) => {
66                if e.id == id {
67                    println!("{}", TVal { env: &env, typ: &e.typ, v: &v })
68                }
69            }
70        }
71    }
72}
73
74fn tui_mods() -> ModuleResolver {
75    ModuleResolver::VFS(HashMap::from_iter([
76        (Path::from("/tui"), literal!(include_str!("tui/mod.gx"))),
77        (
78            Path::from("/tui/input_handler"),
79            literal!(include_str!("tui/input_handler.gx")),
80        ),
81        (Path::from("/tui/text"), literal!(include_str!("tui/text.gx"))),
82        (Path::from("/tui/paragraph"), literal!(include_str!("tui/paragraph.gx"))),
83        (Path::from("/tui/block"), literal!(include_str!("tui/block.gx"))),
84        (Path::from("/tui/scrollbar"), literal!(include_str!("tui/scrollbar.gx"))),
85        (Path::from("/tui/layout"), literal!(include_str!("tui/layout.gx"))),
86        (Path::from("/tui/tabs"), literal!(include_str!("tui/tabs.gx"))),
87        (Path::from("/tui/barchart"), literal!(include_str!("tui/barchart.gx"))),
88        (Path::from("/tui/chart"), literal!(include_str!("tui/chart.gx"))),
89        (Path::from("/tui/sparkline"), literal!(include_str!("tui/sparkline.gx"))),
90        (Path::from("/tui/line_gauge"), literal!(include_str!("tui/line_gauge.gx"))),
91        (Path::from("/tui/gauge"), literal!(include_str!("tui/gauge.gx"))),
92        (Path::from("/tui/list"), literal!(include_str!("tui/list.gx"))),
93        (Path::from("/tui/table"), literal!(include_str!("tui/table.gx"))),
94        (Path::from("/tui/calendar"), literal!(include_str!("tui/calendar.gx"))),
95        (Path::from("/tui/canvas"), literal!(include_str!("tui/canvas.gx"))),
96        (Path::from("/tui/browser"), literal!(include_str!("tui/browser.gx"))),
97    ]))
98}
99
100#[derive(Debug, Clone)]
101pub enum Mode {
102    /// Read input line by line from the user and compile/execute it.
103    /// provide completion and print the value of the last expression
104    /// as it executes. Ctrl-C cancel's execution of the last
105    /// expression and Ctrl-D exits the shell.
106    Repl,
107    /// Load compile and execute the specified file. Print the value
108    /// of the last expression in the file to stdout. Ctrl-C exits the
109    /// shell.
110    File(PathBuf),
111    /// Compile and execute the code in the specified string. Besides
112    /// not loading from a file this mode behaves exactly like File.
113    Static(ArcStr),
114}
115
116impl Mode {
117    fn file_mode(&self) -> bool {
118        match self {
119            Self::Repl => false,
120            Self::File(_) | Self::Static(_) => true,
121        }
122    }
123}
124
125#[derive(Builder)]
126pub struct Shell {
127    /// do not run the users init module
128    #[builder(default = "false")]
129    no_init: bool,
130    /// drop subscribers if they don't consume updates after this timeout
131    #[builder(setter(strip_option), default)]
132    publish_timeout: Option<Duration>,
133    /// module resolution from netidx will fail if it can't subscribe
134    /// before this time elapses
135    #[builder(setter(strip_option), default)]
136    resolve_timeout: Option<Duration>,
137    /// define extra loadable modules built into this binary
138    #[builder(setter(strip_option), default)]
139    extra_builtin_modules: Option<FxHashMap<Path, ArcStr>>,
140    /// enable or disable features of the standard library
141    #[builder(default = "BitFlags::all()")]
142    stdlib_modules: BitFlags<Module>,
143    /// set the shell's mode
144    #[builder(default = "Mode::Repl")]
145    mode: Mode,
146    /// The netidx publisher to use. If you do not wish to use netidx
147    /// you can use netidx::InternalOnly to create an internal netidx
148    /// environment
149    publisher: Publisher,
150    /// The netidx subscriber to use. If you do not wish to use netidx
151    /// you can use netidx::InternalOnly to create an internal netidx
152    /// environment
153    subscriber: Subscriber,
154}
155
156impl Shell {
157    async fn init(
158        &mut self,
159        sub: mpsc::Sender<Pooled<Vec<RtEvent>>>,
160    ) -> Result<GXHandle> {
161        let publisher = self.publisher.clone();
162        let subscriber = self.subscriber.clone();
163        let mut ctx = ExecCtx::new(GXCtx::new(publisher, subscriber));
164        let (root, mods) = graphix_stdlib::register(&mut ctx, self.stdlib_modules)?;
165        let root = ArcStr::from(format!("{root};\nmod tui"));
166        let mut mods = vec![mods, tui_mods()];
167        if let Some(v) = self.extra_builtin_modules.take() {
168            mods.push(ModuleResolver::VFS(v));
169        }
170        let mut gx = GXConfig::builder(ctx, sub);
171        if let Some(s) = self.publish_timeout {
172            gx = gx.publish_timeout(s);
173        }
174        if let Some(s) = self.resolve_timeout {
175            gx = gx.resolve_timeout(s);
176        }
177        Ok(gx
178            .root(root)
179            .resolvers(mods)
180            .build()
181            .context("building rt config")?
182            .start()
183            .await
184            .context("loading initial modules")?)
185    }
186
187    async fn load_env(
188        &mut self,
189        gx: &GXHandle,
190        newenv: &mut Option<Env>,
191        output: &mut Output,
192        exprs: &mut Vec<CompExp>,
193    ) -> Result<Env> {
194        let env;
195        macro_rules! file_mode {
196            ($r:expr) => {{
197                exprs.extend($r.exprs);
198                env = gx.get_env().await?;
199                if let Some(e) = exprs.pop() {
200                    *output = Output::from_expr(&gx, &env, e);
201                }
202                *newenv = None
203            }};
204        }
205        match &self.mode {
206            Mode::File(file) => {
207                let r = gx.load(file.clone()).await?;
208                file_mode!(r)
209            }
210            Mode::Static(s) => {
211                let r = gx.compile(s.clone()).await?;
212                file_mode!(r)
213            }
214            Mode::Repl if !self.no_init => match gx.compile("mod init".into()).await {
215                Ok(res) => {
216                    env = res.env;
217                    exprs.extend(res.exprs);
218                    *newenv = Some(env.clone())
219                }
220                Err(e) if e.is::<CouldNotResolve>() => {
221                    env = gx.get_env().await?;
222                    *newenv = Some(env.clone())
223                }
224                Err(e) => {
225                    eprintln!("error in init module: {e:?}");
226                    env = gx.get_env().await?;
227                    *newenv = Some(env.clone())
228                }
229            },
230            Mode::Repl => {
231                env = gx.get_env().await?;
232                *newenv = Some(env.clone());
233            }
234        }
235        Ok(env)
236    }
237
238    pub async fn run(mut self) -> Result<()> {
239        let (tx, mut from_gx) = mpsc::channel(100);
240        let gx = self.init(tx).await?;
241        let script = self.mode.file_mode();
242        let mut input = InputReader::new();
243        let mut output = Output::None;
244        let mut newenv = None;
245        let mut exprs = vec![];
246        let mut env = self.load_env(&gx, &mut newenv, &mut output, &mut exprs).await?;
247        if !script {
248            println!("Welcome to the graphix shell");
249            println!("Press ctrl-c to cancel, ctrl-d to exit, and tab for help")
250        }
251        loop {
252            select! {
253                batch = from_gx.recv() => match batch {
254                    None => bail!("graphix runtime is dead"),
255                    Some(mut batch) => {
256                        for e in batch.drain(..) {
257                            match e {
258                                RtEvent::Updated(id, v) => output.process_update(&env, id, v).await,
259                                RtEvent::Env(e) => {
260                                    env = e;
261                                    newenv = Some(env.clone());
262                                }
263                            }
264                        }
265                    }
266                },
267                input = input.read_line(&mut output, &mut newenv) => {
268                    match input {
269                        Err(e) => eprintln!("error reading line {e:?}"),
270                        Ok(Signal::CtrlC) if script => break Ok(()),
271                        Ok(Signal::CtrlC) => output.clear().await,
272                        Ok(Signal::CtrlD) => break Ok(()),
273                        Ok(Signal::Success(line)) => {
274                            match gx.compile(ArcStr::from(line)).await {
275                                Err(e) => eprintln!("error: {e:?}"),
276                                Ok(res) => {
277                                    env = res.env;
278                                    newenv = Some(env.clone());
279                                    exprs.extend(res.exprs);
280                                    if exprs.last().map(|e| e.output).unwrap_or(false) {
281                                        let e = exprs.pop().unwrap();
282                                        let typ = e.typ
283                                            .with_deref(|t| t.cloned())
284                                            .unwrap_or_else(|| e.typ.clone());
285                                        format_with_flags(
286                                            PrintFlag::DerefTVars | PrintFlag::ReplacePrims,
287                                            || println!("-: {}", typ)
288                                        );
289                                        output = Output::from_expr(&gx, &env, e);
290                                    } else {
291                                        output.clear().await
292                                    }
293                                }
294                            }
295                        }
296                    }
297                },
298            }
299        }
300    }
301}