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 Repl,
107 File(PathBuf),
111 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 #[builder(default = "false")]
129 no_init: bool,
130 #[builder(setter(strip_option), default)]
132 publish_timeout: Option<Duration>,
133 #[builder(setter(strip_option), default)]
136 resolve_timeout: Option<Duration>,
137 #[builder(setter(strip_option), default)]
139 extra_builtin_modules: Option<FxHashMap<Path, ArcStr>>,
140 #[builder(default = "BitFlags::all()")]
142 stdlib_modules: BitFlags<Module>,
143 #[builder(default = "Mode::Repl")]
145 mode: Mode,
146 publisher: Publisher,
150 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}