Skip to main content

graphix_package_tui/
lib.rs

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}