1use 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
40const 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
58pub struct ClsCommand {
60 metadata: CallableMetadata,
61 console: Rc<RefCell<dyn Console>>,
62}
63
64impl ClsCommand {
65 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
91pub 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 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
190pub struct InKeyFunction {
192 metadata: CallableMetadata,
193 console: Rc<RefCell<dyn Console>>,
194}
195
196impl InKeyFunction {
197 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
260pub struct InputCommand {
262 metadata: CallableMetadata,
263 console: Rc<RefCell<dyn Console>>,
264}
265
266impl InputCommand {
267 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
414pub struct LocateCommand {
416 metadata: CallableMetadata,
417 console: Rc<RefCell<dyn Console>>,
418}
419
420impl LocateCommand {
421 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
491pub struct PrintCommand {
493 metadata: CallableMetadata,
494 console: Rc<RefCell<dyn Console>>,
495}
496
497impl PrintCommand {
498 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
599pub struct ScrColsFunction {
601 metadata: CallableMetadata,
602 console: Rc<RefCell<dyn Console>>,
603}
604
605impl ScrColsFunction {
606 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
636pub struct ScrRowsFunction {
638 metadata: CallableMetadata,
639 console: Rc<RefCell<dyn Console>>,
640}
641
642impl ScrRowsFunction {
643 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
673pub 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 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}