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.gx"), literal!(include_str!("tui/mod.gx"))),
82 (Path::from("/tui.gxi"), literal!(include_str!("tui/mod.gxi"))),
83 (
84 Path::from("/tui/input_handler.gx"),
85 literal!(include_str!("tui/input_handler.gx")),
86 ),
87 (
88 Path::from("/tui/input_handler.gxi"),
89 literal!(include_str!("tui/input_handler.gxi")),
90 ),
91 (Path::from("/tui/text.gx"), literal!(include_str!("tui/text.gx"))),
92 (Path::from("/tui/text.gxi"), literal!(include_str!("tui/text.gxi"))),
93 (Path::from("/tui/paragraph.gx"), literal!(include_str!("tui/paragraph.gx"))),
94 (Path::from("/tui/paragraph.gxi"), literal!(include_str!("tui/paragraph.gxi"))),
95 (Path::from("/tui/block.gx"), literal!(include_str!("tui/block.gx"))),
96 (Path::from("/tui/block.gxi"), literal!(include_str!("tui/block.gxi"))),
97 (Path::from("/tui/scrollbar.gx"), literal!(include_str!("tui/scrollbar.gx"))),
98 (Path::from("/tui/scrollbar.gxi"), literal!(include_str!("tui/scrollbar.gxi"))),
99 (Path::from("/tui/layout.gx"), literal!(include_str!("tui/layout.gx"))),
100 (Path::from("/tui/layout.gxi"), literal!(include_str!("tui/layout.gxi"))),
101 (Path::from("/tui/tabs.gx"), literal!(include_str!("tui/tabs.gx"))),
102 (Path::from("/tui/tabs.gxi"), literal!(include_str!("tui/tabs.gxi"))),
103 (Path::from("/tui/barchart.gx"), literal!(include_str!("tui/barchart.gx"))),
104 (Path::from("/tui/barchart.gxi"), literal!(include_str!("tui/barchart.gxi"))),
105 (Path::from("/tui/chart.gx"), literal!(include_str!("tui/chart.gx"))),
106 (Path::from("/tui/chart.gxi"), literal!(include_str!("tui/chart.gxi"))),
107 (Path::from("/tui/sparkline.gx"), literal!(include_str!("tui/sparkline.gx"))),
108 (Path::from("/tui/sparkline.gxi"), literal!(include_str!("tui/sparkline.gxi"))),
109 (Path::from("/tui/line_gauge.gx"), literal!(include_str!("tui/line_gauge.gx"))),
110 (Path::from("/tui/line_gauge.gxi"), literal!(include_str!("tui/line_gauge.gxi"))),
111 (Path::from("/tui/gauge.gx"), literal!(include_str!("tui/gauge.gx"))),
112 (Path::from("/tui/gauge.gxi"), literal!(include_str!("tui/gauge.gxi"))),
113 (Path::from("/tui/list.gx"), literal!(include_str!("tui/list.gx"))),
114 (Path::from("/tui/list.gxi"), literal!(include_str!("tui/list.gxi"))),
115 (Path::from("/tui/table.gx"), literal!(include_str!("tui/table.gx"))),
116 (Path::from("/tui/table.gxi"), literal!(include_str!("tui/table.gxi"))),
117 (Path::from("/tui/calendar.gx"), literal!(include_str!("tui/calendar.gx"))),
118 (Path::from("/tui/calendar.gxi"), literal!(include_str!("tui/calendar.gxi"))),
119 (Path::from("/tui/canvas.gx"), literal!(include_str!("tui/canvas.gx"))),
120 (Path::from("/tui/canvas.gxi"), literal!(include_str!("tui/canvas.gxi"))),
121 (Path::from("/tui/browser.gx"), literal!(include_str!("tui/browser.gx"))),
122 (Path::from("/tui/browser.gxi"), literal!(include_str!("tui/browser.gxi"))),
123 ]))
124}
125
126#[derive(Debug, Clone)]
127pub enum Mode {
128 Repl,
133 Script(Source),
137 Check(Source),
139}
140
141impl Mode {
142 fn file_mode(&self) -> bool {
143 match self {
144 Self::Repl => false,
145 Self::Script(_) | Self::Check(_) => true,
146 }
147 }
148}
149
150#[derive(Builder)]
151#[builder(pattern = "owned")]
152pub struct Shell<X: GXExt> {
153 #[builder(default = "false")]
155 no_init: bool,
156 #[builder(setter(strip_option), default)]
158 publish_timeout: Option<Duration>,
159 #[builder(setter(strip_option), default)]
162 resolve_timeout: Option<Duration>,
163 #[builder(default)]
165 module_resolvers: Vec<ModuleResolver>,
166 #[builder(default = "BitFlags::all()")]
168 stdlib_modules: BitFlags<Module>,
169 #[builder(default = "Mode::Repl")]
171 mode: Mode,
172 publisher: Publisher,
176 subscriber: Subscriber,
180 #[builder(setter(strip_option), default)]
199 register: Option<Arc<dyn Fn(&mut ExecCtx<GXRt<X>, X::UserEvent>) -> Result<ArcStr>>>,
200 #[builder(default)]
203 enable_flags: BitFlags<CFlag>,
204 #[builder(default)]
207 disable_flags: BitFlags<CFlag>,
208}
209
210impl<X: GXExt> Shell<X> {
211 async fn init(
212 &mut self,
213 sub: mpsc::Sender<GPooled<Vec<GXEvent<X>>>>,
214 ) -> Result<GXHandle<X>> {
215 let publisher = self.publisher.clone();
216 let subscriber = self.subscriber.clone();
217 let mut ctx = ExecCtx::new(GXRt::<X>::new(publisher, subscriber));
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<X>>,
262 output: &mut Output<X>,
263 exprs: &mut Vec<CompExp<X>>,
264 ) -> Result<Env<X>> {
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}