1use anyhow::{bail, Context, Result};
2use arcstr::{literal, ArcStr};
3use derive_builder::Builder;
4use enumflags2::BitFlags;
5use graphix_compiler::{
6 expr::{ExprId, ModPath, ModuleResolver},
7 format_with_flags,
8 typ::{TVal, Type},
9 CFlag, ExecCtx, PrintFlag,
10};
11use graphix_rt::{CompExp, CouldNotResolve, 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, 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<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 Tui(Tui<X>),
41 Text(CompExp<X>),
42}
43
44impl<X: GXExt> Output<X> {
45 fn from_expr(gx: &GXHandle<X>, env: &Env<X>, e: CompExp<X>) -> Self {
46 if TUITYP.contains(env, &e.typ).unwrap()
47 && e.typ != Type::Bottom
48 && e.typ != Type::Any
49 {
50 Self::Tui(Tui::start(gx, env.clone(), e))
51 } else {
52 Self::Text(e)
53 }
54 }
55
56 async fn clear(&mut self) {
57 match self {
58 Self::None | Self::Text(_) => (),
59 Self::Tui(tui) => tui.stop().await,
60 }
61 *self = Self::None
62 }
63
64 async fn process_update(&mut self, env: &Env<X>, id: ExprId, v: Value) {
65 match self {
66 Self::None => (),
67 Self::Tui(tui) => tui.update(id, v).await,
68 Self::Text(e) => {
69 if e.id == id {
70 println!("{}", TVal { env: &env, typ: &e.typ, v: &v })
71 }
72 }
73 }
74 }
75}
76
77fn tui_mods() -> ModuleResolver {
78 ModuleResolver::VFS(HashMap::from_iter([
79 (Path::from("/tui"), literal!(include_str!("tui/mod.gx"))),
80 (
81 Path::from("/tui/input_handler"),
82 literal!(include_str!("tui/input_handler.gx")),
83 ),
84 (Path::from("/tui/text"), literal!(include_str!("tui/text.gx"))),
85 (Path::from("/tui/paragraph"), literal!(include_str!("tui/paragraph.gx"))),
86 (Path::from("/tui/block"), literal!(include_str!("tui/block.gx"))),
87 (Path::from("/tui/scrollbar"), literal!(include_str!("tui/scrollbar.gx"))),
88 (Path::from("/tui/layout"), literal!(include_str!("tui/layout.gx"))),
89 (Path::from("/tui/tabs"), literal!(include_str!("tui/tabs.gx"))),
90 (Path::from("/tui/barchart"), literal!(include_str!("tui/barchart.gx"))),
91 (Path::from("/tui/chart"), literal!(include_str!("tui/chart.gx"))),
92 (Path::from("/tui/sparkline"), literal!(include_str!("tui/sparkline.gx"))),
93 (Path::from("/tui/line_gauge"), literal!(include_str!("tui/line_gauge.gx"))),
94 (Path::from("/tui/gauge"), literal!(include_str!("tui/gauge.gx"))),
95 (Path::from("/tui/list"), literal!(include_str!("tui/list.gx"))),
96 (Path::from("/tui/table"), literal!(include_str!("tui/table.gx"))),
97 (Path::from("/tui/calendar"), literal!(include_str!("tui/calendar.gx"))),
98 (Path::from("/tui/canvas"), literal!(include_str!("tui/canvas.gx"))),
99 (Path::from("/tui/browser"), literal!(include_str!("tui/browser.gx"))),
100 ]))
101}
102
103#[derive(Debug, Clone)]
104pub enum Mode {
105 Repl,
110 File(PathBuf),
114 Static(ArcStr),
117}
118
119impl Mode {
120 fn file_mode(&self) -> bool {
121 match self {
122 Self::Repl => false,
123 Self::File(_) | Self::Static(_) => true,
124 }
125 }
126}
127
128#[derive(Builder)]
129#[builder(pattern = "owned")]
130pub struct Shell<X: GXExt> {
131 #[builder(default = "false")]
133 no_init: bool,
134 #[builder(setter(strip_option), default)]
136 publish_timeout: Option<Duration>,
137 #[builder(setter(strip_option), default)]
140 resolve_timeout: Option<Duration>,
141 #[builder(default)]
143 module_resolvers: Vec<ModuleResolver>,
144 #[builder(default = "BitFlags::all()")]
146 stdlib_modules: BitFlags<Module>,
147 #[builder(default = "Mode::Repl")]
149 mode: Mode,
150 publisher: Publisher,
154 subscriber: Subscriber,
158 #[builder(setter(strip_option), default)]
177 register: Option<Arc<dyn Fn(&mut ExecCtx<GXRt<X>, X::UserEvent>) -> ArcStr>>,
178 #[builder(default)]
181 enable_flags: BitFlags<CFlag>,
182 #[builder(default)]
185 disable_flags: BitFlags<CFlag>,
186}
187
188impl<X: GXExt> Shell<X> {
189 async fn init(
190 &mut self,
191 sub: mpsc::Sender<GPooled<Vec<GXEvent<X>>>>,
192 ) -> Result<GXHandle<X>> {
193 let publisher = self.publisher.clone();
194 let subscriber = self.subscriber.clone();
195 let mut ctx = ExecCtx::new(GXRt::<X>::new(publisher, subscriber));
196 let (root, mods) = graphix_stdlib::register(&mut ctx, self.stdlib_modules)?;
197 let usermods = self.register.as_mut().map(|f| f(&mut ctx));
198 let root = match usermods {
199 Some(m) => ArcStr::from(format!("{root};\nmod tui;\n{m}")),
200 None => ArcStr::from(format!("{root};\nmod tui")),
201 };
202 let mut flags = match self.mode {
203 Mode::File(_) | Mode::Static(_) => CFlag::WarnUnhandled | CFlag::WarnUnused,
204 Mode::Repl => BitFlags::empty(),
205 };
206 flags.insert(self.enable_flags);
207 flags.remove(self.disable_flags);
208 let mut mods = vec![mods, tui_mods()];
209 for res in self.module_resolvers.drain(..) {
210 mods.push(res);
211 }
212 let mut gx = GXConfig::builder(ctx, sub);
213 gx = gx.flags(flags);
214 if let Some(s) = self.publish_timeout {
215 gx = gx.publish_timeout(s);
216 }
217 if let Some(s) = self.resolve_timeout {
218 gx = gx.resolve_timeout(s);
219 }
220 Ok(gx
221 .root(root)
222 .resolvers(mods)
223 .build()
224 .context("building rt config")?
225 .start()
226 .await
227 .context("loading initial modules")?)
228 }
229
230 async fn load_env(
231 &mut self,
232 gx: &GXHandle<X>,
233 newenv: &mut Option<Env<X>>,
234 output: &mut Output<X>,
235 exprs: &mut Vec<CompExp<X>>,
236 ) -> Result<Env<X>> {
237 let env;
238 macro_rules! file_mode {
239 ($r:expr) => {{
240 exprs.extend($r.exprs);
241 env = gx.get_env().await?;
242 if let Some(e) = exprs.pop() {
243 *output = Output::from_expr(&gx, &env, e);
244 }
245 *newenv = None
246 }};
247 }
248 match &self.mode {
249 Mode::File(file) => {
250 let r = gx.load(file.clone()).await?;
251 file_mode!(r)
252 }
253 Mode::Static(s) => {
254 let r = gx.compile(s.clone()).await?;
255 file_mode!(r)
256 }
257 Mode::Repl if !self.no_init => match gx.compile("mod init".into()).await {
258 Ok(res) => {
259 env = res.env;
260 exprs.extend(res.exprs);
261 *newenv = Some(env.clone())
262 }
263 Err(e) if e.is::<CouldNotResolve>() => {
264 env = gx.get_env().await?;
265 *newenv = Some(env.clone())
266 }
267 Err(e) => {
268 eprintln!("error in init module: {e:?}");
269 env = gx.get_env().await?;
270 *newenv = Some(env.clone())
271 }
272 },
273 Mode::Repl => {
274 env = gx.get_env().await?;
275 *newenv = Some(env.clone());
276 }
277 }
278 Ok(env)
279 }
280
281 pub async fn run(mut self) -> Result<()> {
282 let (tx, mut from_gx) = mpsc::channel(100);
283 let gx = self.init(tx).await?;
284 let script = self.mode.file_mode();
285 let mut input = InputReader::new();
286 let mut output = Output::None;
287 let mut newenv = None;
288 let mut exprs = vec![];
289 let mut env = self.load_env(&gx, &mut newenv, &mut output, &mut exprs).await?;
290 if !script {
291 println!("Welcome to the graphix shell");
292 println!("Press ctrl-c to cancel, ctrl-d to exit, and tab for help")
293 }
294 loop {
295 select! {
296 batch = from_gx.recv() => match batch {
297 None => bail!("graphix runtime is dead"),
298 Some(mut batch) => {
299 for e in batch.drain(..) {
300 match e {
301 GXEvent::Updated(id, v) => {
302 output.process_update(&env, id, v).await
303 },
304 GXEvent::Env(e) => {
305 env = e;
306 newenv = Some(env.clone());
307 }
308 }
309 }
310 }
311 },
312 input = input.read_line(&mut output, &mut newenv) => {
313 match input {
314 Err(e) => eprintln!("error reading line {e:?}"),
315 Ok(Signal::CtrlC) if script => break Ok(()),
316 Ok(Signal::CtrlC) => output.clear().await,
317 Ok(Signal::CtrlD) => break Ok(()),
318 Ok(Signal::Success(line)) => {
319 match gx.compile(ArcStr::from(line)).await {
320 Err(e) => eprintln!("error: {e:?}"),
321 Ok(res) => {
322 env = res.env;
323 newenv = Some(env.clone());
324 exprs.extend(res.exprs);
325 if exprs.last().map(|e| e.output).unwrap_or(false) {
326 let e = exprs.pop().unwrap();
327 let typ = e.typ
328 .with_deref(|t| t.cloned())
329 .unwrap_or_else(|| e.typ.clone());
330 format_with_flags(
331 PrintFlag::DerefTVars | PrintFlag::ReplacePrims,
332 || println!("-: {}", typ)
333 );
334 output = Output::from_expr(&gx, &env, e);
335 } else {
336 output.clear().await
337 }
338 }
339 }
340 }
341 }
342 },
343 }
344 }
345 }
346}