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