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}