#![doc(
html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
)]
use anyhow::{anyhow, bail, Result};
use arcstr::{literal, ArcStr};
use async_trait::async_trait;
use barchart::BarChartW;
use block::BlockW;
use calendar::CalendarW;
use chart::ChartW;
use crossterm::{
event::{
DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture,
Event, EventStream, KeyCode, KeyModifiers,
},
terminal, ExecutableCommand,
};
use futures::{channel::mpsc, SinkExt, StreamExt};
use gauge::GaugeW;
use graphix_compiler::{
env::Env,
expr::{ExprId, ModPath},
typ::{Type, TypeRef},
BindId,
};
use graphix_package::CustomDisplay;
use graphix_rt::{CompExp, GXExt, GXHandle, TRef};
use input_handler::{event_to_value, InputHandlerW};
use layout::LayoutW;
use line_gauge::LineGaugeW;
use list::ListW;
use log::error;
use netidx::publisher::{FromValue, Value};
use paragraph::ParagraphW;
use ratatui::{
layout::{Alignment, Direction, Flex, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::TitlePosition,
Frame,
};
use scrollbar::ScrollbarW;
use smallvec::SmallVec;
use sparkline::SparklineW;
use std::sync::LazyLock;
use std::{borrow::Cow, future::Future, marker::PhantomData, pin::Pin};
use text::TextW;
use tokio::{select, sync::oneshot, task};
use triomphe::Arc;
mod barchart;
mod block;
mod calendar;
mod canvas;
mod chart;
mod gauge;
mod input_handler;
mod layout;
mod line_gauge;
mod list;
mod paragraph;
mod scrollbar;
mod sparkline;
mod table;
mod tabs;
mod text;
mod validate;
#[cfg(test)]
mod test;
#[derive(Clone, Copy)]
struct AlignmentV(Alignment);
impl FromValue for AlignmentV {
fn from_value(v: Value) -> Result<Self> {
match v {
Value::String(s) => match &*s {
"Left" => Ok(AlignmentV(Alignment::Left)),
"Right" => Ok(AlignmentV(Alignment::Right)),
"Center" => Ok(AlignmentV(Alignment::Center)),
s => bail!("invalid alignment {s}"),
},
v => bail!("invalid alignment {v}"),
}
}
}
#[derive(Clone, Copy)]
struct ColorV(Color);
impl FromValue for ColorV {
fn from_value(v: Value) -> Result<Self> {
match v {
Value::String(s) => match &*s {
"Reset" => Ok(Self(Color::Reset)),
"Black" => Ok(Self(Color::Black)),
"Red" => Ok(Self(Color::Red)),
"Green" => Ok(Self(Color::Green)),
"Yellow" => Ok(Self(Color::Yellow)),
"Blue" => Ok(Self(Color::Blue)),
"Magenta" => Ok(Self(Color::Magenta)),
"Cyan" => Ok(Self(Color::Cyan)),
"Gray" => Ok(Self(Color::Gray)),
"DarkGray" => Ok(Self(Color::DarkGray)),
"LightRed" => Ok(Self(Color::LightRed)),
"LightGreen" => Ok(Self(Color::LightGreen)),
"LightYellow" => Ok(Self(Color::LightYellow)),
"LightBlue" => Ok(Self(Color::LightBlue)),
"LightMagenta" => Ok(Self(Color::LightMagenta)),
"LightCyan" => Ok(Self(Color::LightCyan)),
"White" => Ok(Self(Color::White)),
s => bail!("invalid color name {s}"),
},
v => match v.cast_to::<(ArcStr, Value)>()? {
(s, v) if &*s == "Rgb" => {
let [(_, b), (_, g), (_, r)] = v.cast_to::<[(ArcStr, u8); 3]>()?;
Ok(Self(Color::Rgb(r, g, b)))
}
(s, v) if &*s == "Indexed" => {
Ok(Self(Color::Indexed(v.cast_to::<u8>()?)))
}
(s, v) => bail!("invalid color ({s} {v})"),
},
}
}
}
#[derive(Clone, Copy)]
struct ModifierV(Modifier);
impl FromValue for ModifierV {
fn from_value(v: Value) -> Result<Self> {
let mut m = Modifier::empty();
if let Some(o) = v.cast_to::<Option<SmallVec<[ArcStr; 2]>>>()? {
for s in o {
match &*s {
"Bold" => m |= Modifier::BOLD,
"Italic" => m |= Modifier::ITALIC,
s => bail!("invalid modifier {s}"),
}
}
}
Ok(Self(m))
}
}
#[derive(Debug, Clone, Copy)]
struct StyleV(Style);
impl FromValue for StyleV {
fn from_value(v: Value) -> Result<Self> {
let [(_, add_modifier), (_, bg), (_, fg), (_, sub_modifier), (_, underline_color)] =
v.cast_to::<[(ArcStr, Value); 5]>()?;
let add_modifier = add_modifier.cast_to::<ModifierV>()?.0;
let bg = bg.cast_to::<Option<ColorV>>()?.map(|c| c.0);
let fg = fg.cast_to::<Option<ColorV>>()?.map(|c| c.0);
let sub_modifier = sub_modifier.cast_to::<ModifierV>()?.0;
let underline_color = underline_color.cast_to::<Option<ColorV>>()?.map(|c| c.0);
Ok(Self(Style { fg, bg, underline_color, add_modifier, sub_modifier }))
}
}
struct SpanV(Span<'static>);
impl FromValue for SpanV {
fn from_value(v: Value) -> Result<Self> {
let [(_, content), (_, style)] = v.cast_to::<[(ArcStr, Value); 2]>()?;
Ok(Self(Span {
content: Cow::Owned(content.cast_to::<String>()?),
style: style.cast_to::<StyleV>()?.0,
}))
}
}
#[derive(Debug, Clone)]
struct LineV(Line<'static>);
impl FromValue for LineV {
fn from_value(v: Value) -> Result<Self> {
let [(_, alignment), (_, spans), (_, style)] =
v.cast_to::<[(ArcStr, Value); 3]>()?;
let alignment = alignment.cast_to::<Option<AlignmentV>>()?.map(|a| a.0);
let spans = match spans {
Value::String(s) => vec![Span::raw(String::from(&*s))],
v => v
.clone()
.cast_to::<Vec<SpanV>>()?
.into_iter()
.map(|s| s.0)
.collect::<Vec<_>>(),
};
let style = style.cast_to::<StyleV>()?.0;
Ok(Self(Line { style, alignment, spans }))
}
}
struct LinesV(Vec<Line<'static>>);
impl FromValue for LinesV {
fn from_value(v: Value) -> Result<Self> {
match v {
Value::String(s) => Ok(Self(vec![Line::raw(String::from(s.as_str()))])),
v => Ok(Self(v.cast_to::<Vec<LineV>>()?.into_iter().map(|l| l.0).collect())),
}
}
}
#[derive(Clone, Copy)]
struct FlexV(Flex);
impl FromValue for FlexV {
fn from_value(v: Value) -> Result<Self> {
let t = match &*v.cast_to::<ArcStr>()? {
"Legacy" => Flex::Legacy,
"Start" => Flex::Start,
"End" => Flex::End,
"Center" => Flex::Center,
"SpaceBetween" => Flex::SpaceBetween,
"SpaceEvenly" => Flex::SpaceEvenly,
"SpaceAround" => Flex::SpaceAround,
s => bail!("invalid flex {s}"),
};
Ok(Self(t))
}
}
#[derive(Debug, Clone, Copy)]
struct ScrollV((i64, i64));
impl FromValue for ScrollV {
fn from_value(v: Value) -> Result<Self> {
let [(_, x), (_, y)] = v.cast_to::<[(ArcStr, i64); 2]>()?;
Ok(Self((y, x)))
}
}
#[derive(Clone, Copy)]
struct TitlePositionV(TitlePosition);
impl FromValue for TitlePositionV {
fn from_value(v: Value) -> Result<Self> {
match &*v.cast_to::<ArcStr>()? {
"Top" => Ok(Self(TitlePosition::Top)),
"Bottom" => Ok(Self(TitlePosition::Bottom)),
s => bail!("invalid position {s}"),
}
}
}
#[derive(Clone, Copy)]
struct DirectionV(Direction);
impl FromValue for DirectionV {
fn from_value(v: Value) -> Result<Self> {
let t = match &*v.cast_to::<ArcStr>()? {
"Horizontal" => Direction::Horizontal,
"Vertical" => Direction::Vertical,
s => bail!("invalid direction tag {s}"),
};
Ok(Self(t))
}
}
#[derive(Clone)]
struct HighlightSpacingV(ratatui::widgets::HighlightSpacing);
impl FromValue for HighlightSpacingV {
fn from_value(v: Value) -> Result<Self> {
match &*v.cast_to::<ArcStr>()? {
"Always" => Ok(Self(ratatui::widgets::HighlightSpacing::Always)),
"Never" => Ok(Self(ratatui::widgets::HighlightSpacing::Never)),
"WhenSelected" => Ok(Self(ratatui::widgets::HighlightSpacing::WhenSelected)),
s => bail!("invalid highlight spacing {s}"),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
struct SizeV {
width: u16,
height: u16,
}
impl Into<Value> for SizeV {
fn into(self) -> Value {
[
(literal!("height"), (self.height as i64)),
(literal!("width"), (self.width as i64)),
]
.into()
}
}
impl From<Rect> for SizeV {
fn from(r: Rect) -> Self {
let s = r.as_size();
Self { width: s.width, height: s.height }
}
}
impl SizeV {
fn from_terminal() -> Result<Self> {
let (width, height) = terminal::size()?;
Ok(Self { width, height })
}
}
#[derive(Clone, Copy)]
struct MarkerV(symbols::Marker);
impl FromValue for MarkerV {
fn from_value(v: Value) -> Result<Self> {
let m = match &*v.cast_to::<ArcStr>()? {
"Dot" => symbols::Marker::Dot,
"Block" => symbols::Marker::Block,
"Bar" => symbols::Marker::Bar,
"Braille" => symbols::Marker::Braille,
"HalfBlock" => symbols::Marker::HalfBlock,
"Quadrant" => symbols::Marker::Quadrant,
"Sextant" => symbols::Marker::Sextant,
"Octant" => symbols::Marker::Octant,
s => bail!("invalid marker {s}"),
};
Ok(Self(m))
}
}
fn into_borrowed_line<'a>(line: &'a Line<'static>) -> Line<'a> {
let spans = line
.spans
.iter()
.map(|s| {
let content = match &s.content {
Cow::Owned(s) => Cow::Borrowed(s.as_str()),
Cow::Borrowed(s) => Cow::Borrowed(*s),
};
Span { content, style: s.style }
})
.collect();
Line { alignment: line.alignment, style: line.style, spans }
}
fn into_borrowed_lines<'a>(lines: &'a [Line<'static>]) -> Vec<Line<'a>> {
lines.iter().map(|l| into_borrowed_line(l)).collect::<Vec<_>>()
}
#[async_trait]
trait TuiWidget {
async fn handle_event(&mut self, e: Event, v: Value) -> Result<()>;
async fn handle_update(&mut self, id: ExprId, v: Value) -> Result<()>;
fn draw(&mut self, frame: &mut Frame, rect: Rect) -> Result<()>;
}
type TuiW = Box<dyn TuiWidget + Send + Sync + 'static>;
type CompRes = Pin<Box<dyn Future<Output = Result<TuiW>> + Send + Sync + 'static>>;
fn compile<X: GXExt>(gx: GXHandle<X>, source: Value) -> CompRes {
Box::pin(async move {
match source.cast_to::<(ArcStr, Value)>()? {
(s, v) if &s == "Text" => TextW::compile(gx, v).await,
(s, v) if &s == "Paragraph" => ParagraphW::compile(gx, v).await,
(s, v) if &s == "Block" => BlockW::compile(gx, v).await,
(s, v) if &s == "Scrollbar" => ScrollbarW::compile(gx, v).await,
(s, v) if &s == "Layout" => LayoutW::compile(gx, v).await,
(s, v) if &s == "BarChart" => BarChartW::compile(gx, v).await,
(s, v) if &s == "Chart" => ChartW::compile(gx, v).await,
(s, v) if &s == "Sparkline" => SparklineW::compile(gx, v).await,
(s, v) if &s == "LineGauge" => LineGaugeW::compile(gx, v).await,
(s, v) if &s == "Calendar" => CalendarW::compile(gx, v).await,
(s, v) if &s == "Table" => table::TableW::compile(gx, v).await,
(s, v) if &s == "Gauge" => GaugeW::compile(gx, v).await,
(s, v) if &s == "List" => ListW::compile(gx, v).await,
(s, v) if &s == "Tabs" => tabs::TabsW::compile(gx, v).await,
(s, v) if &s == "Canvas" => canvas::CanvasW::compile(gx, v).await,
(s, v) if &s == "InputHandler" => InputHandlerW::compile(gx, v).await,
(s, v) => bail!("invalid widget type `{s}({v})"),
}
})
}
struct EmptyW;
#[async_trait]
impl TuiWidget for EmptyW {
async fn handle_event(&mut self, _e: Event, _v: Value) -> Result<()> {
Ok(())
}
async fn handle_update(&mut self, _id: ExprId, _v: Value) -> Result<()> {
Ok(())
}
fn draw(&mut self, _frame: &mut Frame, _rect: Rect) -> Result<()> {
Ok(())
}
}
enum ToTui {
Update(ExprId, Value),
Stop(oneshot::Sender<()>),
}
struct Tui<X: GXExt> {
to: mpsc::Sender<ToTui>,
ph: PhantomData<X>,
}
impl<X: GXExt> Tui<X> {
fn start(
gx: &GXHandle<X>,
env: Env,
root: CompExp<X>,
stop: oneshot::Sender<()>,
) -> Self {
let gx = gx.clone();
let (to_tx, to_rx) = mpsc::channel(3);
task::spawn(async move {
if let Err(e) = run(gx, env, root, to_rx, Some(stop)).await {
error!("tui::run returned {e:?}")
}
});
Self { to: to_tx, ph: PhantomData }
}
async fn clear(&mut self) {
let (tx, rx) = oneshot::channel();
let _ = self.to.send(ToTui::Stop(tx)).await;
let _ = rx.await;
}
async fn update(&mut self, id: ExprId, v: Value) {
if let Err(_) = self.to.send(ToTui::Update(id, v)).await {
error!("could not send update because tui task died")
}
}
}
fn is_ctrl_c(e: &Event) -> bool {
e.as_key_press_event()
.map(|e| match e.code {
KeyCode::Char('c') if e.modifiers == KeyModifiers::CONTROL.into() => true,
_ => false,
})
.unwrap_or(false)
}
fn get_id(env: &Env, name: &ModPath) -> Result<BindId> {
Ok(env
.lookup_bind(&ModPath::root(), name)
.ok_or_else(|| anyhow!("could not find {name}"))?
.1
.id)
}
fn set_size<X: GXExt>(gx: &GXHandle<X>, id: BindId, size: SizeV) -> Result<()> {
gx.set(id, size)
}
fn set_mouse(enable: bool) {
use std::io::stdout;
let mut stdout = stdout();
if enable {
if let Err(e) = stdout.execute(EnableMouseCapture) {
error!("could not enable mouse capture {e:?}")
}
if let Err(e) = stdout.execute(EnableFocusChange) {
error!("could not enable focus change {e:?}")
}
} else {
if let Err(e) = stdout.execute(DisableMouseCapture) {
error!("could not disable mouse capture {e:?}")
}
if let Err(e) = stdout.execute(DisableFocusChange) {
error!("could not disable mouse capture {e:?}")
}
}
}
async fn run<X: GXExt>(
gx: GXHandle<X>,
env: Env,
root_exp: CompExp<X>,
mut to_rx: mpsc::Receiver<ToTui>,
mut stop: Option<oneshot::Sender<()>>,
) -> Result<()> {
let mut terminal = ratatui::init();
let size = get_id(&env, &["tui", "size"].into())?;
let event = get_id(&env, &["tui", "event"].into())?;
let mut mouse: TRef<X, bool> =
TRef::new(gx.compile_ref(get_id(&env, &["tui", "mouse"].into())?).await?)?;
if let Some(b) = mouse.t {
set_mouse(b)
}
set_size(&gx, size, SizeV::from_terminal()?)?;
let mut events = EventStream::new().fuse();
let mut root: TuiW = Box::new(EmptyW);
let notify = loop {
terminal.draw(|f| {
if let Err(e) = root.draw(f, f.area()) {
error!("error drawing {e:?}")
}
})?;
select! {
m = to_rx.next() => match m {
None => break oneshot::channel().0,
Some(ToTui::Stop(tx)) => break tx,
Some(ToTui::Update(id, v)) => {
if let Ok(Some(v)) = mouse.update(id, &v) {
set_mouse(*v)
}
if id == root_exp.id {
match compile(gx.clone(), v).await {
Err(e) => error!("invalid widget specification {e:?}"),
Ok(w) => root = w,
}
} else {
if let Err(e) = root.handle_update(id, v).await {
error!("error handling update {e:?}")
}
}
},
},
e = events.select_next_some() => match e {
Ok(e) if is_ctrl_c(&e) => {
if let Some(tx) = stop.take() {
let _ = tx.send(());
}
}
Ok(e) => {
let v = event_to_value(&e);
if let Event::Resize(width, height) = e
&& let Err(e) = set_size(&gx, size, SizeV { width, height }) {
error!("could not set the size ref {e:?}")
}
if let Err(e) = gx.set(event, v.clone()) {
error!("could not set event ref {e:?}")
}
if let Err(e) = root.handle_event(e, v).await {
error!("error handling event {e:?}")
}
},
Err(e) => {
error!("error reading event from terminal {e:?}");
break oneshot::channel().0
}
}
}
};
if let Some(true) = mouse.t {
set_mouse(false)
}
ratatui::restore();
let _ = notify.send(());
Ok(())
}
static TUITYP: LazyLock<Type> = LazyLock::new(|| Type::Ref (TypeRef {
scope: ModPath::root(),
name: ModPath::from(["tui", "Tui"]),
params: Arc::from_iter([]),
..Default::default()}));
#[async_trait]
impl<X: GXExt> CustomDisplay<X> for Tui<X> {
async fn clear(&mut self) {
self.clear().await;
}
async fn process_update(&mut self, _env: &Env, id: ExprId, v: Value) {
self.update(id, v).await;
}
}
graphix_derive::defpackage! {
builtins => [],
is_custom => |gx, env, e| {
if let Some(typ) = e.typ.with_deref(|t| t.cloned())
&& typ != Type::Bottom
&& typ != Type::Any
{
TUITYP.contains(env, &typ).unwrap_or(false)
} else {
false
}
},
init_custom => |gx, env, stop, e, _run_on_main| {
Ok(Box::new(Tui::<X>::start(gx, env.clone(), e, stop)))
},
}