1use std::io::Write as _;
6
7use serde_json::{Map, Value};
8
9use crate::{
10 Error, Input, Options, Session, SurfaceOptions,
11 plain::plain,
12 term,
13 ui::View,
14 wire::{Event, Font},
15};
16
17#[derive(Clone, Debug, Default)]
19pub struct PrintOptions {
20 pub css: Option<String>,
22 pub fallback: Option<String>,
25}
26
27impl PrintOptions {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn css(mut self, css: impl Into<String>) -> Self {
35 self.css = Some(css.into());
36 self
37 }
38
39 pub fn fallback(mut self, text: impl Into<String>) -> Self {
41 self.fallback = Some(text.into());
42 self
43 }
44}
45
46pub fn print(view: impl Into<View>, options: PrintOptions) -> Result<(), Error> {
71 let view = view.into();
72 let session = if term::stdout_is_tty() {
73 Session::<()>::connect(Options::new().bracketed_paste(false).kitty_keyboard(false))?
74 } else {
75 None
76 };
77 let Some(mut session) = session else {
78 let text = options
79 .fallback
80 .unwrap_or_else(|| plain(view, term::columns().map_or(80, usize::from)));
81 let mut out = std::io::stdout().lock();
82 out.write_all(text.as_bytes())?;
83 if !text.ends_with('\n') {
84 out.write_all(b"\n")?;
85 }
86 out.flush()?;
87 return Ok(());
88 };
89 let sf = session.open(SurfaceOptions::flow().listen(false))?;
90 if let Some(css) = &options.css {
91 session.stylesheet(sf, "main", Some(css))?;
92 }
93 session.render(sf, view)?;
94 session.close()
95}
96
97#[derive(Clone, Debug)]
99pub struct AskOptions {
100 pub css: Option<String>,
102 pub submit: String,
104 pub font: Option<Font>,
106}
107
108impl Default for AskOptions {
109 fn default() -> Self {
110 Self { css: None, submit: "submit".into(), font: None }
111 }
112}
113
114impl AskOptions {
115 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn css(mut self, css: impl Into<String>) -> Self {
122 self.css = Some(css.into());
123 self
124 }
125
126 pub const fn font(mut self, font: Font) -> Self {
128 self.font = Some(font);
129 self
130 }
131
132 pub fn submit(mut self, act: impl Into<String>) -> Self {
134 self.submit = act.into();
135 self
136 }
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Submission {
142 pub id: String,
144 pub act: String,
146 pub values: Map<String, Value>,
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
152pub enum Answer {
153 Submitted(Submission),
155 Cancelled,
157 Unsupported,
159}
160
161pub fn ask<M>(view: impl Into<View<M>>, options: AskOptions) -> Result<Answer, Error> {
193 ask_with(view, options, |_| None)
194}
195
196pub fn ask_with<M>(
202 view: impl Into<View<M>>,
203 options: AskOptions,
204 update: impl FnMut(M) -> Option<View<M>>,
205) -> Result<Answer, Error> {
206 let Some(mut session) = Session::<M>::connect(Options::new())? else {
207 return Ok(Answer::Unsupported);
208 };
209 let answer = ask_session(&mut session, view.into(), options, update)?;
210 session.close()?;
211 Ok(answer)
212}
213
214fn ask_session<M>(
216 session: &mut Session<M>,
217 view: View<M>,
218 options: AskOptions,
219 mut update: impl FnMut(M) -> Option<View<M>>,
220) -> Result<Answer, Error> {
221 let mut surface = SurfaceOptions::flow();
222 if let Some(font) = options.font {
223 surface = surface.font(font);
224 }
225 let sf = session.open(surface)?;
226 if let Some(css) = &options.css {
227 session.stylesheet(sf, "main", Some(css))?;
228 }
229 session.render(sf, view)?;
230 let answer = loop {
231 let Some(input) = session.next(None)? else {
232 continue;
233 };
234 let (msg, event) = match input {
235 Input::Key(key) => {
236 if key.is("escape") || key.is("ctrl+c") || key.is("ctrl+d") {
237 break Answer::Cancelled;
238 }
239 continue;
240 },
241 Input::Clipboard(_) => continue,
242 Input::Event(event) => (None, event),
243 Input::Msg(msg, event) => (Some(msg), event),
244 };
245 if let Some(next) = msg.and_then(&mut update) {
246 session.render(sf, next)?;
247 }
248 if let Event::Action(a) = &event
249 && a.act == options.submit
250 {
251 break Answer::Submitted(Submission {
252 id: a.id.clone(),
253 act: a.act.clone(),
254 values: a.values.clone().unwrap_or_default(),
255 });
256 }
257 if !session.is_open(sf) {
258 break Answer::Cancelled;
259 }
260 };
261 Ok(answer)
262}
263
264#[cfg(test)]
265mod tests {
266 use std::{collections::VecDeque, io, time::Duration};
267
268 use super::{Answer, AskOptions, ask_session};
269 use crate::{Options, Session, term::Terminal, ui};
270
271 struct Script(VecDeque<Vec<u8>>);
272
273 impl Terminal for Script {
274 fn write(&mut self, _: &[u8]) -> io::Result<()> {
275 Ok(())
276 }
277
278 fn read(&mut self, buf: &mut [u8], _: Option<Duration>) -> io::Result<Option<usize>> {
279 let Some(bytes) = self.0.pop_front() else {
280 return Ok(None);
281 };
282 buf[..bytes.len()].copy_from_slice(&bytes);
283 Ok(Some(bytes.len()))
284 }
285 }
286
287 #[test]
288 fn ask_runs_submit_handler_before_returning_submission() {
289 let term = Script(VecDeque::from([
290 b"\x1b_tsp;r;{\"r\":\"hello\",\"v\":1,\"credits\":2}\x1b\\".to_vec(),
291 b"\x1b_tsp;e;{\"ev\":\"action\",\"sf\":\"s1\",\"id\":\"main.0\",\"act\":\"submit\",\"values\":{\"size\":\"l\"}}\x1b\\".to_vec(),
292 ]));
293 let mut session = Session::with_terminal(term, Options::new().record(None))
294 .unwrap()
295 .unwrap();
296 let view = ui::html::button("Submit")
297 .actions(ui::Actions::new().click("submit"))
298 .on_click(42)
299 .into();
300 let mut handled = false;
301 let answer = ask_session(&mut session, view, AskOptions::new(), |msg| {
302 assert_eq!(msg, 42);
303 handled = true;
304 Some(ui::text("Submitted").into())
305 })
306 .unwrap();
307 assert!(handled);
308 assert!(
309 matches!(answer, Answer::Submitted(s) if s.act == "submit" && s.values["size"] == "l")
310 );
311 }
312}