1#![doc(
2 html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
3 html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
4)]
5use anyhow::{anyhow, bail, Result};
6use arcstr::{literal, ArcStr};
7use async_trait::async_trait;
8use barchart::BarChartW;
9use block::BlockW;
10use calendar::CalendarW;
11use chart::ChartW;
12use crossterm::{
13 event::{
14 DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture,
15 Event, EventStream, KeyCode, KeyModifiers,
16 },
17 terminal, ExecutableCommand,
18};
19use futures::{channel::mpsc, SinkExt, StreamExt};
20use gauge::GaugeW;
21use graphix_compiler::{
22 env::Env,
23 expr::{ExprId, ModPath},
24 typ::Type,
25 BindId,
26};
27use graphix_package::CustomDisplay;
28use graphix_rt::{CompExp, GXExt, GXHandle, TRef};
29use input_handler::{event_to_value, InputHandlerW};
30use layout::LayoutW;
31use line_gauge::LineGaugeW;
32use list::ListW;
33use log::error;
34use netidx::publisher::{FromValue, Value};
35use paragraph::ParagraphW;
36use ratatui::{
37 layout::{Alignment, Direction, Flex, Rect},
38 style::{Color, Modifier, Style},
39 symbols,
40 text::{Line, Span},
41 widgets::TitlePosition,
42 Frame,
43};
44use scrollbar::ScrollbarW;
45use smallvec::SmallVec;
46use sparkline::SparklineW;
47use std::sync::LazyLock;
48use std::{borrow::Cow, future::Future, marker::PhantomData, pin::Pin};
49use text::TextW;
50use tokio::{select, sync::oneshot, task};
51use triomphe::Arc;
52
53mod barchart;
54mod block;
55mod calendar;
56mod canvas;
57mod chart;
58mod gauge;
59mod input_handler;
60mod layout;
61mod line_gauge;
62mod list;
63mod paragraph;
64mod scrollbar;
65mod sparkline;
66mod table;
67mod tabs;
68mod text;
69
70#[derive(Clone, Copy)]
71struct AlignmentV(Alignment);
72
73impl FromValue for AlignmentV {
74 fn from_value(v: Value) -> Result<Self> {
75 match v {
76 Value::String(s) => match &*s {
77 "Left" => Ok(AlignmentV(Alignment::Left)),
78 "Right" => Ok(AlignmentV(Alignment::Right)),
79 "Center" => Ok(AlignmentV(Alignment::Center)),
80 s => bail!("invalid alignment {s}"),
81 },
82 v => bail!("invalid alignment {v}"),
83 }
84 }
85}
86
87#[derive(Clone, Copy)]
88struct ColorV(Color);
89
90impl FromValue for ColorV {
91 fn from_value(v: Value) -> Result<Self> {
92 match v {
93 Value::String(s) => match &*s {
94 "Reset" => Ok(Self(Color::Reset)),
95 "Black" => Ok(Self(Color::Black)),
96 "Red" => Ok(Self(Color::Red)),
97 "Green" => Ok(Self(Color::Green)),
98 "Yellow" => Ok(Self(Color::Yellow)),
99 "Blue" => Ok(Self(Color::Blue)),
100 "Magenta" => Ok(Self(Color::Magenta)),
101 "Cyan" => Ok(Self(Color::Cyan)),
102 "Gray" => Ok(Self(Color::Gray)),
103 "DarkGray" => Ok(Self(Color::DarkGray)),
104 "LightRed" => Ok(Self(Color::LightRed)),
105 "LightGreen" => Ok(Self(Color::LightGreen)),
106 "LightYellow" => Ok(Self(Color::LightYellow)),
107 "LightBlue" => Ok(Self(Color::LightBlue)),
108 "LightMagenta" => Ok(Self(Color::LightMagenta)),
109 "LightCyan" => Ok(Self(Color::LightCyan)),
110 "White" => Ok(Self(Color::White)),
111 s => bail!("invalid color name {s}"),
112 },
113 v => match v.cast_to::<(ArcStr, Value)>()? {
114 (s, v) if &*s == "Rgb" => {
115 let [(_, b), (_, g), (_, r)] = v.cast_to::<[(ArcStr, u8); 3]>()?;
116 Ok(Self(Color::Rgb(r, g, b)))
117 }
118 (s, v) if &*s == "Indexed" => {
119 Ok(Self(Color::Indexed(v.cast_to::<u8>()?)))
120 }
121 (s, v) => bail!("invalid color ({s} {v})"),
122 },
123 }
124 }
125}
126
127#[derive(Clone, Copy)]
128struct ModifierV(Modifier);
129
130impl FromValue for ModifierV {
131 fn from_value(v: Value) -> Result<Self> {
132 let mut m = Modifier::empty();
133 if let Some(o) = v.cast_to::<Option<SmallVec<[ArcStr; 2]>>>()? {
134 for s in o {
135 match &*s {
136 "Bold" => m |= Modifier::BOLD,
137 "Italic" => m |= Modifier::ITALIC,
138 s => bail!("invalid modifier {s}"),
139 }
140 }
141 }
142 Ok(Self(m))
143 }
144}
145
146#[derive(Debug, Clone, Copy)]
147struct StyleV(Style);
148
149impl FromValue for StyleV {
150 fn from_value(v: Value) -> Result<Self> {
151 let [(_, add_modifier), (_, bg), (_, fg), (_, sub_modifier), (_, underline_color)] =
152 v.cast_to::<[(ArcStr, Value); 5]>()?;
153 let add_modifier = add_modifier.cast_to::<ModifierV>()?.0;
154 let bg = bg.cast_to::<Option<ColorV>>()?.map(|c| c.0);
155 let fg = fg.cast_to::<Option<ColorV>>()?.map(|c| c.0);
156 let sub_modifier = sub_modifier.cast_to::<ModifierV>()?.0;
157 let underline_color = underline_color.cast_to::<Option<ColorV>>()?.map(|c| c.0);
158 Ok(Self(Style { fg, bg, underline_color, add_modifier, sub_modifier }))
159 }
160}
161
162struct SpanV(Span<'static>);
163
164impl FromValue for SpanV {
165 fn from_value(v: Value) -> Result<Self> {
166 let [(_, content), (_, style)] = v.cast_to::<[(ArcStr, Value); 2]>()?;
167 Ok(Self(Span {
168 content: Cow::Owned(content.cast_to::<String>()?),
169 style: style.cast_to::<StyleV>()?.0,
170 }))
171 }
172}
173
174#[derive(Debug, Clone)]
175struct LineV(Line<'static>);
176
177impl FromValue for LineV {
178 fn from_value(v: Value) -> Result<Self> {
179 let [(_, alignment), (_, spans), (_, style)] =
180 v.cast_to::<[(ArcStr, Value); 3]>()?;
181 let alignment = alignment.cast_to::<Option<AlignmentV>>()?.map(|a| a.0);
182 let spans = match spans {
183 Value::String(s) => vec![Span::raw(String::from(&*s))],
184 v => v
185 .clone()
186 .cast_to::<Vec<SpanV>>()?
187 .into_iter()
188 .map(|s| s.0)
189 .collect::<Vec<_>>(),
190 };
191 let style = style.cast_to::<StyleV>()?.0;
192 Ok(Self(Line { style, alignment, spans }))
193 }
194}
195
196struct LinesV(Vec<Line<'static>>);
197
198impl FromValue for LinesV {
199 fn from_value(v: Value) -> Result<Self> {
200 match v {
201 Value::String(s) => Ok(Self(vec![Line::raw(String::from(s.as_str()))])),
202 v => Ok(Self(v.cast_to::<Vec<LineV>>()?.into_iter().map(|l| l.0).collect())),
203 }
204 }
205}
206
207#[derive(Clone, Copy)]
208struct FlexV(Flex);
209
210impl FromValue for FlexV {
211 fn from_value(v: Value) -> Result<Self> {
212 let t = match &*v.cast_to::<ArcStr>()? {
213 "Legacy" => Flex::Legacy,
214 "Start" => Flex::Start,
215 "End" => Flex::End,
216 "Center" => Flex::Center,
217 "SpaceBetween" => Flex::SpaceBetween,
218 "SpaceEvenly" => Flex::SpaceEvenly,
219 "SpaceAround" => Flex::SpaceAround,
220 s => bail!("invalid flex {s}"),
221 };
222 Ok(Self(t))
223 }
224}
225
226#[derive(Debug, Clone, Copy)]
227struct ScrollV((u16, u16));
228
229impl FromValue for ScrollV {
230 fn from_value(v: Value) -> Result<Self> {
231 let [(_, x), (_, y)] = v.cast_to::<[(ArcStr, u16); 2]>()?;
232 Ok(Self((y, x)))
233 }
234}
235
236#[derive(Clone, Copy)]
237struct TitlePositionV(TitlePosition);
238
239impl FromValue for TitlePositionV {
240 fn from_value(v: Value) -> Result<Self> {
241 match &*v.cast_to::<ArcStr>()? {
242 "Top" => Ok(Self(TitlePosition::Top)),
243 "Bottom" => Ok(Self(TitlePosition::Bottom)),
244 s => bail!("invalid position {s}"),
245 }
246 }
247}
248
249#[derive(Clone, Copy)]
250struct DirectionV(Direction);
251
252impl FromValue for DirectionV {
253 fn from_value(v: Value) -> Result<Self> {
254 let t = match &*v.cast_to::<ArcStr>()? {
255 "Horizontal" => Direction::Horizontal,
256 "Vertical" => Direction::Vertical,
257 s => bail!("invalid direction tag {s}"),
258 };
259 Ok(Self(t))
260 }
261}
262
263#[derive(Clone)]
264struct HighlightSpacingV(ratatui::widgets::HighlightSpacing);
265
266impl FromValue for HighlightSpacingV {
267 fn from_value(v: Value) -> Result<Self> {
268 match &*v.cast_to::<ArcStr>()? {
269 "Always" => Ok(Self(ratatui::widgets::HighlightSpacing::Always)),
270 "Never" => Ok(Self(ratatui::widgets::HighlightSpacing::Never)),
271 "WhenSelected" => Ok(Self(ratatui::widgets::HighlightSpacing::WhenSelected)),
272 s => bail!("invalid highlight spacing {s}"),
273 }
274 }
275}
276
277#[derive(Clone, Copy, PartialEq, Eq, Default)]
278struct SizeV {
279 width: u16,
280 height: u16,
281}
282
283impl Into<Value> for SizeV {
284 fn into(self) -> Value {
285 [
286 (literal!("height"), (self.height as i64)),
287 (literal!("width"), (self.width as i64)),
288 ]
289 .into()
290 }
291}
292
293impl From<Rect> for SizeV {
294 fn from(r: Rect) -> Self {
295 let s = r.as_size();
296 Self { width: s.width, height: s.height }
297 }
298}
299
300impl SizeV {
301 fn from_terminal() -> Result<Self> {
302 let (width, height) = terminal::size()?;
303 Ok(Self { width, height })
304 }
305}
306
307#[derive(Clone, Copy)]
308struct MarkerV(symbols::Marker);
309
310impl FromValue for MarkerV {
311 fn from_value(v: Value) -> Result<Self> {
312 let m = match &*v.cast_to::<ArcStr>()? {
313 "Dot" => symbols::Marker::Dot,
314 "Block" => symbols::Marker::Block,
315 "Bar" => symbols::Marker::Bar,
316 "Braille" => symbols::Marker::Braille,
317 "HalfBlock" => symbols::Marker::HalfBlock,
318 "Quadrant" => symbols::Marker::Quadrant,
319 "Sextant" => symbols::Marker::Sextant,
320 "Octant" => symbols::Marker::Octant,
321 s => bail!("invalid marker {s}"),
322 };
323 Ok(Self(m))
324 }
325}
326
327fn into_borrowed_line<'a>(line: &'a Line<'static>) -> Line<'a> {
328 let spans = line
329 .spans
330 .iter()
331 .map(|s| {
332 let content = match &s.content {
333 Cow::Owned(s) => Cow::Borrowed(s.as_str()),
334 Cow::Borrowed(s) => Cow::Borrowed(*s),
335 };
336 Span { content, style: s.style }
337 })
338 .collect();
339 Line { alignment: line.alignment, style: line.style, spans }
340}
341
342fn into_borrowed_lines<'a>(lines: &'a [Line<'static>]) -> Vec<Line<'a>> {
343 lines.iter().map(|l| into_borrowed_line(l)).collect::<Vec<_>>()
344}
345
346#[async_trait]
347trait TuiWidget {
348 async fn handle_event(&mut self, e: Event, v: Value) -> Result<()>;
349 async fn handle_update(&mut self, id: ExprId, v: Value) -> Result<()>;
350 fn draw(&mut self, frame: &mut Frame, rect: Rect) -> Result<()>;
351}
352
353type TuiW = Box<dyn TuiWidget + Send + Sync + 'static>;
354type CompRes = Pin<Box<dyn Future<Output = Result<TuiW>> + Send + Sync + 'static>>;
355
356fn compile<X: GXExt>(gx: GXHandle<X>, source: Value) -> CompRes {
357 Box::pin(async move {
358 match source.cast_to::<(ArcStr, Value)>()? {
359 (s, v) if &s == "Text" => TextW::compile(gx, v).await,
360 (s, v) if &s == "Paragraph" => ParagraphW::compile(gx, v).await,
361 (s, v) if &s == "Block" => BlockW::compile(gx, v).await,
362 (s, v) if &s == "Scrollbar" => ScrollbarW::compile(gx, v).await,
363 (s, v) if &s == "Layout" => LayoutW::compile(gx, v).await,
364 (s, v) if &s == "BarChart" => BarChartW::compile(gx, v).await,
365 (s, v) if &s == "Chart" => ChartW::compile(gx, v).await,
366 (s, v) if &s == "Sparkline" => SparklineW::compile(gx, v).await,
367 (s, v) if &s == "LineGauge" => LineGaugeW::compile(gx, v).await,
368 (s, v) if &s == "Calendar" => CalendarW::compile(gx, v).await,
369 (s, v) if &s == "Table" => table::TableW::compile(gx, v).await,
370 (s, v) if &s == "Gauge" => GaugeW::compile(gx, v).await,
371 (s, v) if &s == "List" => ListW::compile(gx, v).await,
372 (s, v) if &s == "Tabs" => tabs::TabsW::compile(gx, v).await,
373 (s, v) if &s == "Canvas" => canvas::CanvasW::compile(gx, v).await,
374 (s, v) if &s == "InputHandler" => InputHandlerW::compile(gx, v).await,
375 (s, v) => bail!("invalid widget type `{s}({v})"),
376 }
377 })
378}
379
380struct EmptyW;
381
382#[async_trait]
383impl TuiWidget for EmptyW {
384 async fn handle_event(&mut self, _e: Event, _v: Value) -> Result<()> {
385 Ok(())
386 }
387
388 async fn handle_update(&mut self, _id: ExprId, _v: Value) -> Result<()> {
389 Ok(())
390 }
391
392 fn draw(&mut self, _frame: &mut Frame, _rect: Rect) -> Result<()> {
393 Ok(())
394 }
395}
396
397enum ToTui {
398 Update(ExprId, Value),
399 Stop(oneshot::Sender<()>),
400}
401
402struct Tui<X: GXExt> {
403 to: mpsc::Sender<ToTui>,
404 ph: PhantomData<X>,
405}
406
407impl<X: GXExt> Tui<X> {
408 fn start(
409 gx: &GXHandle<X>,
410 env: Env,
411 root: CompExp<X>,
412 stop: oneshot::Sender<()>,
413 ) -> Self {
414 let gx = gx.clone();
415 let (to_tx, to_rx) = mpsc::channel(3);
416 task::spawn(async move {
417 if let Err(e) = run(gx, env, root, to_rx, Some(stop)).await {
418 error!("tui::run returned {e:?}")
419 }
420 });
421 Self { to: to_tx, ph: PhantomData }
422 }
423
424 async fn clear(&mut self) {
425 let (tx, rx) = oneshot::channel();
426 let _ = self.to.send(ToTui::Stop(tx)).await;
427 let _ = rx.await;
428 }
429
430 async fn update(&mut self, id: ExprId, v: Value) {
431 if let Err(_) = self.to.send(ToTui::Update(id, v)).await {
432 error!("could not send update because tui task died")
433 }
434 }
435}
436
437fn is_ctrl_c(e: &Event) -> bool {
438 e.as_key_press_event()
439 .map(|e| match e.code {
440 KeyCode::Char('c') if e.modifiers == KeyModifiers::CONTROL.into() => true,
441 _ => false,
442 })
443 .unwrap_or(false)
444}
445
446fn get_id(env: &Env, name: &ModPath) -> Result<BindId> {
447 Ok(env
448 .lookup_bind(&ModPath::root(), name)
449 .ok_or_else(|| anyhow!("could not find {name}"))?
450 .1
451 .id)
452}
453
454fn set_size<X: GXExt>(gx: &GXHandle<X>, id: BindId, size: SizeV) -> Result<()> {
455 gx.set(id, size)
456}
457
458fn set_mouse(enable: bool) {
459 use std::io::stdout;
460 let mut stdout = stdout();
461 if enable {
462 if let Err(e) = stdout.execute(EnableMouseCapture) {
463 error!("could not enable mouse capture {e:?}")
464 }
465 if let Err(e) = stdout.execute(EnableFocusChange) {
466 error!("could not enable focus change {e:?}")
467 }
468 } else {
469 if let Err(e) = stdout.execute(DisableMouseCapture) {
470 error!("could not disable mouse capture {e:?}")
471 }
472 if let Err(e) = stdout.execute(DisableFocusChange) {
473 error!("could not disable mouse capture {e:?}")
474 }
475 }
476}
477
478async fn run<X: GXExt>(
479 gx: GXHandle<X>,
480 env: Env,
481 root_exp: CompExp<X>,
482 mut to_rx: mpsc::Receiver<ToTui>,
483 mut stop: Option<oneshot::Sender<()>>,
484) -> Result<()> {
485 let mut terminal = ratatui::init();
486 let size = get_id(&env, &["tui", "size"].into())?;
487 let event = get_id(&env, &["tui", "event"].into())?;
488 let mut mouse: TRef<X, bool> =
489 TRef::new(gx.compile_ref(get_id(&env, &["tui", "mouse"].into())?).await?)?;
490 if let Some(b) = mouse.t {
491 set_mouse(b)
492 }
493 set_size(&gx, size, SizeV::from_terminal()?)?;
494 let mut events = EventStream::new().fuse();
495 let mut root: TuiW = Box::new(EmptyW);
496 let notify = loop {
497 terminal.draw(|f| {
498 if let Err(e) = root.draw(f, f.area()) {
499 error!("error drawing {e:?}")
500 }
501 })?;
502 select! {
503 m = to_rx.next() => match m {
504 None => break oneshot::channel().0,
505 Some(ToTui::Stop(tx)) => break tx,
506 Some(ToTui::Update(id, v)) => {
507 if let Ok(Some(v)) = mouse.update(id, &v) {
508 set_mouse(*v)
509 }
510 if id == root_exp.id {
511 match compile(gx.clone(), v).await {
512 Err(e) => error!("invalid widget specification {e:?}"),
513 Ok(w) => root = w,
514 }
515 } else {
516 if let Err(e) = root.handle_update(id, v).await {
517 error!("error handling update {e:?}")
518 }
519 }
520 },
521 },
522 e = events.select_next_some() => match e {
523 Ok(e) if is_ctrl_c(&e) => {
524 if let Some(tx) = stop.take() {
525 let _ = tx.send(());
526 }
527 }
528 Ok(e) => {
529 let v = event_to_value(&e);
530 if let Event::Resize(width, height) = e
531 && let Err(e) = set_size(&gx, size, SizeV { width, height }) {
532 error!("could not set the size ref {e:?}")
533 }
534 if let Err(e) = gx.set(event, v.clone()) {
535 error!("could not set event ref {e:?}")
536 }
537 if let Err(e) = root.handle_event(e, v).await {
538 error!("error handling event {e:?}")
539 }
540 },
541 Err(e) => {
542 error!("error reading event from terminal {e:?}");
543 break oneshot::channel().0
544 }
545 }
546 }
547 };
548 if let Some(true) = mouse.t {
549 set_mouse(false)
550 }
551 ratatui::restore();
552 let _ = notify.send(());
553 Ok(())
554}
555
556static TUITYP: LazyLock<Type> = LazyLock::new(|| Type::Ref {
557 scope: ModPath::root(),
558 name: ModPath::from(["tui", "Tui"]),
559 params: Arc::from_iter([]),
560});
561
562#[async_trait]
563impl<X: GXExt> CustomDisplay<X> for Tui<X> {
564 async fn clear(&mut self) {
565 self.clear().await;
566 }
567
568 async fn process_update(&mut self, _env: &Env, id: ExprId, v: Value) {
569 self.update(id, v).await;
570 }
571}
572
573graphix_derive::defpackage! {
574 builtins => [],
575 is_custom => |gx, env, e| {
576 if let Some(typ) = e.typ.with_deref(|t| t.cloned())
577 && typ != Type::Bottom
578 && typ != Type::Any
579 {
580 TUITYP.contains(env, &typ).unwrap_or(false)
581 } else {
582 false
583 }
584 },
585 init_custom => |gx, env, stop, e, _run_on_main| {
586 Ok(Box::new(Tui::<X>::start(gx, env.clone(), e, stop)))
587 },
588}