Skip to main content

graphix_package_tui/
lib.rs

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