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 Repl,
113 File(PathBuf),
117 Static(ArcStr),
120 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 #[builder(default = "false")]
138 no_init: bool,
139 #[builder(setter(strip_option), default)]
141 publish_timeout: Option<Duration>,
142 #[builder(setter(strip_option), default)]
145 resolve_timeout: Option<Duration>,
146 #[builder(default)]
148 module_resolvers: Vec<ModuleResolver>,
149 #[builder(default = "BitFlags::all()")]
151 stdlib_modules: BitFlags<Module>,
152 #[builder(default = "Mode::Repl")]
154 mode: Mode,
155 publisher: Publisher,
159 subscriber: Subscriber,
163 #[builder(setter(strip_option), default)]
182 register: Option<Arc<dyn Fn(&mut ExecCtx<GXRt<X>, X::UserEvent>) -> Result<ArcStr>>>,
183 #[builder(default)]
186 enable_flags: BitFlags<CFlag>,
187 #[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}