Skip to main content

greentic_flow/
questions.rs

1use anyhow::{Context, Result, anyhow};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::io::{self, Read, Write};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum QuestionKind {
8    String,
9    Bool,
10    Choice,
11    Int,
12    Float,
13}
14
15#[derive(Debug, Clone)]
16pub struct Question {
17    pub id: String,
18    pub prompt: String,
19    pub kind: QuestionKind,
20    pub required: bool,
21    pub default: Option<Value>,
22    pub choices: Vec<Value>,
23    pub show_if: Option<Value>,
24    pub writes_to: Option<String>,
25}
26
27pub type Answers = HashMap<String, Value>;
28
29#[derive(Debug, Clone)]
30pub struct MissingRequired {
31    pub missing: Vec<String>,
32    pub template: String,
33}
34
35impl std::fmt::Display for MissingRequired {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(
38            f,
39            "missing required answers: {}. Provide via --answers/--answers-file. Example:\n{}",
40            self.missing.join(", "),
41            self.template
42        )
43    }
44}
45
46impl std::error::Error for MissingRequired {}
47
48pub fn merge_answers(cli_answers: Option<Answers>, file_answers: Option<Answers>) -> Answers {
49    let mut merged = Answers::new();
50    if let Some(cli) = cli_answers {
51        merged.extend(cli);
52    }
53    if let Some(file) = file_answers {
54        merged.extend(file);
55    }
56    merged
57}
58
59pub fn validate_required(questions: &[Question], answers: &Answers) -> Result<()> {
60    let missing = missing_required(questions, answers);
61    if missing.is_empty() {
62        return Ok(());
63    }
64    let template = serde_json::to_string_pretty(&template_for_questions(questions, answers))
65        .unwrap_or_else(|_| "{}".to_string());
66    Err(MissingRequired { missing, template }.into())
67}
68
69pub fn run_interactive(questions: &[Question]) -> Result<Answers> {
70    run_interactive_with_seed(questions, Answers::new())
71}
72
73pub fn run_interactive_with_seed(questions: &[Question], seed: Answers) -> Result<Answers> {
74    let stdin = io::stdin();
75    let stdout = io::stdout();
76    run_interactive_with_io(questions, seed, stdin.lock(), stdout.lock())
77}
78
79pub fn run_interactive_with_io<R: Read, W: Write>(
80    questions: &[Question],
81    mut answers: Answers,
82    mut reader: R,
83    mut writer: W,
84) -> Result<Answers> {
85    let mut input = String::new();
86    for question in questions {
87        if !question_visible(question, &answers) {
88            continue;
89        }
90        if answers.contains_key(&question.id) {
91            continue;
92        }
93        let effective_default = question.default.clone();
94        loop {
95            input.clear();
96            write_prompt(&mut writer, question, effective_default.as_ref())?;
97            writer.flush().ok();
98            let read_any = read_line(&mut reader, &mut input)?;
99            let raw = input.trim();
100            if raw.is_empty() {
101                if let Some(default) = effective_default.clone() {
102                    answers.insert(question.id.clone(), default);
103                    break;
104                }
105                if !read_any {
106                    return Err(anyhow!(
107                        "stdin closed while waiting for answer for '{}'",
108                        question.id
109                    ));
110                }
111                if question.required {
112                    continue;
113                }
114                break;
115            }
116            match parse_answer(raw, question) {
117                Ok(value) => {
118                    answers.insert(question.id.clone(), value);
119                    break;
120                }
121                Err(_) => {
122                    continue;
123                }
124            }
125        }
126    }
127    Ok(answers)
128}
129
130pub fn extract_questions_from_flow(flow: &Value) -> Result<Vec<Question>> {
131    let Some(nodes) = flow.get("nodes").and_then(Value::as_object) else {
132        return Ok(Vec::new());
133    };
134    let mut questions = Vec::new();
135    for node in nodes.values() {
136        let Some(qnode) = node.get("questions") else {
137            continue;
138        };
139        let fields = qnode
140            .get("fields")
141            .and_then(Value::as_array)
142            .ok_or_else(|| anyhow!("questions node missing fields array"))?;
143        for field in fields {
144            let id = field
145                .get("id")
146                .and_then(Value::as_str)
147                .ok_or_else(|| anyhow!("questions field missing id"))?;
148            let prompt = field
149                .get("prompt")
150                .and_then(Value::as_str)
151                .unwrap_or(id)
152                .to_string();
153            let default = field.get("default").cloned();
154            let required = field
155                .get("required")
156                .and_then(Value::as_bool)
157                .unwrap_or(default.is_none());
158            let kind = match field.get("type").and_then(Value::as_str) {
159                Some("bool") | Some("boolean") => QuestionKind::Bool,
160                Some("int") | Some("integer") => QuestionKind::Int,
161                Some("float") | Some("number") => QuestionKind::Float,
162                Some("choice") | Some("enum") => QuestionKind::Choice,
163                _ => QuestionKind::String,
164            };
165            let choices = field
166                .get("options")
167                .and_then(Value::as_array)
168                .map(|opts| opts.to_vec())
169                .unwrap_or_default();
170            let show_if = field.get("show_if").cloned();
171            questions.push(Question {
172                id: id.to_string(),
173                prompt,
174                kind,
175                required,
176                default,
177                choices,
178                show_if,
179                writes_to: field
180                    .get("writes_to")
181                    .and_then(Value::as_str)
182                    .map(|s| s.to_string()),
183            });
184        }
185    }
186    Ok(questions)
187}
188fn write_prompt<W: Write>(
189    writer: &mut W,
190    question: &Question,
191    default_override: Option<&Value>,
192) -> Result<()> {
193    write!(writer, "Question ({}): {}", question.id, question.prompt).context("write prompt")?;
194    if let Some(default) = default_override.or(question.default.as_ref()) {
195        write!(writer, " [default: {}]", display_value(default)).ok();
196    }
197    writeln!(writer).ok();
198    if question.kind == QuestionKind::Choice && !question.choices.is_empty() {
199        for (idx, choice) in question.choices.iter().enumerate() {
200            writeln!(writer, "  {}) {}", idx + 1, display_value(choice)).ok();
201        }
202    }
203    Ok(())
204}
205
206fn read_line<R: Read>(reader: &mut R, buf: &mut String) -> Result<bool> {
207    let mut bytes = Vec::new();
208    let mut cursor = 0usize;
209    let mut byte = [0u8; 1];
210    let mut read_any = false;
211    while reader.read(&mut byte).context("read input")? == 1 {
212        read_any = true;
213        match byte[0] {
214            b'\n' => break,
215            b'\r' => continue,
216            // Backspace (Ctrl+H) and DEL.
217            0x08 | 0x7f => {
218                if cursor > 0 {
219                    cursor -= 1;
220                    bytes.remove(cursor);
221                }
222            }
223            // Ctrl+A / Ctrl+E.
224            0x01 => cursor = 0,
225            0x05 => cursor = bytes.len(),
226            // ANSI escape sequence (arrow keys/home/end/delete).
227            0x1b => consume_escape_sequence(reader, &mut bytes, &mut cursor)?,
228            b if b.is_ascii_control() => {}
229            b => {
230                bytes.insert(cursor, b);
231                cursor += 1;
232            }
233        }
234    }
235    *buf = String::from_utf8(bytes).context("parse input as UTF-8")?;
236    Ok(read_any)
237}
238
239fn consume_escape_sequence<R: Read>(
240    reader: &mut R,
241    bytes: &mut Vec<u8>,
242    cursor: &mut usize,
243) -> Result<()> {
244    let mut next = [0u8; 1];
245    if reader.read(&mut next).context("read escape sequence")? != 1 {
246        return Ok(());
247    }
248    if next[0] != b'[' {
249        return Ok(());
250    }
251
252    let mut seq = Vec::new();
253    loop {
254        let mut b = [0u8; 1];
255        if reader.read(&mut b).context("read escape sequence")? != 1 {
256            return Ok(());
257        }
258        seq.push(b[0]);
259        if b[0].is_ascii_alphabetic() || b[0] == b'~' {
260            break;
261        }
262        if seq.len() > 8 {
263            return Ok(());
264        }
265    }
266
267    match seq.as_slice() {
268        [b'D'] if *cursor > 0 => {
269            *cursor -= 1;
270        }
271        [b'C'] if *cursor < bytes.len() => {
272            *cursor += 1;
273        }
274        [b'H'] | [b'1', b'~'] | [b'7', b'~'] => *cursor = 0,
275        [b'F'] | [b'4', b'~'] | [b'8', b'~'] => *cursor = bytes.len(),
276        [b'3', b'~'] if *cursor < bytes.len() => {
277            bytes.remove(*cursor);
278        }
279        _ => {}
280    }
281    Ok(())
282}
283
284fn parse_answer(raw: &str, question: &Question) -> Result<Value> {
285    match question.kind {
286        QuestionKind::String => Ok(Value::String(raw.to_string())),
287        QuestionKind::Bool => parse_bool(raw).map(Value::Bool),
288        QuestionKind::Int => {
289            let parsed = raw.parse::<i64>().map_err(|_| anyhow!("invalid integer"))?;
290            Ok(Value::Number(parsed.into()))
291        }
292        QuestionKind::Float => {
293            let parsed = raw.parse::<f64>().map_err(|_| anyhow!("invalid number"))?;
294            let number =
295                serde_json::Number::from_f64(parsed).ok_or_else(|| anyhow!("invalid number"))?;
296            Ok(Value::Number(number))
297        }
298        QuestionKind::Choice => parse_choice(raw, question),
299    }
300}
301
302fn parse_bool(raw: &str) -> Result<bool> {
303    let lowered = raw.trim().to_lowercase();
304    let compact: String = lowered.chars().filter(|c| !c.is_whitespace()).collect();
305    match compact.as_str() {
306        "yes=true" => Ok(true),
307        "no=false" => Ok(false),
308        "y" | "yes" | "true" | "1" => Ok(true),
309        "n" | "no" | "false" | "0" => Ok(false),
310        _ => Err(anyhow!("invalid boolean")),
311    }
312}
313
314fn parse_choice(raw: &str, question: &Question) -> Result<Value> {
315    if let Ok(idx) = raw.parse::<usize>()
316        && idx >= 1
317        && idx <= question.choices.len()
318    {
319        return Ok(question.choices[idx - 1].clone());
320    }
321    for choice in &question.choices {
322        if display_value(choice) == raw {
323            return Ok(choice.clone());
324        }
325    }
326    Err(anyhow!("invalid choice"))
327}
328
329fn display_value(value: &Value) -> String {
330    match value {
331        Value::String(s) => s.clone(),
332        other => other.to_string(),
333    }
334}
335
336pub fn apply_writes_to(
337    mut base: Value,
338    questions: &[Question],
339    answers: &Answers,
340) -> Result<Value> {
341    for question in questions {
342        let Some(path) = question.writes_to.as_deref() else {
343            continue;
344        };
345        let Some(answer) = answers.get(&question.id) else {
346            continue;
347        };
348        let tokens = parse_path_tokens(path)?;
349        set_value_at_path(&mut base, &tokens, answer.clone());
350    }
351    Ok(base)
352}
353
354pub fn extract_answers_from_payload(questions: &[Question], payload: &Value) -> Answers {
355    let mut answers = Answers::new();
356    for question in questions {
357        let Some(path) = question.writes_to.as_deref() else {
358            continue;
359        };
360        if let Ok(tokens) = parse_path_tokens(path)
361            && let Some(value) = get_value_at_path(payload, &tokens)
362        {
363            answers.insert(question.id.clone(), value);
364        }
365    }
366    answers
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370enum PathToken {
371    Key(String),
372    Index(usize),
373}
374
375fn parse_path_tokens(path: &str) -> Result<Vec<PathToken>> {
376    let mut tokens = Vec::new();
377    let mut buf = String::new();
378    let mut chars = path.chars().peekable();
379    while let Some(ch) = chars.next() {
380        match ch {
381            '.' => {
382                if !buf.is_empty() {
383                    tokens.push(PathToken::Key(std::mem::take(&mut buf)));
384                }
385            }
386            '[' => {
387                if !buf.is_empty() {
388                    tokens.push(PathToken::Key(std::mem::take(&mut buf)));
389                }
390                let mut idx_buf = String::new();
391                for c in chars.by_ref() {
392                    if c == ']' {
393                        break;
394                    }
395                    idx_buf.push(c);
396                }
397                let idx = idx_buf
398                    .parse::<usize>()
399                    .map_err(|_| anyhow!("invalid index in writes_to path"))?;
400                tokens.push(PathToken::Index(idx));
401            }
402            _ => buf.push(ch),
403        }
404    }
405    if !buf.is_empty() {
406        tokens.push(PathToken::Key(buf));
407    }
408    if tokens.is_empty() {
409        Err(anyhow!("writes_to path is empty"))
410    } else {
411        Ok(tokens)
412    }
413}
414
415fn ensure_array_len(arr: &mut Vec<Value>, index: usize) {
416    if arr.len() <= index {
417        arr.resize(index + 1, Value::Null);
418    }
419}
420
421fn set_value_at_path(target: &mut Value, tokens: &[PathToken], value: Value) {
422    let mut current = target;
423    for (i, token) in tokens.iter().enumerate() {
424        let last = i == tokens.len() - 1;
425        match token {
426            PathToken::Key(key) => {
427                if !current.is_object() {
428                    *current = Value::Object(serde_json::Map::new());
429                }
430                let obj = current.as_object_mut().unwrap();
431                if last {
432                    obj.insert(key.clone(), value);
433                    return;
434                }
435                current = obj.entry(key.clone()).or_insert(Value::Null);
436            }
437            PathToken::Index(index) => {
438                if !current.is_array() {
439                    *current = Value::Array(Vec::new());
440                }
441                let arr = current.as_array_mut().unwrap();
442                ensure_array_len(arr, *index);
443                if last {
444                    arr[*index] = value;
445                    return;
446                }
447                current = &mut arr[*index];
448            }
449        }
450    }
451}
452
453fn get_value_at_path(target: &Value, tokens: &[PathToken]) -> Option<Value> {
454    let mut current = target;
455    for token in tokens {
456        match token {
457            PathToken::Key(key) => {
458                current = current.as_object()?.get(key)?;
459            }
460            PathToken::Index(index) => {
461                current = current.as_array()?.get(*index)?;
462            }
463        }
464    }
465    Some(current.clone())
466}
467
468fn missing_required(questions: &[Question], answers: &Answers) -> Vec<String> {
469    questions
470        .iter()
471        .filter(|q| q.required && question_visible(q, answers) && !answers.contains_key(&q.id))
472        .map(|q| q.id.clone())
473        .collect::<Vec<_>>()
474}
475
476fn template_for_questions(questions: &[Question], answers: &Answers) -> Value {
477    let mut obj = serde_json::Map::new();
478    for question in questions {
479        if !question_visible(question, answers) {
480            continue;
481        }
482        let value = if let Some(default) = question.default.clone() {
483            default
484        } else {
485            match question.kind {
486                QuestionKind::Bool => Value::Bool(false),
487                QuestionKind::Int => Value::Number(0.into()),
488                QuestionKind::Float => Value::Number(
489                    serde_json::Number::from_f64(0.0)
490                        .unwrap_or_else(|| serde_json::Number::from(0)),
491                ),
492                QuestionKind::Choice => question
493                    .choices
494                    .first()
495                    .cloned()
496                    .unwrap_or_else(|| Value::String(String::new())),
497                QuestionKind::String => Value::String(String::new()),
498            }
499        };
500        obj.insert(question.id.clone(), value);
501    }
502    Value::Object(obj)
503}
504
505fn question_visible(question: &Question, answers: &Answers) -> bool {
506    let Some(show_if) = &question.show_if else {
507        return true;
508    };
509    match show_if {
510        Value::Bool(value) => *value,
511        Value::Object(map) => {
512            let Some(id) = map.get("id").and_then(Value::as_str) else {
513                return true;
514            };
515            let Some(expected) = map.get("equals") else {
516                return true;
517            };
518            let Some(actual) = answers.get(id) else {
519                return false;
520            };
521            actual == expected
522        }
523        _ => true,
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use serde_json::json;
531    use std::io::Cursor;
532
533    #[test]
534    fn interactive_accepts_default_on_empty() {
535        let question = Question {
536            id: "name".to_string(),
537            prompt: "Name?".to_string(),
538            kind: QuestionKind::String,
539            required: true,
540            default: Some(Value::String("Ada".to_string())),
541            choices: Vec::new(),
542            show_if: None,
543            writes_to: None,
544        };
545        let input = Cursor::new("\n");
546        let output = Vec::new();
547        let answers = run_interactive_with_io(&[question], Answers::new(), input, output).unwrap();
548        assert_eq!(answers.get("name"), Some(&Value::String("Ada".to_string())));
549    }
550
551    #[test]
552    fn choice_accepts_index_or_value() {
553        let question = Question {
554            id: "color".to_string(),
555            prompt: "Color?".to_string(),
556            kind: QuestionKind::Choice,
557            required: true,
558            default: None,
559            choices: vec![
560                Value::String("red".to_string()),
561                Value::String("blue".to_string()),
562            ],
563            show_if: None,
564            writes_to: None,
565        };
566        let input = Cursor::new("2\n");
567        let output = Vec::new();
568        let answers = run_interactive_with_io(
569            std::slice::from_ref(&question),
570            Answers::new(),
571            input,
572            output,
573        )
574        .unwrap();
575        assert_eq!(
576            answers.get("color"),
577            Some(&Value::String("blue".to_string()))
578        );
579
580        let input = Cursor::new("red\n");
581        let output = Vec::new();
582        let answers = run_interactive_with_io(&[question], Answers::new(), input, output).unwrap();
583        assert_eq!(
584            answers.get("color"),
585            Some(&Value::String("red".to_string()))
586        );
587    }
588
589    #[test]
590    fn missing_required_reports_all_fields() {
591        let questions = vec![
592            Question {
593                id: "a".to_string(),
594                prompt: "A?".to_string(),
595                kind: QuestionKind::String,
596                required: true,
597                default: None,
598                choices: Vec::new(),
599                show_if: None,
600                writes_to: None,
601            },
602            Question {
603                id: "b".to_string(),
604                prompt: "B?".to_string(),
605                kind: QuestionKind::String,
606                required: true,
607                default: None,
608                choices: Vec::new(),
609                show_if: None,
610                writes_to: None,
611            },
612        ];
613        let err = validate_required(&questions, &Answers::new()).unwrap_err();
614        let msg = err.to_string();
615        assert!(msg.contains("a"));
616        assert!(msg.contains("b"));
617        assert!(msg.contains("--answers"));
618        assert!(msg.contains('{'));
619    }
620
621    #[test]
622    fn interactive_parses_int_and_bool() {
623        let questions = vec![
624            Question {
625                id: "count".to_string(),
626                prompt: "Count?".to_string(),
627                kind: QuestionKind::Int,
628                required: true,
629                default: None,
630                choices: Vec::new(),
631                show_if: None,
632                writes_to: None,
633            },
634            Question {
635                id: "flag".to_string(),
636                prompt: "Flag?".to_string(),
637                kind: QuestionKind::Bool,
638                required: true,
639                default: None,
640                choices: Vec::new(),
641                show_if: None,
642                writes_to: None,
643            },
644        ];
645        let input = Cursor::new("42\ny\n");
646        let output = Vec::new();
647        let answers = run_interactive_with_io(&questions, Answers::new(), input, output).unwrap();
648        assert_eq!(answers.get("count"), Some(&Value::Number(42.into())));
649        assert_eq!(answers.get("flag"), Some(&Value::Bool(true)));
650    }
651
652    #[test]
653    fn interactive_accepts_yes_no_equals_true_false() {
654        let questions = vec![
655            Question {
656                id: "enabled".to_string(),
657                prompt: "Enabled?".to_string(),
658                kind: QuestionKind::Bool,
659                required: true,
660                default: None,
661                choices: Vec::new(),
662                show_if: None,
663                writes_to: None,
664            },
665            Question {
666                id: "disabled".to_string(),
667                prompt: "Disabled?".to_string(),
668                kind: QuestionKind::Bool,
669                required: true,
670                default: None,
671                choices: Vec::new(),
672                show_if: None,
673                writes_to: None,
674            },
675        ];
676        let input = Cursor::new("YeS = TrUe\nNo = False\n");
677        let output = Vec::new();
678        let answers = run_interactive_with_io(&questions, Answers::new(), input, output).unwrap();
679        assert_eq!(answers.get("enabled"), Some(&Value::Bool(true)));
680        assert_eq!(answers.get("disabled"), Some(&Value::Bool(false)));
681    }
682
683    #[test]
684    fn interactive_respects_show_if_equals() {
685        let questions = vec![
686            Question {
687                id: "mode".to_string(),
688                prompt: "Mode?".to_string(),
689                kind: QuestionKind::String,
690                required: true,
691                default: Some(Value::String("asset".to_string())),
692                choices: Vec::new(),
693                show_if: None,
694                writes_to: None,
695            },
696            Question {
697                id: "asset_path".to_string(),
698                prompt: "Asset?".to_string(),
699                kind: QuestionKind::String,
700                required: true,
701                default: None,
702                choices: Vec::new(),
703                show_if: Some(json!({ "id": "mode", "equals": "asset" })),
704                writes_to: None,
705            },
706        ];
707        let input = Cursor::new("\npath.json\n");
708        let output = Vec::new();
709        let answers = run_interactive_with_io(&questions, Answers::new(), input, output).unwrap();
710        assert_eq!(
711            answers.get("mode"),
712            Some(&Value::String("asset".to_string()))
713        );
714        assert_eq!(
715            answers.get("asset_path"),
716            Some(&Value::String("path.json".to_string()))
717        );
718
719        let input = Cursor::new("inline\n");
720        let output = Vec::new();
721        let answers = run_interactive_with_io(&questions, Answers::new(), input, output).unwrap();
722        assert_eq!(
723            answers.get("mode"),
724            Some(&Value::String("inline".to_string()))
725        );
726        assert!(!answers.contains_key("asset_path"));
727    }
728
729    #[test]
730    fn validate_required_skips_hidden_questions() {
731        let questions = vec![Question {
732            id: "hidden".to_string(),
733            prompt: "Hidden?".to_string(),
734            kind: QuestionKind::String,
735            required: true,
736            default: None,
737            choices: Vec::new(),
738            show_if: Some(Value::Bool(false)),
739            writes_to: None,
740        }];
741        validate_required(&questions, &Answers::new()).unwrap();
742    }
743
744    #[test]
745    fn writes_to_creates_nested_objects() {
746        let questions = vec![Question {
747            id: "asset_path".to_string(),
748            prompt: "Asset?".to_string(),
749            kind: QuestionKind::String,
750            required: true,
751            default: None,
752            choices: Vec::new(),
753            show_if: None,
754            writes_to: Some("card_spec.asset_path".to_string()),
755        }];
756        let mut answers = Answers::new();
757        answers.insert(
758            "asset_path".to_string(),
759            Value::String("path.json".to_string()),
760        );
761        let output = apply_writes_to(Value::Object(Default::default()), &questions, &answers)
762            .expect("apply");
763        let card_spec = output.get("card_spec").and_then(Value::as_object).unwrap();
764        assert_eq!(
765            card_spec.get("asset_path").and_then(Value::as_str),
766            Some("path.json")
767        );
768    }
769
770    #[test]
771    fn writes_to_supports_array_indexes() {
772        let questions = vec![Question {
773            id: "action_id".to_string(),
774            prompt: "Action?".to_string(),
775            kind: QuestionKind::String,
776            required: true,
777            default: None,
778            choices: Vec::new(),
779            show_if: None,
780            writes_to: Some("actions[0].id".to_string()),
781        }];
782        let mut answers = Answers::new();
783        answers.insert(
784            "action_id".to_string(),
785            Value::String("action-1".to_string()),
786        );
787        let output = apply_writes_to(Value::Object(Default::default()), &questions, &answers)
788            .expect("apply");
789        let actions = output.get("actions").and_then(Value::as_array).unwrap();
790        let first = actions[0].as_object().unwrap();
791        assert_eq!(first.get("id").and_then(Value::as_str), Some("action-1"));
792    }
793
794    #[test]
795    fn read_line_supports_backspace_and_arrow_edits() {
796        let mut input = Cursor::new(b"abc\x1b[D\x1b[D\x7fX\n".to_vec());
797        let mut buf = String::new();
798        let read_any = read_line(&mut input, &mut buf).expect("read line");
799        assert!(read_any);
800        assert_eq!(buf, "Xbc");
801    }
802
803    #[test]
804    fn read_line_supports_home_end_and_delete() {
805        let mut input = Cursor::new(b"abcd\x1b[D\x1b[D\x1b[D\x1b[3~X\x1b[H*\x1b[F!\n".to_vec());
806        let mut buf = String::new();
807        let read_any = read_line(&mut input, &mut buf).expect("read line");
808        assert!(read_any);
809        assert_eq!(buf, "*aXcd!");
810    }
811}