use std::io::Write as _;
use serde_json::{Map, Value};
use crate::{
Error, Input, Options, Session, SurfaceOptions, plain::plain, term, ui::View, wire::Event,
};
#[derive(Clone, Debug, Default)]
pub struct PrintOptions {
pub css: Option<String>,
pub fallback: Option<String>,
}
impl PrintOptions {
pub fn new() -> Self {
Self::default()
}
pub fn css(mut self, css: impl Into<String>) -> Self {
self.css = Some(css.into());
self
}
pub fn fallback(mut self, text: impl Into<String>) -> Self {
self.fallback = Some(text.into());
self
}
}
pub fn print(view: impl Into<View>, options: PrintOptions) -> Result<(), Error> {
let view = view.into();
let session = if term::stdout_is_tty() {
Session::<()>::connect(Options::new().bracketed_paste(false).kitty_keyboard(false))?
} else {
None
};
let Some(mut session) = session else {
let text = options
.fallback
.unwrap_or_else(|| plain(view, term::columns().map_or(80, usize::from)));
let mut out = std::io::stdout().lock();
out.write_all(text.as_bytes())?;
if !text.ends_with('\n') {
out.write_all(b"\n")?;
}
out.flush()?;
return Ok(());
};
let sf = session.open(SurfaceOptions::flow().listen(false))?;
if let Some(css) = &options.css {
session.stylesheet(sf, "main", Some(css))?;
}
session.render(sf, view)?;
session.close()
}
#[derive(Clone, Debug)]
pub struct AskOptions {
pub css: Option<String>,
pub submit: String,
}
impl Default for AskOptions {
fn default() -> Self {
Self { css: None, submit: "submit".into() }
}
}
impl AskOptions {
pub fn new() -> Self {
Self::default()
}
pub fn css(mut self, css: impl Into<String>) -> Self {
self.css = Some(css.into());
self
}
pub fn submit(mut self, act: impl Into<String>) -> Self {
self.submit = act.into();
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Submission {
pub id: String,
pub act: String,
pub values: Map<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Answer {
Submitted(Submission),
Cancelled,
Unsupported,
}
pub fn ask<M>(view: impl Into<View<M>>, options: AskOptions) -> Result<Answer, Error> {
ask_with(view, options, |_| None)
}
pub fn ask_with<M>(
view: impl Into<View<M>>,
options: AskOptions,
update: impl FnMut(M) -> Option<View<M>>,
) -> Result<Answer, Error> {
let Some(mut session) = Session::<M>::connect(Options::new())? else {
return Ok(Answer::Unsupported);
};
let answer = ask_session(&mut session, view.into(), options, update)?;
session.close()?;
Ok(answer)
}
fn ask_session<M>(
session: &mut Session<M>,
view: View<M>,
options: AskOptions,
mut update: impl FnMut(M) -> Option<View<M>>,
) -> Result<Answer, Error> {
let sf = session.open(SurfaceOptions::flow())?;
if let Some(css) = &options.css {
session.stylesheet(sf, "main", Some(css))?;
}
session.render(sf, view)?;
let answer = loop {
let Some(input) = session.next(None)? else {
continue;
};
let (msg, event) = match input {
Input::Key(key) => {
if key.is("escape") || key.is("ctrl+c") || key.is("ctrl+d") {
break Answer::Cancelled;
}
continue;
},
Input::Event(event) => (None, event),
Input::Msg(msg, event) => (Some(msg), event),
};
if let Some(next) = msg.and_then(&mut update) {
session.render(sf, next)?;
}
if let Event::Action(a) = &event
&& a.act == options.submit
{
break Answer::Submitted(Submission {
id: a.id.clone(),
act: a.act.clone(),
values: a.values.clone().unwrap_or_default(),
});
}
if !session.is_open(sf) {
break Answer::Cancelled;
}
};
Ok(answer)
}
#[cfg(test)]
mod tests {
use std::{collections::VecDeque, io, time::Duration};
use super::{Answer, AskOptions, ask_session};
use crate::{Options, Session, term::Terminal, ui};
struct Script(VecDeque<Vec<u8>>);
impl Terminal for Script {
fn write(&mut self, _: &[u8]) -> io::Result<()> {
Ok(())
}
fn read(&mut self, buf: &mut [u8], _: Option<Duration>) -> io::Result<Option<usize>> {
let Some(bytes) = self.0.pop_front() else {
return Ok(None);
};
buf[..bytes.len()].copy_from_slice(&bytes);
Ok(Some(bytes.len()))
}
}
#[test]
fn ask_runs_submit_handler_before_returning_submission() {
let term = Script(VecDeque::from([
b"\x1b_tsp;r;{\"r\":\"hello\",\"v\":1,\"credits\":2}\x1b\\".to_vec(),
b"\x1b_tsp;e;{\"ev\":\"action\",\"sf\":\"s1\",\"id\":\"main.0\",\"act\":\"submit\",\"values\":{\"size\":\"l\"}}\x1b\\".to_vec(),
]));
let mut session = Session::with_terminal(term, Options::new().record(None))
.unwrap()
.unwrap();
let view = ui::html::button("Submit")
.actions(ui::Actions::new().click("submit"))
.on_click(42)
.into();
let mut handled = false;
let answer = ask_session(&mut session, view, AskOptions::new(), |msg| {
assert_eq!(msg, 42);
handled = true;
Some(ui::text("Submitted").into())
})
.unwrap();
assert!(handled);
assert!(
matches!(answer, Answer::Submitted(s) if s.act == "submit" && s.values["size"] == "l")
);
}
}