endbasic_std/console/
cmds.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Commands for console interaction.
17
18use crate::console::readline::read_line;
19use crate::console::{CharsXY, ClearType, Console, ConsoleClearable, Key};
20use crate::strings::{
21    format_boolean, format_double, format_integer, parse_boolean, parse_double, parse_integer,
22};
23use async_trait::async_trait;
24use endbasic_core::ast::{ArgSep, ExprType, Value, VarRef};
25use endbasic_core::compiler::{
26    ArgSepSyntax, OptionalValueSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax,
27    RequiredValueSyntax, SingularArgSyntax,
28};
29use endbasic_core::exec::{Machine, Scope, ValueTag};
30use endbasic_core::syms::{
31    CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
32};
33use endbasic_core::LineCol;
34use std::borrow::Cow;
35use std::cell::RefCell;
36use std::convert::TryFrom;
37use std::io;
38use std::rc::Rc;
39
40/// Category description for all symbols provided by this module.
41const CATEGORY: &str = "Console
42The EndBASIC console is the display you are seeing: both the interpreter and the \
43effects of all commands happen within the same console.  There is no separate output window as \
44other didactical interpreters provide.  This unified console supports text and, depending on the \
45output backend, graphics.  This help section focuses on the textual console; for information about \
46graphics, run HELP \"GRAPHICS\".
47The text console is a matrix of variable size.  The upper left position is row 0 and column 0.  \
48Each position in this matrix contains a character and a color attribute.  The color attribute \
49indicates the foreground and background colors of that character.  There is a default attribute \
50to match the default settings of your terminal, which might not be a color: for example, in a \
51terminal emulator configured with a black tint (aka a transparent terminal), the default color \
52respects the transparency whereas color 0 (black) does not.
53If you are writing a script and do not want the script to interfere with other parts of the \
54console, you should restrict the script to using only the INPUT and PRINT commands.
55Be aware that the console currently reacts poorly to size changes.  Avoid resizing your terminal \
56or web browser.  If you do resize them, however, restart the interpreter.";
57
58/// The `CLS` command.
59pub struct ClsCommand {
60    metadata: CallableMetadata,
61    console: Rc<RefCell<dyn Console>>,
62}
63
64impl ClsCommand {
65    /// Creates a new `CLS` command that clears the `console`.
66    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
67        Rc::from(Self {
68            metadata: CallableMetadataBuilder::new("CLS")
69                .with_syntax(&[(&[], None)])
70                .with_category(CATEGORY)
71                .with_description("Clears the screen.")
72                .build(),
73            console,
74        })
75    }
76}
77
78#[async_trait(?Send)]
79impl Callable for ClsCommand {
80    fn metadata(&self) -> &CallableMetadata {
81        &self.metadata
82    }
83
84    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
85        debug_assert_eq!(0, scope.nargs());
86        self.console.borrow_mut().clear(ClearType::All)?;
87        Ok(())
88    }
89}
90
91/// The `COLOR` command.
92pub struct ColorCommand {
93    metadata: CallableMetadata,
94    console: Rc<RefCell<dyn Console>>,
95}
96
97impl ColorCommand {
98    const NO_COLOR: i32 = 0;
99    const HAS_COLOR: i32 = 1;
100
101    /// Creates a new `COLOR` command that changes the color of the `console`.
102    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
103        Rc::from(Self {
104            metadata: CallableMetadataBuilder::new("COLOR")
105                .with_syntax(&[
106                    (&[], None),
107                    (
108                        &[SingularArgSyntax::RequiredValue(
109                            RequiredValueSyntax {
110                                name: Cow::Borrowed("fg"),
111                                vtype: ExprType::Integer,
112                            },
113                            ArgSepSyntax::End,
114                        )],
115                        None,
116                    ),
117                    (
118                        &[
119                            SingularArgSyntax::OptionalValue(
120                                OptionalValueSyntax {
121                                    name: Cow::Borrowed("fg"),
122                                    vtype: ExprType::Integer,
123                                    missing_value: Self::NO_COLOR,
124                                    present_value: Self::HAS_COLOR,
125                                },
126                                ArgSepSyntax::Exactly(ArgSep::Long),
127                            ),
128                            SingularArgSyntax::OptionalValue(
129                                OptionalValueSyntax {
130                                    name: Cow::Borrowed("bg"),
131                                    vtype: ExprType::Integer,
132                                    missing_value: Self::NO_COLOR,
133                                    present_value: Self::HAS_COLOR,
134                                },
135                                ArgSepSyntax::End,
136                            ),
137                        ],
138                        None,
139                    ),
140                ])
141                .with_category(CATEGORY)
142                .with_description(
143                    "Sets the foreground and background colors.
144Color numbers are given as ANSI numbers and can be between 0 and 255.  If a color number is not \
145specified, then the color is reset to the console's default.  The console default does not \
146necessarily match any other color specifiable in the 0 to 255 range, as it might be transparent.",
147                )
148                .build(),
149            console,
150        })
151    }
152}
153
154#[async_trait(?Send)]
155impl Callable for ColorCommand {
156    fn metadata(&self) -> &CallableMetadata {
157        &self.metadata
158    }
159
160    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
161        fn get_color((i, pos): (i32, LineCol)) -> Result<Option<u8>, CallError> {
162            if i >= 0 && i <= u8::MAX as i32 {
163                Ok(Some(i as u8))
164            } else {
165                Err(CallError::ArgumentError(pos, "Color out of range".to_owned()))
166            }
167        }
168
169        fn get_optional_color(scope: &mut Scope<'_>) -> Result<Option<u8>, CallError> {
170            match scope.pop_integer() {
171                ColorCommand::NO_COLOR => Ok(None),
172                ColorCommand::HAS_COLOR => get_color(scope.pop_integer_with_pos()),
173                _ => unreachable!(),
174            }
175        }
176
177        let (fg, bg) = if scope.nargs() == 0 {
178            (None, None)
179        } else if scope.nargs() == 1 {
180            (get_color(scope.pop_integer_with_pos())?, None)
181        } else {
182            (get_optional_color(&mut scope)?, get_optional_color(&mut scope)?)
183        };
184
185        self.console.borrow_mut().set_color(fg, bg)?;
186        Ok(())
187    }
188}
189
190/// The `INKEY` function.
191pub struct InKeyFunction {
192    metadata: CallableMetadata,
193    console: Rc<RefCell<dyn Console>>,
194}
195
196impl InKeyFunction {
197    /// Creates a new `INKEY` function that waits for a key press.
198    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
199        Rc::from(Self {
200            metadata: CallableMetadataBuilder::new("INKEY")
201                .with_return_type(ExprType::Text)
202                .with_syntax(&[(&[], None)])
203                .with_category(CATEGORY)
204                .with_description(
205                    "Checks for an available key press and returns it.
206If a key press is available to be read, returns its name.  Otherwise, returns the empty string.  \
207The returned key matches its name, number, or symbol and maintains case.  In other words, \
208pressing the X key will return 'x' or 'X' depending on the SHIFT modifier.
209The following special keys are recognized: arrow keys (UP, DOWN, LEFT, RIGHT), backspace (BS), \
210end or CTRL+E (END), enter (ENTER), CTRL+D (EOF), escape (ESC), home or CTRL+A (HOME), \
211CTRL+C (INT), page up (PGUP), page down (PGDOWN), and tab (TAB).
212This function never blocks.  To wait for a key press, you need to explicitly poll the keyboard.  \
213For example, to wait until the escape key is pressed, you could do:
214    k$ = \"\": WHILE k$ <> \"ESC\": k = INKEY$: SLEEP 0.01: WEND
215This non-blocking design lets you to combine the reception of multiple evens, such as from \
216GPIO_INPUT?, within the same loop.",
217                )
218                .build(),
219            console,
220        })
221    }
222}
223
224#[async_trait(?Send)]
225impl Callable for InKeyFunction {
226    fn metadata(&self) -> &CallableMetadata {
227        &self.metadata
228    }
229
230    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
231        debug_assert_eq!(0, scope.nargs());
232
233        let key = self.console.borrow_mut().poll_key().await?;
234        let key_name = match key {
235            Some(Key::ArrowDown) => "DOWN".to_owned(),
236            Some(Key::ArrowLeft) => "LEFT".to_owned(),
237            Some(Key::ArrowRight) => "RIGHT".to_owned(),
238            Some(Key::ArrowUp) => "UP".to_owned(),
239
240            Some(Key::Backspace) => "BS".to_owned(),
241            Some(Key::CarriageReturn) => "ENTER".to_owned(),
242            Some(Key::Char(x)) => format!("{}", x),
243            Some(Key::End) => "END".to_owned(),
244            Some(Key::Eof) => "EOF".to_owned(),
245            Some(Key::Escape) => "ESC".to_owned(),
246            Some(Key::Home) => "HOME".to_owned(),
247            Some(Key::Interrupt) => "INT".to_owned(),
248            Some(Key::NewLine) => "ENTER".to_owned(),
249            Some(Key::PageDown) => "PGDOWN".to_owned(),
250            Some(Key::PageUp) => "PGUP".to_owned(),
251            Some(Key::Tab) => "TAB".to_owned(),
252            Some(Key::Unknown(_)) => "".to_owned(),
253
254            None => "".to_owned(),
255        };
256        scope.return_string(key_name)
257    }
258}
259
260/// The `INPUT` command.
261pub struct InputCommand {
262    metadata: CallableMetadata,
263    console: Rc<RefCell<dyn Console>>,
264}
265
266impl InputCommand {
267    /// Creates a new `INPUT` command that uses `console` to gather user input.
268    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
269        Rc::from(Self {
270            metadata: CallableMetadataBuilder::new("INPUT")
271                .with_syntax(&[
272                    (
273                        &[SingularArgSyntax::RequiredRef(
274                            RequiredRefSyntax {
275                                name: Cow::Borrowed("vref"),
276                                require_array: false,
277                                define_undefined: true,
278                            },
279                            ArgSepSyntax::End,
280                        )],
281                        None,
282                    ),
283                    (
284                        &[
285                            SingularArgSyntax::OptionalValue(
286                                OptionalValueSyntax {
287                                    name: Cow::Borrowed("prompt"),
288                                    vtype: ExprType::Text,
289                                    missing_value: 0,
290                                    present_value: 1,
291                                },
292                                ArgSepSyntax::OneOf(ArgSep::Long, ArgSep::Short),
293                            ),
294                            SingularArgSyntax::RequiredRef(
295                                RequiredRefSyntax {
296                                    name: Cow::Borrowed("vref"),
297                                    require_array: false,
298                                    define_undefined: true,
299                                },
300                                ArgSepSyntax::End,
301                            ),
302                        ],
303                        None,
304                    ),
305                ])
306                .with_category(CATEGORY)
307                .with_description(
308                    "Obtains user input from the console.
309The first expression to this function must be empty or evaluate to a string, and specifies \
310the prompt to print.  If this first argument is followed by the short `;` separator, the \
311prompt is extended with a question mark.
312The second expression to this function must be a bare variable reference and indicates the \
313variable to update with the obtained input.",
314                )
315                .build(),
316            console,
317        })
318    }
319}
320
321#[async_trait(?Send)]
322impl Callable for InputCommand {
323    fn metadata(&self) -> &CallableMetadata {
324        &self.metadata
325    }
326
327    async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
328        let prompt = if scope.nargs() == 1 {
329            "".to_owned()
330        } else {
331            debug_assert!((3..=4).contains(&scope.nargs()));
332
333            let has_prompt = scope.pop_integer();
334
335            let mut prompt = if has_prompt == 1 {
336                scope.pop_string()
337            } else {
338                debug_assert_eq!(0, has_prompt);
339                String::new()
340            };
341
342            match scope.pop_sep_tag() {
343                ArgSep::Long => (),
344                ArgSep::Short => prompt.push_str("? "),
345                _ => unreachable!(),
346            }
347
348            prompt
349        };
350        let (vname, vtype, pos) = scope.pop_varref_with_pos();
351
352        let mut console = self.console.borrow_mut();
353        let mut previous_answer = String::new();
354        let vref = VarRef::new(vname.to_string(), Some(vtype));
355        loop {
356            match read_line(&mut *console, &prompt, &previous_answer, None).await {
357                Ok(answer) => {
358                    let trimmed_answer = answer.trim_end();
359                    let e = match vtype {
360                        ExprType::Boolean => match parse_boolean(trimmed_answer) {
361                            Ok(b) => {
362                                machine
363                                    .get_mut_symbols()
364                                    .set_var(&vref, Value::Boolean(b))
365                                    .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?;
366                                return Ok(());
367                            }
368                            Err(e) => e,
369                        },
370
371                        ExprType::Double => match parse_double(trimmed_answer) {
372                            Ok(d) => {
373                                machine
374                                    .get_mut_symbols()
375                                    .set_var(&vref, Value::Double(d))
376                                    .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?;
377                                return Ok(());
378                            }
379                            Err(e) => e,
380                        },
381
382                        ExprType::Integer => match parse_integer(trimmed_answer) {
383                            Ok(i) => {
384                                machine
385                                    .get_mut_symbols()
386                                    .set_var(&vref, Value::Integer(i))
387                                    .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?;
388                                return Ok(());
389                            }
390                            Err(e) => e,
391                        },
392
393                        ExprType::Text => {
394                            machine
395                                .get_mut_symbols()
396                                .set_var(&vref, Value::Text(trimmed_answer.to_owned()))
397                                .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?;
398                            return Ok(());
399                        }
400                    };
401
402                    console.print(&format!("Retry input: {}", e))?;
403                    previous_answer = answer;
404                }
405                Err(e) if e.kind() == io::ErrorKind::InvalidData => {
406                    console.print(&format!("Retry input: {}", e))?
407                }
408                Err(e) => return Err(e.into()),
409            }
410        }
411    }
412}
413
414/// The `LOCATE` command.
415pub struct LocateCommand {
416    metadata: CallableMetadata,
417    console: Rc<RefCell<dyn Console>>,
418}
419
420impl LocateCommand {
421    /// Creates a new `LOCATE` command that moves the cursor of the `console`.
422    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
423        Rc::from(Self {
424            metadata: CallableMetadataBuilder::new("LOCATE")
425                .with_syntax(&[(
426                    &[
427                        SingularArgSyntax::RequiredValue(
428                            RequiredValueSyntax {
429                                name: Cow::Borrowed("column"),
430                                vtype: ExprType::Integer,
431                            },
432                            ArgSepSyntax::Exactly(ArgSep::Long),
433                        ),
434                        SingularArgSyntax::RequiredValue(
435                            RequiredValueSyntax {
436                                name: Cow::Borrowed("row"),
437                                vtype: ExprType::Integer,
438                            },
439                            ArgSepSyntax::End,
440                        ),
441                    ],
442                    None,
443                )])
444                .with_category(CATEGORY)
445                .with_description("Moves the cursor to the given position.")
446                .build(),
447            console,
448        })
449    }
450}
451
452#[async_trait(?Send)]
453impl Callable for LocateCommand {
454    fn metadata(&self) -> &CallableMetadata {
455        &self.metadata
456    }
457
458    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
459        fn get_coord((i, pos): (i32, LineCol), name: &str) -> Result<(u16, LineCol), CallError> {
460            match u16::try_from(i) {
461                Ok(v) => Ok((v, pos)),
462                Err(_) => Err(CallError::ArgumentError(pos, format!("{} out of range", name))),
463            }
464        }
465
466        debug_assert_eq!(2, scope.nargs());
467        let (column, column_pos) = get_coord(scope.pop_integer_with_pos(), "Column")?;
468        let (row, row_pos) = get_coord(scope.pop_integer_with_pos(), "Row")?;
469
470        let mut console = self.console.borrow_mut();
471        let size = console.size_chars()?;
472
473        if column >= size.x {
474            return Err(CallError::ArgumentError(
475                column_pos,
476                format!("Column {} exceeds visible range of {}", column, size.x - 1),
477            ));
478        }
479        if row >= size.y {
480            return Err(CallError::ArgumentError(
481                row_pos,
482                format!("Row {} exceeds visible range of {}", row, size.y - 1),
483            ));
484        }
485
486        console.locate(CharsXY::new(column, row))?;
487        Ok(())
488    }
489}
490
491/// The `PRINT` command.
492pub struct PrintCommand {
493    metadata: CallableMetadata,
494    console: Rc<RefCell<dyn Console>>,
495}
496
497impl PrintCommand {
498    /// Creates a new `PRINT` command that writes to `console`.
499    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
500        Rc::from(Self {
501            metadata: CallableMetadataBuilder::new("PRINT")
502                .with_syntax(&[(
503                    &[],
504                    Some(&RepeatedSyntax {
505                        name: Cow::Borrowed("expr"),
506                        type_syn: RepeatedTypeSyntax::AnyValue,
507                        sep: ArgSepSyntax::OneOf(ArgSep::Long, ArgSep::Short),
508                        require_one: false,
509                        allow_missing: true,
510                    }),
511                )])
512                .with_category(CATEGORY)
513                .with_description(
514                    "Prints one or more values to the console.
515The expressions given as arguments are all evaluated and converted to strings before they are \
516printed.  See the documentation of STR$() for the conversion rules.
517Using a `;` separator between arguments causes the two adjacent values to be displayed together.  \
518For strings, this means that no space is added between them; for all other types, a space is added \
519after the value on the left side.
520Using a `,` separator between arguments works the same as `;` except that the fields are \
521left-aligned to 14-character wide fields on the screen.
522If the last expression is empty (i.e. if the statement ends in a semicolon or a comma), then \
523the cursor position remains on the same line of the message right after what was printed.",
524                )
525                .build(),
526            console,
527        })
528    }
529}
530
531#[async_trait(?Send)]
532impl Callable for PrintCommand {
533    fn metadata(&self) -> &CallableMetadata {
534        &self.metadata
535    }
536
537    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
538        let mut text = String::new();
539        let mut nl = true;
540        while scope.nargs() > 0 {
541            let mut add_space = false;
542
543            match scope.pop_value_tag() {
544                ValueTag::Boolean => {
545                    let b = scope.pop_boolean();
546                    add_space = true;
547                    nl = true;
548                    text += format_boolean(b);
549                }
550                ValueTag::Double => {
551                    let d = scope.pop_double();
552                    add_space = true;
553                    nl = true;
554                    text += &format_double(d);
555                }
556                ValueTag::Integer => {
557                    let i = scope.pop_integer();
558                    add_space = true;
559                    nl = true;
560                    text += &format_integer(i);
561                }
562                ValueTag::Text => {
563                    let s = scope.pop_string();
564                    nl = true;
565                    text += &s;
566                }
567                ValueTag::Missing => {
568                    nl = false;
569                }
570            }
571
572            if scope.nargs() > 0 {
573                match scope.pop_sep_tag() {
574                    ArgSep::Short => {
575                        if add_space {
576                            text += " "
577                        }
578                    }
579                    ArgSep::Long => {
580                        text += " ";
581                        while text.len() % 14 != 0 {
582                            text += " ";
583                        }
584                    }
585                    _ => unreachable!(),
586                }
587            }
588        }
589
590        if nl {
591            self.console.borrow_mut().print(&text)?;
592        } else {
593            self.console.borrow_mut().write(&text)?;
594        }
595        Ok(())
596    }
597}
598
599/// The `SCRCOLS` function.
600pub struct ScrColsFunction {
601    metadata: CallableMetadata,
602    console: Rc<RefCell<dyn Console>>,
603}
604
605impl ScrColsFunction {
606    /// Creates a new instance of the function.
607    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
608        Rc::from(Self {
609            metadata: CallableMetadataBuilder::new("SCRCOLS")
610                .with_return_type(ExprType::Integer)
611                .with_syntax(&[(&[], None)])
612                .with_category(CATEGORY)
613                .with_description(
614                    "Returns the number of columns in the text console.
615See SCRROWS to query the other dimension.",
616                )
617                .build(),
618            console,
619        })
620    }
621}
622
623#[async_trait(?Send)]
624impl Callable for ScrColsFunction {
625    fn metadata(&self) -> &CallableMetadata {
626        &self.metadata
627    }
628
629    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
630        debug_assert_eq!(0, scope.nargs());
631        let size = self.console.borrow().size_chars()?;
632        scope.return_integer(i32::from(size.x))
633    }
634}
635
636/// The `SCRROWS` function.
637pub struct ScrRowsFunction {
638    metadata: CallableMetadata,
639    console: Rc<RefCell<dyn Console>>,
640}
641
642impl ScrRowsFunction {
643    /// Creates a new instance of the function.
644    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
645        Rc::from(Self {
646            metadata: CallableMetadataBuilder::new("SCRROWS")
647                .with_return_type(ExprType::Integer)
648                .with_syntax(&[(&[], None)])
649                .with_category(CATEGORY)
650                .with_description(
651                    "Returns the number of rows in the text console.
652See SCRCOLS to query the other dimension.",
653                )
654                .build(),
655            console,
656        })
657    }
658}
659
660#[async_trait(?Send)]
661impl Callable for ScrRowsFunction {
662    fn metadata(&self) -> &CallableMetadata {
663        &self.metadata
664    }
665
666    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
667        debug_assert_eq!(0, scope.nargs());
668        let size = self.console.borrow().size_chars()?;
669        scope.return_integer(i32::from(size.y))
670    }
671}
672
673/// Adds all console-related commands for the given `console` to the `machine`.
674pub fn add_all(machine: &mut Machine, console: Rc<RefCell<dyn Console>>) {
675    machine.add_clearable(ConsoleClearable::new(console.clone()));
676    machine.add_callable(ClsCommand::new(console.clone()));
677    machine.add_callable(ColorCommand::new(console.clone()));
678    machine.add_callable(InKeyFunction::new(console.clone()));
679    machine.add_callable(InputCommand::new(console.clone()));
680    machine.add_callable(LocateCommand::new(console.clone()));
681    machine.add_callable(PrintCommand::new(console.clone()));
682    machine.add_callable(ScrColsFunction::new(console.clone()));
683    machine.add_callable(ScrRowsFunction::new(console));
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689    use crate::testutils::*;
690
691    #[test]
692    fn test_cls_ok() {
693        Tester::default().run("CLS").expect_output([CapturedOut::Clear(ClearType::All)]).check();
694    }
695
696    #[test]
697    fn test_cls_errors() {
698        check_stmt_compilation_err("1:1: In call to CLS: expected no arguments", "CLS 1");
699    }
700
701    #[test]
702    fn test_color_ok() {
703        fn t() -> Tester {
704            Tester::default()
705        }
706        t().run("COLOR").expect_output([CapturedOut::SetColor(None, None)]).check();
707        t().run("COLOR ,").expect_output([CapturedOut::SetColor(None, None)]).check();
708        t().run("COLOR 1").expect_output([CapturedOut::SetColor(Some(1), None)]).check();
709        t().run("COLOR 1,").expect_output([CapturedOut::SetColor(Some(1), None)]).check();
710        t().run("COLOR , 1").expect_output([CapturedOut::SetColor(None, Some(1))]).check();
711        t().run("COLOR 10, 5").expect_output([CapturedOut::SetColor(Some(10), Some(5))]).check();
712        t().run("COLOR 0, 0").expect_output([CapturedOut::SetColor(Some(0), Some(0))]).check();
713        t().run("COLOR 255, 255")
714            .expect_output([CapturedOut::SetColor(Some(255), Some(255))])
715            .check();
716    }
717
718    #[test]
719    fn test_color_errors() {
720        check_stmt_compilation_err(
721            "1:1: In call to COLOR: expected <> | <fg%> | <[fg%], [bg%]>",
722            "COLOR 1, 2, 3",
723        );
724        check_stmt_compilation_err(
725            "1:1: In call to COLOR: expected <> | <fg%> | <[fg%], [bg%]>",
726            "COLOR 1; 2",
727        );
728
729        check_stmt_err("1:1: In call to COLOR: 1:7: Color out of range", "COLOR 1000, 0");
730        check_stmt_err("1:1: In call to COLOR: 1:10: Color out of range", "COLOR 0, 1000");
731
732        check_stmt_compilation_err(
733            "1:1: In call to COLOR: 1:7: BOOLEAN is not a number",
734            "COLOR TRUE, 0",
735        );
736        check_stmt_compilation_err(
737            "1:1: In call to COLOR: 1:10: BOOLEAN is not a number",
738            "COLOR 0, TRUE",
739        );
740    }
741
742    #[test]
743    fn test_inkey_ok() {
744        Tester::default()
745            .run("result = INKEY")
746            .expect_var("result", Value::Text("".to_owned()))
747            .check();
748
749        Tester::default()
750            .add_input_chars("x")
751            .run("result = INKEY")
752            .expect_var("result", Value::Text("x".to_owned()))
753            .check();
754
755        Tester::default()
756            .add_input_keys(&[Key::CarriageReturn, Key::Backspace, Key::NewLine])
757            .run("r1 = INKEY$: r2 = INKEY: r3 = INKEY$")
758            .expect_var("r1", Value::Text("ENTER".to_owned()))
759            .expect_var("r2", Value::Text("BS".to_owned()))
760            .expect_var("r3", Value::Text("ENTER".to_owned()))
761            .check();
762    }
763
764    #[test]
765    fn test_inkey_errors() {
766        check_expr_compilation_error(
767            "1:10: In call to INKEY: expected no arguments nor parenthesis",
768            "INKEY()",
769        );
770        check_expr_compilation_error(
771            "1:10: In call to INKEY: expected no arguments nor parenthesis",
772            "INKEY(1)",
773        );
774    }
775
776    #[test]
777    fn test_input_ok() {
778        fn t<V: Into<Value>>(stmt: &str, input: &str, output: &str, var: &str, value: V) {
779            Tester::default()
780                .add_input_chars(input)
781                .run(stmt)
782                .expect_prints([output])
783                .expect_var(var, value)
784                .check();
785        }
786
787        t("INPUT foo\nPRINT foo", "9\n", " 9", "foo", 9);
788        t("INPUT ; foo\nPRINT foo", "9\n", " 9", "foo", 9);
789        t("INPUT ; foo\nPRINT foo", "-9\n", "-9", "foo", -9);
790        t("INPUT , bar?\nPRINT bar", "true\n", "TRUE", "bar", true);
791        t("INPUT ; foo$\nPRINT foo", "\n", "", "foo", "");
792        t(
793            "INPUT \"With question mark\"; a$\nPRINT a$",
794            "some long text\n",
795            "some long text",
796            "a",
797            "some long text",
798        );
799
800        Tester::default()
801            .add_input_chars("42\n")
802            .run("prompt$ = \"Indirectly without question mark\"\nINPUT prompt$, b\nPRINT b * 2")
803            .expect_prints([" 84"])
804            .expect_var("prompt", "Indirectly without question mark")
805            .expect_var("b", 42)
806            .check();
807    }
808
809    #[test]
810    fn test_input_on_predefined_vars() {
811        Tester::default()
812            .add_input_chars("1.5\n")
813            .run("d = 3.0\nINPUT ; d")
814            .expect_var("d", 1.5)
815            .check();
816
817        Tester::default()
818            .add_input_chars("foo bar\n")
819            .run("DIM s AS STRING\nINPUT ; s")
820            .expect_var("s", "foo bar")
821            .check();
822
823        Tester::default()
824            .add_input_chars("5\ntrue\n")
825            .run("DIM b AS BOOLEAN\nINPUT ; b")
826            .expect_prints(["Retry input: Invalid boolean literal 5"])
827            .expect_var("b", true)
828            .check();
829    }
830
831    #[test]
832    fn test_input_retry() {
833        Tester::default()
834            .add_input_chars("\ntrue\n")
835            .run("INPUT ; b?")
836            .expect_prints(["Retry input: Invalid boolean literal "])
837            .expect_var("b", true)
838            .check();
839
840        Tester::default()
841            .add_input_chars("0\ntrue\n")
842            .run("INPUT ; b?")
843            .expect_prints(["Retry input: Invalid boolean literal 0"])
844            .expect_var("b", true)
845            .check();
846
847        Tester::default()
848            .add_input_chars("\n7\n")
849            .run("a = 3\nINPUT ; a")
850            .expect_prints(["Retry input: Invalid integer literal "])
851            .expect_var("a", 7)
852            .check();
853
854        Tester::default()
855            .add_input_chars("x\n7\n")
856            .run("a = 3\nINPUT ; a")
857            .expect_prints(["Retry input: Invalid integer literal x"])
858            .expect_var("a", 7)
859            .check();
860    }
861
862    #[test]
863    fn test_input_errors() {
864        check_stmt_compilation_err(
865            "1:1: In call to INPUT: expected <vref> | <[prompt$] <,|;> vref>",
866            "INPUT",
867        );
868        check_stmt_compilation_err(
869            "1:1: In call to INPUT: expected <vref> | <[prompt$] <,|;> vref>",
870            "INPUT ; ,",
871        );
872        check_stmt_compilation_err(
873            "1:1: In call to INPUT: expected <vref> | <[prompt$] <,|;> vref>",
874            "INPUT ;",
875        );
876        check_stmt_compilation_err(
877            "1:1: In call to INPUT: 1:7: INTEGER is not a STRING",
878            "INPUT 3 ; a",
879        );
880        check_stmt_compilation_err(
881            "1:1: In call to INPUT: expected <vref> | <[prompt$] <,|;> vref>",
882            "INPUT \"foo\" AS bar",
883        );
884        check_stmt_err("1:1: In call to INPUT: 1:7: Undefined variable a", "INPUT a + 1 ; b");
885        Tester::default()
886            .run("a = 3: INPUT ; a + 1")
887            .expect_compilation_err(
888                "1:8: In call to INPUT: 1:16: Requires a variable reference, not a value",
889            )
890            .check();
891        check_stmt_err(
892            "1:1: In call to INPUT: 1:11: Cannot + STRING and BOOLEAN",
893            "INPUT \"a\" + TRUE; b?",
894        );
895    }
896
897    #[test]
898    fn test_locate_ok() {
899        Tester::default()
900            .run("LOCATE 0, 0")
901            .expect_output([CapturedOut::Locate(CharsXY::default())])
902            .check();
903
904        Tester::default()
905            .run("LOCATE 63000, 64000")
906            .expect_output([CapturedOut::Locate(CharsXY::new(63000, 64000))])
907            .check();
908    }
909
910    #[test]
911    fn test_locate_errors() {
912        check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE");
913        check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE 1");
914        check_stmt_compilation_err(
915            "1:1: In call to LOCATE: expected column%, row%",
916            "LOCATE 1, 2, 3",
917        );
918        check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE 1; 2");
919
920        check_stmt_err("1:1: In call to LOCATE: 1:8: Column out of range", "LOCATE -1, 2");
921        check_stmt_err("1:1: In call to LOCATE: 1:8: Column out of range", "LOCATE 70000, 2");
922        check_stmt_compilation_err(
923            "1:1: In call to LOCATE: 1:8: BOOLEAN is not a number",
924            "LOCATE TRUE, 2",
925        );
926        check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE , 2");
927
928        check_stmt_err("1:1: In call to LOCATE: 1:11: Row out of range", "LOCATE 1, -2");
929        check_stmt_err("1:1: In call to LOCATE: 1:11: Row out of range", "LOCATE 1, 70000");
930        check_stmt_compilation_err(
931            "1:1: In call to LOCATE: 1:11: BOOLEAN is not a number",
932            "LOCATE 1, TRUE",
933        );
934        check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE 1,");
935
936        let mut t = Tester::default();
937        t.get_console().borrow_mut().set_size_chars(CharsXY { x: 30, y: 20 });
938        t.run("LOCATE 30, 0")
939            .expect_err("1:1: In call to LOCATE: 1:8: Column 30 exceeds visible range of 29")
940            .check();
941        t.run("LOCATE 31, 0")
942            .expect_err("1:1: In call to LOCATE: 1:8: Column 31 exceeds visible range of 29")
943            .check();
944        t.run("LOCATE 0, 20")
945            .expect_err("1:1: In call to LOCATE: 1:11: Row 20 exceeds visible range of 19")
946            .check();
947        t.run("LOCATE 0, 21")
948            .expect_err("1:1: In call to LOCATE: 1:11: Row 21 exceeds visible range of 19")
949            .check();
950    }
951
952    #[test]
953    fn test_print_ok() {
954        Tester::default().run("PRINT").expect_prints([""]).check();
955        Tester::default().run("PRINT ;").expect_output([CapturedOut::Write("".to_owned())]).check();
956        Tester::default()
957            .run("PRINT ,")
958            .expect_output([CapturedOut::Write("              ".to_owned())])
959            .check();
960        Tester::default()
961            .run("PRINT ;,;,")
962            .expect_output([CapturedOut::Write("                            ".to_owned())])
963            .check();
964
965        Tester::default()
966            .run("PRINT \"1234567890123\", \"4\"")
967            .expect_prints(["1234567890123 4"])
968            .check();
969        Tester::default()
970            .run("PRINT \"12345678901234\", \"5\"")
971            .expect_prints(["12345678901234              5"])
972            .check();
973
974        Tester::default().run("PRINT \"abcdefg\", 1").expect_prints(["abcdefg        1"]).check();
975        Tester::default().run("PRINT \"abcdefgh\", 1").expect_prints(["abcdefgh       1"]).check();
976
977        Tester::default().run("PRINT 3").expect_prints([" 3"]).check();
978        Tester::default().run("PRINT -3").expect_prints(["-3"]).check();
979        Tester::default().run("PRINT 3 = 5").expect_prints(["FALSE"]).check();
980
981        Tester::default().run("PRINT 3; -1; 4").expect_prints([" 3 -1  4"]).check();
982        Tester::default().run("PRINT \"foo\"; \"bar\"").expect_prints(["foobar"]).check();
983        Tester::default()
984            .run(r#"PRINT "foo";: PRINT "bar""#)
985            .expect_output([
986                CapturedOut::Write("foo".to_owned()),
987                CapturedOut::Print("bar".to_owned()),
988            ])
989            .check();
990        Tester::default()
991            .run("PRINT true;123;\"foo bar\"")
992            .expect_prints(["TRUE  123 foo bar"])
993            .check();
994
995        Tester::default()
996            .run("PRINT 6,1;3,5")
997            .expect_prints([" 6             1  3          5"])
998            .check();
999
1000        Tester::default()
1001            .run(r#"word = "foo": PRINT word, word: PRINT word + "s""#)
1002            .expect_prints(["foo           foo", "foos"])
1003            .expect_var("word", "foo")
1004            .check();
1005
1006        Tester::default()
1007            .run(r#"word = "foo": PRINT word,: PRINT word;: PRINT word + "s""#)
1008            .expect_output([
1009                CapturedOut::Write("foo           ".to_owned()),
1010                CapturedOut::Write("foo".to_owned()),
1011                CapturedOut::Print("foos".to_owned()),
1012            ])
1013            .expect_var("word", "foo")
1014            .check();
1015    }
1016
1017    #[test]
1018    fn test_print_control_chars() {
1019        let mut found_any = false;
1020        for i in 0..1024 {
1021            let ch = char::from_u32(i).unwrap();
1022            let ch_var = format!("{}", ch);
1023            let exp_ch = if ch.is_control() {
1024                found_any = true;
1025                " "
1026            } else {
1027                &ch_var
1028            };
1029            Tester::default()
1030                .set_var("ch", Value::Text(ch_var.clone()))
1031                .run("PRINT ch")
1032                .expect_prints([exp_ch])
1033                .expect_var("ch", Value::Text(ch_var.clone()))
1034                .check();
1035        }
1036        assert!(found_any, "Test did not exercise what we wanted");
1037    }
1038
1039    #[test]
1040    fn test_print_errors() {
1041        check_stmt_compilation_err(
1042            "1:1: In call to PRINT: expected [expr1 <,|;> .. <,|;> exprN]",
1043            "PRINT 3 AS 4",
1044        );
1045        check_stmt_compilation_err(
1046            "1:1: In call to PRINT: expected [expr1 <,|;> .. <,|;> exprN]",
1047            "PRINT 3, 4 AS 5",
1048        );
1049        // Ensure type errors from `Expr` and `Value` bubble up.
1050        check_stmt_err("1:9: Unexpected value in expression", "PRINT a b");
1051        check_stmt_err("1:9: Cannot + INTEGER and BOOLEAN", "PRINT 3 + TRUE");
1052    }
1053
1054    #[test]
1055    fn test_scrcols() {
1056        let mut t = Tester::default();
1057        t.get_console().borrow_mut().set_size_chars(CharsXY { x: 12345, y: 0 });
1058        t.run("result = SCRCOLS").expect_var("result", 12345i32).check();
1059
1060        check_expr_compilation_error(
1061            "1:10: In call to SCRCOLS: expected no arguments nor parenthesis",
1062            "SCRCOLS()",
1063        );
1064        check_expr_compilation_error(
1065            "1:10: In call to SCRCOLS: expected no arguments nor parenthesis",
1066            "SCRCOLS(1)",
1067        );
1068    }
1069
1070    #[test]
1071    fn test_scrrows() {
1072        let mut t = Tester::default();
1073        t.get_console().borrow_mut().set_size_chars(CharsXY { x: 0, y: 768 });
1074        t.run("result = SCRROWS").expect_var("result", 768i32).check();
1075
1076        check_expr_compilation_error(
1077            "1:10: In call to SCRROWS: expected no arguments nor parenthesis",
1078            "SCRROWS()",
1079        );
1080        check_expr_compilation_error(
1081            "1:10: In call to SCRROWS: expected no arguments nor parenthesis",
1082            "SCRROWS(1)",
1083        );
1084    }
1085}