terminal/
terminal.rs

1#![allow(clippy::missing_transmute_annotations)]
2
3/// Test program for fltk-rs wrapper for Terminal widget
4/// Intended to test the wrapper interface but not the underlying fltk functionality
5/// Jonathan Griffitts, 11-2023
6///
7use fltk::{
8    enums::{Color, Font, LabelType},
9    menu::MenuBar,
10    prelude::*,
11    terminal::{
12        Attrib, CharFlags, OutFlags, RedrawStyle, ScrollbarStyle, Terminal, Utf8Char, XtermColor,
13    },
14    window::{Window, WindowType},
15};
16
17const WIN_WIDTH: i32 = 900;
18const WIN_HEIGHT: i32 = 600;
19
20fn main() {
21    let app = fltk::app::App::default();
22
23    // Set panic handler for main thread (will become UI thread)
24    std::panic::set_hook(Box::new({
25        |e| {
26            eprintln!("!!!!PANIC!!!!{:#?}", e);
27            error_box(e.to_string()); // Only works from the UI thread
28            std::process::exit(2);
29        }
30    }));
31
32    let mut main_win = Window::new(
33        2285,
34        180,
35        WIN_WIDTH,
36        WIN_HEIGHT,
37        "FLTK/Terminal Rust wrapper test",
38    );
39    main_win.set_type(WindowType::Double);
40    main_win.make_resizable(true);
41
42    let mut menu_bar = MenuBar::new(0, 0, WIN_WIDTH, 30, None);
43
44    let mut term = Terminal::new(0, 30, WIN_WIDTH, WIN_HEIGHT - 30, None);
45    term.set_label("term");
46    main_win.resizable(&term);
47    term.set_label_type(LabelType::None);
48
49    let idx = menu_bar.add_choice("Test&1");
50    menu_bar.at(idx).unwrap().set_callback({
51        let mut term1 = term.clone();
52        move |c| mb_test1_cb(c, &mut term1)
53    });
54    menu_bar
55        .at(idx)
56        .unwrap()
57        .set_shortcut(unsafe { std::mem::transmute(0x80031) }); // Alt-1
58
59    let idx = menu_bar.add_choice("Test&2");
60    menu_bar.at(idx).unwrap().set_callback({
61        let mut term1 = term.clone();
62        move |c| mb_test2_cb(c, &mut term1)
63    });
64    menu_bar
65        .at(idx)
66        .unwrap()
67        .set_shortcut(unsafe { std::mem::transmute(0x80032) }); // Alt-2
68
69    let idx = menu_bar.add_choice("Test&3");
70    menu_bar.at(idx).unwrap().set_callback({
71        let mut term1 = term.clone();
72        move |c| mb_test3_cb(c, &mut term1)
73    });
74    menu_bar
75        .at(idx)
76        .unwrap()
77        .set_shortcut(unsafe { std::mem::transmute(0x80033) }); // Alt-3
78
79    let idx = menu_bar.add_choice("Test&4");
80    menu_bar.at(idx).unwrap().set_callback({
81        let mut term1 = term.clone();
82        move |c| mb_test4_cb(c, &mut term1)
83    });
84    menu_bar
85        .at(idx)
86        .unwrap()
87        .set_shortcut(unsafe { std::mem::transmute(0x80034) }); // Alt-4
88
89    let idx = menu_bar.add_choice("Test&5");
90    menu_bar.at(idx).unwrap().set_callback({
91        let mut term1 = term.clone();
92        move |c| mb_test5_cb(c, &mut term1)
93    });
94    menu_bar
95        .at(idx)
96        .unwrap()
97        .set_shortcut(unsafe { std::mem::transmute(0x80035) }); // Alt-5
98
99    menu_bar.end();
100
101    main_win.end();
102    main_win.show();
103
104    // Worker thread that drives the startup tests
105    let _worker_thread: std::thread::JoinHandle<_> = std::thread::spawn({
106        let mut term = term.clone();
107        move || {
108            println!("Startup tests\n");
109            term.append("Startup tests\n\n");
110            term.append("<tmp>\n"); // This line will be overwritten later
111
112            term.cursor_up(2, false);
113            assert_eq!(term.text(false), "Startup tests\n\n"); // Ignores lines below cursor
114            assert_eq!(
115                term.text(true),
116                "Startup tests\n\n<tmp>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
117            );
118
119            // Testing ansi() and set_ansi() methods
120            assert!(term.ansi(), "Default ANSI mode should be ON at startup");
121            term.append("ANSI mode is \x1b[4mON\x1b[0m\n");
122            term.set_ansi(false);
123            assert!(!term.ansi());
124            term.append("ANSI mode is \x1b[4mOFF\x1b[0m\n");
125            // append() method is already being used/tested. Test the u8, ascii, and utf8 variants
126            term.append_u8(b"Appending u8 array\n");
127            term.append_ascii("Appending ASCII array ↑ (up-arrow is dropped)\n");
128            term.set_ansi(true); // Restore ANSI state
129
130            // Play with the horizontal scrollbar
131            assert_eq!(term.hscrollbar_style(), ScrollbarStyle::AUTO);
132            term.set_hscrollbar_style(ScrollbarStyle::ON);
133            assert_eq!(term.hscrollbar_style(), ScrollbarStyle::ON);
134
135            // Test show_unknown() as incidental part of testing append methods
136            term.set_show_unknown(true);
137            assert!(term.show_unknown());
138            term.append_ascii(
139                "Appending ASCII array with show_unknown() ↑ (up-arrow is three unknown bytes)\n",
140            );
141            term.set_show_unknown(false);
142            assert!(!term.show_unknown());
143
144            term.append_utf8("Appending UTF8 array ↑ (up-arrow is visible)\n");
145            term.append_utf8_u8(b"Appending UTF8 array as u8 \xe2\x86\x91 (up-arrow is visible)\n");
146
147            let r = term.cursor_row();
148            assert_eq!(term.cursor_col(), 0);
149            term.append(&format!("Testing cursor row/col {r}"));
150            assert_eq!(term.cursor_col(), 24);
151            assert_eq!(term.cursor_row(), r);
152
153            // Test cursor color methods
154            assert_eq!(
155                term.cursor_bg_color(),
156                Color::XtermGreen,
157                "Default cursor bg at startup"
158            );
159            term.set_cursor_bg_color(Color::Red);
160            assert_eq!(term.cursor_bg_color(), Color::Red);
161            term.set_cursor_fg_color(Color::Blue);
162            assert_eq!(term.cursor_bg_color(), Color::Red);
163            assert_eq!(term.cursor_fg_color(), Color::Blue);
164            term.set_cursor_bg_color(Color::XtermGreen); // Restore the defaults
165            term.set_cursor_fg_color(Color::from_hex(0xff_ff_f0));
166            assert_eq!(term.cursor_bg_color(), Color::XtermGreen);
167            assert_eq!(term.cursor_fg_color(), Color::from_hex(0xff_ff_f0));
168
169            // The default display_rows() will derive from the window size
170            let dr = term.display_rows();
171            let height = term.h();
172            assert_eq!(height, term.h());
173            assert!(dr > 20, "Default display_rows at startup");
174            term.resize(term.x(), term.y(), term.w(), height * 2);
175            assert_eq!(term.h(), height * 2);
176            assert_eq!(height * 2, term.h());
177            assert!(term.display_rows() > dr);
178            term.resize(term.x(), term.y(), term.w(), height);
179
180            // The default display_columns() will derive from the window size
181            let dc = term.display_columns();
182            assert!(dc > 80, "Default display_rows at startup");
183            term.set_display_columns(200);
184            assert_eq!(term.display_columns(), 200);
185            term.append("\n         1         2         3         4         5         6         7         8         9");
186            term.append("\n123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
187            term.append("[This text should be truncated by display_columns() call below.]\n"); // We shouldn't see this on screen
188            term.set_display_columns(90);
189            assert_eq!(term.display_columns(), 90);
190            term.set_display_columns(dc); // Set back to default
191            assert_eq!(term.display_columns(), dc);
192
193            assert_eq!(term.history_rows(), 100, "Default history_rows at startup");
194            term.set_history_rows(50);
195            assert_eq!(term.history_rows(), 50);
196            term.set_history_rows(100); // Set back to default
197            assert_eq!(term.history_rows(), 100);
198
199            let hu = term.history_use();
200            term.append(&format!(
201                "history_use = {hu} (it's not clear what this means)\n"
202            ));
203            // assert_eq!(term.history_use(), hu+1);
204
205            term.append(&format!(
206                "margins = b:{} l:{} r:{} t{}\n",
207                term.margin_bottom(),
208                term.margin_left(),
209                term.margin_right(),
210                term.margin_top()
211            ));
212            assert_eq!(term.margin_bottom(), 3);
213            assert_eq!(term.margin_left(), 3);
214            assert_eq!(term.margin_right(), 3);
215            assert_eq!(term.margin_top(), 3);
216
217            term.set_margin_bottom(5);
218            term.set_margin_left(10);
219            term.set_margin_right(15);
220            term.set_margin_top(20);
221            assert_eq!(term.margin_bottom(), 5);
222            assert_eq!(term.margin_left(), 10);
223            assert_eq!(term.margin_right(), 15);
224            assert_eq!(term.margin_top(), 20);
225
226            term.append("Single character: '");
227            term.print_char('X');
228            term.append("', single UTF-8 character: '");
229            term.print_char_utf8('↑');
230            term.append("'\n");
231
232            let rr = term.redraw_rate();
233            assert_eq!(rr, 0.1, "Default redraw rate at startup");
234            term.append(&format!("Redraw rate {rr}\n"));
235            term.set_redraw_rate(1.0);
236            assert_eq!(term.redraw_rate(), 1.0);
237            term.set_redraw_rate(rr);
238            assert_eq!(term.redraw_rate(), rr);
239
240            let rs = term.redraw_style();
241            term.append(&format!("Redraw style {rs:?}\n"));
242            assert_eq!(
243                rs,
244                RedrawStyle::RateLimited,
245                "Default redraw style at startup"
246            );
247            term.set_redraw_style(RedrawStyle::NoRedraw);
248            assert_eq!(term.redraw_style(), RedrawStyle::NoRedraw);
249            term.set_redraw_style(rs);
250            assert_eq!(term.redraw_style(), rs);
251
252            // Sanity checks: enum values are implicitly assigned in the C++ code so could change unexpectedly
253            assert_eq!(
254                RedrawStyle::NoRedraw.bits(),
255                0x0000,
256                "RedrawStyle enum values have been reassigned"
257            );
258            assert_eq!(
259                RedrawStyle::RateLimited.bits(),
260                0x0001,
261                "RedrawStyle enum values have been reassigned"
262            );
263            assert_eq!(
264                RedrawStyle::PerWrite.bits(),
265                0x0002,
266                "RedrawStyle enum values have been reassigned"
267            );
268
269            let sb = term.scrollbar();
270            let hsb = term.hscrollbar();
271            // Both vertical and horizontal scrollbars are at zero
272            assert_eq!(sb.value(), 0.0);
273            assert_eq!(hsb.value(), 0.0);
274            term.set_hscrollbar_style(ScrollbarStyle::AUTO);
275
276            term.append(&format!(
277                "Scrollbar actual size {}\n",
278                term.scrollbar_actual_size()
279            ));
280            assert_eq!(term.scrollbar_actual_size(), 16);
281            term.append(&format!("Scrollbar size {}\n", term.scrollbar_size()));
282            assert_eq!(
283                term.scrollbar_size(),
284                0,
285                "Default scrollbar size at startup"
286            );
287            term.set_scrollbar_size(40);
288            assert_eq!(term.scrollbar_size(), 40);
289            assert_eq!(term.scrollbar_actual_size(), 40);
290            term.append(&format!(
291                "Scrollbar actual size {}\n",
292                term.scrollbar_actual_size()
293            ));
294            term.set_scrollbar_size(0); // Restore default
295            assert_eq!(term.scrollbar_size(), 0);
296            assert_eq!(term.scrollbar_actual_size(), 16);
297
298            let sfc = term.selection_fg_color();
299            let sbc = term.selection_bg_color();
300            assert_eq!(sfc, Color::Black);
301            assert_eq!(sbc, Color::White);
302            term.append(&format!("Selection colors: {sfc} {sbc}\n"));
303            term.set_selection_fg_color(Color::Green);
304            term.set_selection_bg_color(Color::DarkBlue);
305            assert_eq!(term.selection_fg_color(), Color::Green);
306            assert_eq!(term.selection_bg_color(), Color::DarkBlue);
307            term.set_selection_fg_color(sfc);
308            term.set_selection_bg_color(sbc);
309            assert_eq!(term.selection_fg_color(), Color::Black);
310            assert_eq!(term.selection_bg_color(), Color::White);
311
312            let tfcd = term.text_fg_color_default();
313            let tbcd = term.text_bg_color_default();
314            assert_eq!(tfcd, Color::XtermWhite);
315            assert_eq!(tbcd, Color::TransparentBg);
316            term.append(&format!("Default text colors: {sfc} {sbc}\n"));
317            term.set_text_fg_color_default(Color::Green);
318            term.set_text_bg_color_default(Color::DarkBlue);
319            assert_eq!(term.text_fg_color_default(), Color::Green);
320            assert_eq!(term.text_bg_color_default(), Color::DarkBlue);
321            term.set_text_fg_color_default(tfcd);
322            term.set_text_bg_color_default(tbcd);
323            assert_eq!(term.text_fg_color_default(), Color::XtermWhite);
324            assert_eq!(term.text_bg_color_default(), Color::TransparentBg);
325
326            let tfc = term.text_fg_color();
327            let tbc = term.text_bg_color();
328            assert_eq!(tfc, Color::XtermWhite);
329            assert_eq!(tbc, Color::TransparentBg);
330            term.append(&format!("Text colors: {sfc} {sbc}\n"));
331            term.set_text_fg_color(Color::Green);
332            term.set_text_bg_color(Color::DarkBlue);
333            assert_eq!(term.text_fg_color(), Color::Green);
334            assert_eq!(term.text_bg_color(), Color::DarkBlue);
335            term.set_text_fg_color(tfc);
336            term.set_text_bg_color(tbc);
337            assert_eq!(term.text_fg_color(), Color::XtermWhite);
338            assert_eq!(term.text_bg_color(), Color::TransparentBg);
339
340            let tf = term.text_font();
341            term.append(&format!("Text font: {tf:?}\n"));
342            assert_eq!(tf, Font::Courier);
343            term.set_text_font(Font::Screen);
344            assert_eq!(term.text_font(), Font::Screen);
345            term.set_text_font(tf);
346            assert_eq!(term.text_font(), Font::Courier);
347
348            let ts = term.text_size();
349            let r = term.h_to_row(100);
350            let c = term.w_to_col(100);
351            term.append(&format!(
352                "Text size: {ts}, h_to_row(100): {r}, w_to_col(100): {c}\n"
353            ));
354            assert_eq!(ts, 14);
355            term.set_text_size(30);
356            assert_eq!(term.text_size(), 30);
357            term.append(&format!(
358                "Text size: {}, h_to_row(100): {}, w_to_col(100): {}\n",
359                term.text_size(),
360                term.h_to_row(100),
361                term.w_to_col(100)
362            ));
363            term.set_text_size(ts);
364            assert_eq!(term.text_size(), ts);
365            term.append(&format!(
366                "Text size: {}, h_to_row(100): {}, w_to_col(100): {}\n",
367                term.text_size(),
368                term.h_to_row(100),
369                term.w_to_col(100)
370            ));
371
372            // Keyboard handler
373            term.handle({
374                move |term, e| {
375                    match e {
376                        fltk::enums::Event::KeyDown
377                            if fltk::app::event_key() == fltk::enums::Key::Escape =>
378                        {
379                            // false to let FLTK handle ESC. true to hide ESC
380                            false
381                        }
382
383                        fltk::enums::Event::KeyDown
384                            if fltk::app::event_length() == 1 && fltk::app::is_event_ctrl() =>
385                        {
386                            // We handle control keystroke
387                            let k = fltk::app::event_text().unwrap();
388                            term.append_utf8(&k);
389                            true
390                        }
391
392                        fltk::enums::Event::KeyDown
393                            if fltk::app::event_length() == 1 && !fltk::app::is_event_alt() =>
394                        {
395                            // We handle normal printable keystroke
396                            let k = fltk::app::event_text().unwrap();
397                            term.take_focus().unwrap();
398                            term.append(&k);
399                            true
400                        }
401
402                        // fltk docs say that keyboard handler should always claim Focus and Unfocus events
403                        // We can do this, or else ignore them (return false)
404                        // fltk::enums::Event::Focus | fltk::enums::Event::Unfocus => {
405                        //     term.redraw();
406                        //     true
407                        // }
408                        _ => false, // Let FLTK handle everything else
409                    }
410                }
411            });
412
413            let attr_save = term.text_attrib();
414            term.set_text_attrib(Attrib::Inverse | Attrib::Italic);
415            term.append("\nStartup tests complete. Keyboard is live.\n");
416            assert_eq!(term.text_attrib(), Attrib::Inverse | Attrib::Italic);
417            term.set_text_attrib(attr_save);
418            assert_eq!(term.text_attrib(), attr_save);
419            term.redraw();
420        }
421    });
422
423    app.run().unwrap();
424}
425//--------------------------------------------------------------------------------------
426/// More tests that run when the menu bar Test1 is clicked
427fn mb_test1_cb(_choice: &mut fltk::menu::Choice, term: &mut Terminal) {
428    term.take_focus().unwrap();
429    term.reset_terminal();
430    term.append("0123456789 0\n");
431    term.append("0123456789 1\n");
432    term.append("0123456789 2\n");
433    term.append("0123456789 3\n");
434    term.append("0123456789 4\n");
435    term.append("0123456789 5\n");
436    term.append("0123456789 6\n");
437    term.append("0123456789 7\n");
438    term.append("0123456789 8\n");
439    term.append("0123456789 9\n");
440    term.append("------------\n");
441
442    term.set_text_fg_color(Color::Green);
443    term.plot_char('A', 0, 0);
444    term.plot_char('B', 1, 1);
445    term.plot_char('C', 2, 2);
446    term.plot_char('D', 3, 3);
447    term.plot_char('E', 4, 4);
448    term.plot_char('F', 5, 5);
449    term.set_text_fg_color(Color::XtermWhite);
450
451    assert_eq!(term.cursor_row(), 11);
452    assert_eq!(term.cursor_col(), 0);
453
454    term.set_text_bg_color(Color::DarkBlue);
455    term.plot_char_utf8('b', 8, 1);
456    term.plot_char_utf8('↑', 9, 1);
457    term.plot_char_utf8('c', 8, 2);
458    term.plot_char_utf8('↑', 9, 2);
459    term.plot_char_utf8('d', 8, 3);
460    term.plot_char_utf8('↑', 9, 3);
461    term.plot_char_utf8('e', 8, 4);
462    term.plot_char_utf8('↑', 9, 4);
463    term.plot_char_utf8('f', 8, 5);
464    term.plot_char_utf8('↑', 9, 5);
465    term.plot_char_utf8('g', 8, 6);
466    term.plot_char_utf8('↑', 9, 6);
467    term.set_text_bg_color(Color::TransparentBg);
468
469    term.set_text_attrib(Attrib::Inverse | Attrib::Italic);
470    term.append("Done!\n");
471    term.set_text_attrib(Attrib::Normal);
472}
473
474//--------------------------------------------------------------------------------------
475/// More tests that run when the menu bar button Test2 is clicked
476fn mb_test2_cb(_choice: &mut fltk::menu::Choice, term: &mut Terminal) {
477    term.take_focus().unwrap();
478    term.reset_terminal();
479
480    for i in 0..50 {
481        term.append(&format!("{i}\n"));
482    }
483    assert_eq!(term.history_rows(), 100);
484
485    term.clear_history();
486    assert_eq!(term.history_use(), 0);
487
488    term.set_text_attrib(Attrib::Inverse | Attrib::Italic);
489    term.append("\nDone!\n");
490    term.set_text_attrib(Attrib::Normal);
491}
492
493//--------------------------------------------------------------------------------------
494/// Another set of tests that run when Test3 is clicked
495fn mb_test3_cb(_choice: &mut fltk::menu::Choice, term: &mut Terminal) {
496    term.take_focus().unwrap();
497    term.reset_terminal();
498    assert_eq!(term.text_bg_color_default(), Color::TransparentBg);
499
500    assert_eq!(term.history_use(), 0);
501    term.clear();
502    assert_eq!(term.cursor_row(), 0);
503    assert_eq!(term.history_use(), term.display_rows()); // A screenful of lines added to history
504
505    term.append("Test\ntext\na\nb\nc\nd");
506    assert_eq!(term.cursor_row(), 5);
507    let hist = term.history_use();
508    term.clear_screen_home(false);
509    assert_eq!(term.cursor_row(), 0);
510    assert_eq!(term.history_use(), hist); // History not changed
511
512    term.append("Test\ntext\na\nb\nc\nd\ne");
513    assert_eq!(term.cursor_row(), 6);
514    term.clear_screen_home(true);
515    assert_eq!(term.cursor_row(), 0);
516
517    term.append("Test\ntext\na\nb\nc\n");
518    assert_eq!(term.cursor_row(), 5);
519    term.clear_to_color(Color::DarkBlue);
520    assert_eq!(term.text_bg_color_default(), Color::TransparentBg);
521    assert_eq!(term.text_bg_color(), Color::TransparentBg);
522    assert_eq!(term.cursor_row(), 0);
523
524    // Test cursor_home()
525    term.append("Test\n\n\n\n\n\n\n\n\n\n");
526    assert_eq!(term.cursor_row(), 10);
527    term.cursor_home();
528    assert_eq!(term.cursor_row(), 0);
529
530    // Test the widget color
531    assert_eq!(term.color(), Color::Black); // Default
532    term.set_color(Color::DarkGreen);
533    assert_eq!(term.color(), Color::DarkGreen);
534    term.set_color(Color::Black);
535    assert_eq!(term.color(), Color::Black);
536    term.append(
537        "This should be one line of white text on black, embedded into the top of a blue field.\n",
538    );
539
540    assert_eq!(term.output_translate(), OutFlags::LF_TO_CRLF); // default
541    term.set_output_translate(OutFlags::OFF);
542    assert_eq!(term.output_translate(), OutFlags::OFF);
543    term.set_output_translate(OutFlags::LF_TO_CRLF); // restore default
544    assert_eq!(term.output_translate(), OutFlags::LF_TO_CRLF);
545
546    term.set_text_attrib(Attrib::Inverse | Attrib::Italic);
547    term.append("\nDone!\n");
548    term.set_text_attrib(Attrib::Normal);
549}
550
551//--------------------------------------------------------------------------------------
552/// Another set of tests for the ring-buffer access methods
553/// Note: these tests depend heavily on the low-level "protected" parts of the fltk library, which should be used with caution.
554fn mb_test4_cb(_choice: &mut fltk::menu::Choice, term: &mut Terminal) {
555    let sel_len = term.selection_text_len();
556    let sel = term.selection_text();
557
558    term.take_focus().unwrap();
559    term.reset_terminal();
560    // Test the Utf8Char primitive
561    let uc = Utf8Char::new(b'Q');
562    let uc1 = uc.text_utf8();
563    assert_eq!(&uc1, b"Q");
564    assert_eq!(&uc.attrib(), &Attrib::Normal);
565    assert_eq!(
566        &uc.charflags(),
567        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
568    );
569    assert_eq!(&uc.fgcolor(), &Color::XtermWhite);
570    assert_eq!(&uc.bgcolor(), &Color::TransparentBg);
571
572    let ring_rows = term.ring_rows();
573
574    term.take_focus().unwrap();
575    term.clear_history();
576    assert_eq!(term.history_use(), 0);
577
578    // Subtract row numbers, modulo `rows`
579    fn row_diff(rows: i32, a: i32, b: i32) -> i32 {
580        match a - b {
581            n if n < 0 => n + rows,
582            n => n,
583        }
584    }
585    // disp_srow is always 1 greater than hist_erow, modulo (ring_rows+1)
586    assert_eq!(row_diff(ring_rows, term.disp_srow(), term.hist_erow()), 1);
587    assert!(term.disp_srow() >= 0);
588    assert!(term.disp_erow() >= 0);
589    assert!(term.hist_srow() >= 0);
590    assert!(term.hist_erow() >= 0);
591    assert!(term.offset() >= 0);
592    assert!(term.disp_srow() <= ring_rows);
593    assert!(term.disp_erow() <= ring_rows);
594    assert!(term.hist_srow() <= ring_rows);
595    assert!(term.hist_erow() <= ring_rows);
596    assert!(term.offset() <= ring_rows);
597
598    assert_eq!(term.ring_srow(), 0);
599    assert_eq!(term.ring_erow(), ring_rows - 1);
600    assert_eq!(
601        row_diff(ring_rows, term.disp_erow(), term.disp_srow()) + 1,
602        term.display_rows()
603    );
604    assert_eq!(
605        row_diff(ring_rows, term.hist_erow(), term.hist_srow()) + 1,
606        term.history_rows()
607    );
608
609    assert_eq!(term.ring_erow(), term.ring_rows() - 1);
610    assert_eq!(term.ring_srow(), 0);
611
612    /// Local function to read back all rows from the display into a long string.
613    /// Does not include scrollback history.
614    /// Trims trailing blanks on each line
615    fn read_disp(term: &Terminal) -> String {
616        let rows = term.display_rows();
617        let mut text: Vec<u8> = Vec::with_capacity((rows * 64) as usize);
618        for row in 0..rows {
619            let r = term.u8c_disp_row(row).trim();
620            // Iterate through a row, accumulating [u8]
621            for c in r.iter() {
622                // Note: Sometimes utf-8 length is > 1
623                text.extend_from_slice(c.text_utf8());
624            }
625            text.extend_from_slice(b"\n");
626        }
627        // Return the result as a string
628        std::str::from_utf8(&text).unwrap().to_string()
629    }
630
631    term.clear();
632    term.append("Top line  ↑ (up-arrow)");
633    term.set_text_attrib(Attrib::Underline);
634    term.append("  ");
635    term.set_text_attrib(Attrib::Normal);
636    term.append("  \n");
637    let mut text_out = read_disp(term);
638    // Trim trailing empty lines
639    text_out = text_out.trim_end_matches(&"\n").to_string();
640    // The two plain blanks at the end will be trimmed, the two underlined blanks will be retained.
641
642    assert_eq!(text_out, "Top line  ↑ (up-arrow)  ");
643    let r = term.u8c_disp_row(0);
644    assert_eq!(r.col(0).text_utf8(), b"T");
645    assert_eq!(r.col(10).text_utf8(), b"\xe2\x86\x91"); // UTF-8 up-arrow
646    assert_eq!(r.col(24).text_utf8(), b" "); // First blank after test text, NOT trimmed
647    let r = term.u8c_disp_row(1);
648    assert_eq!(r.col(0).text_utf8(), b" "); // Second row starts with blanks
649    assert_eq!(r.col(1).text_utf8(), b" "); // Second row is full of blanks
650
651    // Clear the screen again, then append test text, then read it back and compare
652    let test_text = "The wind was a torrent of darkness among the gusty trees.
653The moon was a ghostly galleon tossed upon cloudy seas.
654The road was a ribbon of moonlight over the purple moor,
655And the highwayman came riding—
656            Riding—riding—
657The highwayman came riding, up to the old inn-door.";
658
659    term.clear_history();
660    term.clear();
661    let bg_save = term.text_bg_color();
662    let fg_save = term.text_fg_color();
663    term.set_text_bg_color(Color::DarkBlue); // Set spooky colors
664    term.set_text_fg_color(Color::from_rgb(0x40, 0x40, 0xff));
665    term.append(test_text);
666    term.set_text_bg_color(bg_save);
667    term.set_text_fg_color(fg_save);
668
669    let mut text_out = read_disp(term);
670    // Trim trailing empty lines
671    text_out = text_out.trim_end_matches(&"\n").to_string();
672    assert_eq!(test_text, text_out);
673
674    assert_eq!(row_diff(ring_rows, term.disp_srow(), term.hist_erow()), 1);
675
676    assert_eq!(term.ring_srow(), 0);
677    assert_eq!(term.ring_erow(), ring_rows - 1);
678    assert_eq!(
679        row_diff(ring_rows, term.disp_erow(), term.disp_srow()) + 1,
680        term.display_rows()
681    );
682    assert_eq!(
683        row_diff(ring_rows, term.hist_erow(), term.hist_srow()) + 1,
684        term.history_rows()
685    );
686
687    term.append(&format!(
688        "\n\nScreen has {} rows of {} columns.\n",
689        term.display_rows(),
690        term.display_columns()
691    ));
692
693    term.append(&format!("Selection len: {sel_len}\nSelection: '{sel:?}'\n"));
694}
695
696//--------------------------------------------------------------------------------------
697/// Yet another set of tests for misc cursor functions and other stuff
698/// Note: these tests depend heavily on the low-level "protected" parts of the fltk library, which should be used with caution.
699fn mb_test5_cb(_choice: &mut fltk::menu::Choice, term: &mut Terminal) {
700    term.take_focus().unwrap();
701
702    // Test the attr_fg_color and attr_bg_color methods.
703    // Put a single character 'A' into the buffer and check it
704    term.clear(); // No reset_terminal(), just clear() to preserve the mouse selection for later
705    term.set_text_bg_color(Color::TransparentBg);
706    term.set_text_fg_color(Color::XtermWhite);
707    term.append("A");
708    let r = &term.u8c_disp_row(0);
709    let uc = r.col(0);
710    assert_eq!(uc.text_utf8(), b"A");
711    assert_eq!(&uc.attr_fgcolor(None), &Color::XtermWhite);
712    assert_eq!(&uc.attr_bgcolor(None), &Color::TransparentBg);
713    assert_eq!(&uc.attr_bgcolor(Some(term)), &Color::Black);
714    assert_eq!(&uc.attr_fgcolor(Some(term)), &Color::XtermWhite);
715    assert_eq!(&uc.attrib(), &Attrib::Normal);
716
717    // Put a short string "BCD" into the first line of the buffer, with fg color change after the 'B' and bold after 'C'
718    term.clear();
719    term.set_text_fg_color_xterm(XtermColor::White);
720    term.set_text_bg_color_xterm(XtermColor::Black);
721    assert_eq!(term.text_attrib(), Attrib::Normal);
722
723    assert!(term.ansi());
724    term.append("B\x1b[32mC\x1b[1mD\n");
725
726    let r = &term.u8c_disp_row(0);
727    let uc = r.col(0);
728    assert_eq!(uc.text_utf8(), b"B");
729    assert!(uc.is_char(b'B'));
730    assert!(!uc.is_char(b'A'));
731    assert_eq!(&uc.fgcolor(), &Color::XtermWhite);
732    assert_eq!(&uc.bgcolor(), &Color::XtermBlack);
733    assert_eq!(&uc.attr_fgcolor(None), &Color::XtermWhite);
734    assert_eq!(&uc.attr_bgcolor(None), &Color::XtermBlack);
735    assert_eq!(
736        &uc.charflags(),
737        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
738    );
739
740    let uc = r.col(1);
741    assert_eq!(uc.text_utf8(), b"C");
742    assert!(uc.is_char(b'C'));
743    assert_eq!(&uc.fgcolor(), &Color::XtermGreen);
744    assert_eq!(&uc.bgcolor(), &Color::XtermBlack);
745    assert_eq!(&uc.attr_fgcolor(None), &Color::XtermGreen);
746    assert_eq!(&uc.attr_bgcolor(None), &Color::XtermBlack);
747    assert_eq!(
748        &uc.charflags(),
749        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
750    );
751
752    let uc = r.col(2);
753    assert_eq!(uc.text_utf8(), b"D");
754    assert!(uc.is_char(b'D'));
755    assert_eq!(&uc.fgcolor(), &Color::XtermGreen);
756    assert_eq!(&uc.bgcolor(), &Color::XtermBlack);
757    assert_eq!(&uc.attr_fgcolor(None), &Color::from_rgb(0x20, 0xf0, 0x20));
758    assert_eq!(&uc.attr_bgcolor(None), &Color::from_rgb(0x20, 0x20, 0x20));
759    assert_eq!(
760        &uc.attr_fgcolor(Some(term)),
761        &Color::from_rgb(0x20, 0xf0, 0x20)
762    );
763    assert_eq!(&uc.attr_bgcolor(None), &Color::from_rgb(0x20, 0x20, 0x20));
764    assert_eq!(
765        &uc.charflags(),
766        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
767    );
768
769    // Put a short string "BCDE" into the buffer, with fg color change after the 'B', bg change after 'C', and bold after 'D'
770    term.clear();
771    term.set_text_fg_color_xterm(XtermColor::White);
772    term.set_text_bg_color_xterm(XtermColor::Black);
773    term.set_text_attrib(Attrib::Normal);
774    assert_eq!(term.text_attrib(), Attrib::Normal);
775
776    assert!(term.ansi());
777    term.append("B\x1b[37mC\x1b[44mD\x1b[1mE\n");
778
779    let r = &term.u8c_disp_row(0);
780    let uc = r.col(0);
781    assert_eq!(uc.text_utf8(), b"B");
782    assert!(uc.is_char(b'B'));
783    assert!(!uc.is_char(b'A'));
784    assert_eq!(&uc.fgcolor(), &Color::XtermWhite);
785    assert_eq!(&uc.bgcolor(), &Color::XtermBlack);
786    assert_eq!(&uc.attr_fgcolor(None), &Color::XtermWhite);
787    assert_eq!(&uc.attr_bgcolor(None), &Color::XtermBlack);
788    assert_eq!(
789        &uc.charflags(),
790        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
791    );
792
793    let uc = r.col(1);
794    assert_eq!(uc.text_utf8(), b"C");
795    assert!(uc.is_char(b'C'));
796    assert_eq!(&uc.fgcolor(), &Color::XtermWhite);
797    assert_eq!(&uc.bgcolor(), &Color::XtermBlack);
798    assert_eq!(&uc.attr_fgcolor(None), &Color::XtermWhite);
799    assert_eq!(&uc.attr_bgcolor(None), &Color::XtermBlack);
800    assert_eq!(
801        &uc.charflags(),
802        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
803    );
804
805    let uc = r.col(2);
806    assert_eq!(uc.text_utf8(), b"D");
807    assert!(uc.is_char(b'D'));
808    assert_eq!(&uc.fgcolor(), &Color::XtermWhite);
809    assert_eq!(&uc.bgcolor(), &Color::XtermBgBlue);
810    assert_eq!(&uc.attr_fgcolor(None), &Color::XtermWhite);
811    assert_eq!(&uc.attr_bgcolor(None), &Color::XtermBgBlue);
812    assert_eq!(
813        &uc.charflags(),
814        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
815    );
816
817    let uc = r.col(3);
818    assert_eq!(uc.text_utf8(), b"E");
819    assert!(uc.is_char(b'E'));
820    assert_eq!(&uc.fgcolor(), &Color::XtermWhite);
821    assert_eq!(&uc.bgcolor(), &Color::XtermBgBlue);
822    assert_eq!(&uc.attr_fgcolor(None), &Color::from_hex(0xf0f0f0));
823    assert_eq!(&uc.attr_bgcolor(None), &Color::from_hex(0x2020e0));
824    assert_eq!(
825        &uc.charflags(),
826        &(CharFlags::FG_XTERM | CharFlags::BG_XTERM)
827    );
828
829    // Test some miscellaneous Utf8 constants
830    assert_eq!(uc.length(), 1);
831    assert_eq!(uc.max_utf8(), 4);
832    assert_eq!(uc.pwidth(), 9.0);
833    assert_eq!(uc.pwidth_int(), 9);
834
835    term.set_text_fg_color_xterm(XtermColor::White);
836    term.set_text_bg_color_xterm(XtermColor::Black);
837    term.clear();
838    term.set_text_attrib(Attrib::Normal);
839
840    // Mouse selection functions
841    term.append(&format!("Mouse selection: {:?}\n", &term.get_selection()));
842    term.clear_mouse_selection();
843    assert_eq!(term.get_selection(), None);
844
845    // Play with cursor position
846    term.append("0123456789\n"); // Set up test pattern
847    term.append("ABCDEFGHIJ\n");
848    term.append("abcdefghij\n");
849
850    term.set_cursor_row(1);
851    assert_eq!(term.cursor_row(), 1);
852    term.set_cursor_col(1);
853    assert_eq!(term.cursor_col(), 1);
854    assert_eq!(term.u8c_cursor().text_utf8(), b"1");
855
856    term.append("----"); // Overwrites text at cursor and moves cursor forward
857    assert_eq!(term.cursor_row(), 1);
858    assert_eq!(term.cursor_col(), 5);
859    assert_eq!(term.u8c_cursor().text_utf8(), b"5");
860    term.set_cursor_col(1);
861    assert_eq!(term.u8c_cursor().text_utf8(), b"-"); // Overwritten text
862
863    term.cursor_up(1, false);
864    assert_eq!(term.cursor_row(), 0);
865    assert_eq!(term.cursor_col(), 1);
866    assert_eq!(term.u8c_cursor().text_utf8(), b"o");
867
868    // Hit top of screen, so nothing happens
869    term.cursor_up(1, false);
870    assert_eq!(term.cursor_row(), 0);
871    assert_eq!(term.cursor_col(), 1);
872    assert_eq!(term.u8c_cursor().text_utf8(), b"o");
873
874    // Hit top of screen with scroll enabled. A blank line from history is scrolled in.
875    term.cursor_up(1, true);
876    assert_eq!(term.cursor_row(), 0);
877    assert_eq!(term.cursor_col(), 1);
878    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
879
880    // Go back down to the overwritten text
881    term.cursor_down(2, false);
882    assert_eq!(term.cursor_row(), 2);
883    assert_eq!(term.cursor_col(), 1);
884    assert_eq!(term.u8c_cursor().text_utf8(), b"-");
885
886    // Go right past the overwritten text
887    term.cursor_right(4, false);
888    assert_eq!(term.cursor_row(), 2);
889    assert_eq!(term.cursor_col(), 5);
890    assert_eq!(term.u8c_cursor().text_utf8(), b"5");
891
892    // Go left to the end of the overwritten text
893    term.cursor_left(1);
894    assert_eq!(term.cursor_row(), 2);
895    assert_eq!(term.cursor_col(), 4);
896    assert_eq!(term.u8c_cursor().text_utf8(), b"-");
897
898    // Scroll back down, removing the blank line at the top.
899    // Cursor stays in place, the text moves under it.
900    term.scroll(1);
901    assert_eq!(term.cursor_row(), 2);
902    assert_eq!(term.cursor_col(), 4);
903    assert_eq!(term.u8c_cursor().text_utf8(), b"E");
904
905    // Clear from here to end-of-line
906    term.clear_eol();
907    assert_eq!(term.cursor_row(), 2);
908    assert_eq!(term.cursor_col(), 4);
909    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
910
911    // Now clear from here to start-of-line. Cursor does not move.
912    term.clear_sol();
913    assert_eq!(term.cursor_row(), 2);
914    assert_eq!(term.cursor_col(), 4);
915    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
916    term.cursor_left(1);
917    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
918    term.set_cursor_col(0);
919    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
920
921    // Clear some lines
922    term.clear_line(1);
923    assert_eq!(term.cursor_row(), 2);
924    assert_eq!(term.cursor_col(), 0);
925    term.set_cursor_row(1);
926    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
927    term.set_cursor_row(3);
928    term.clear_cur_line();
929    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
930    assert_eq!(term.cursor_row(), 3);
931    assert_eq!(term.cursor_col(), 0);
932
933    term.append("Two lines above are intentionally left blank.\n");
934    assert_eq!(term.cursor_row(), 4);
935    assert_eq!(term.cursor_col(), 0);
936
937    // Set up the test pattern again, then play with insert/delete
938    term.append("0123456789\n");
939    term.append("ABCDEFGHIJ\n");
940    term.append("abcdefghij\n");
941    assert_eq!(term.cursor_row(), 7);
942
943    term.set_cursor_row(4);
944    term.set_cursor_col(4);
945    assert_eq!(term.u8c_cursor().text_utf8(), b"4");
946
947    term.insert_char('x', 5); // Push this row right 5 chars starting at col 4
948    assert_eq!(term.u8c_cursor().text_utf8(), b"x");
949    term.cursor_right(5, false);
950    assert_eq!(term.cursor_col(), 9);
951    assert_eq!(term.u8c_cursor().text_utf8(), b"4");
952
953    // Insert two blank rows above cursor. Cursor stays put.
954    term.insert_rows(2);
955    assert_eq!(term.cursor_row(), 4);
956    assert_eq!(term.cursor_col(), 9);
957    assert_eq!(term.u8c_cursor().text_utf8(), b" ");
958    term.cursor_down(2, false); // Go down to find our text again
959    assert_eq!(term.u8c_cursor().text_utf8(), b"4");
960
961    // Go back to the beginning of the inserted 'x' characters and delete them.
962    term.cursor_left(5);
963    assert_eq!(term.u8c_cursor().text_utf8(), b"x");
964    term.delete_cur_chars(5);
965    assert_eq!(term.cursor_row(), 6);
966    assert_eq!(term.cursor_col(), 4);
967    assert_eq!(term.u8c_cursor().text_utf8(), b"4");
968
969    term.delete_chars(7, 2, 2); // Delete "CD" from the next row
970    term.cursor_down(1, false);
971    term.cursor_left(2);
972    assert_eq!(term.u8c_cursor().text_utf8(), b"E");
973
974    term.delete_rows(1); // Middle row of pattern is gone, cursor stays put
975    assert_eq!(term.u8c_cursor().text_utf8(), b"c");
976    term.cursor_up(1, false);
977    term.delete_rows(2); // Delete remains of test pattern
978
979    term.set_text_attrib(Attrib::Bold);
980    term.insert_char_eol('-', 3, 15, 20);
981    term.set_cursor_row(3);
982    term.set_cursor_col(15);
983    assert_eq!(term.u8c_cursor().text_utf8(), b"-"); // Check the insertion
984    assert_eq!(term.u8c_cursor().attrib(), Attrib::Bold);
985
986    term.set_text_attrib(Attrib::Italic);
987    term.append(" and all lines below");
988    term.set_text_attrib(Attrib::Normal);
989    term.cursor_down(1, false);
990
991    let mut hsb = term.hscrollbar();
992    let mut sb = term.scrollbar();
993    hsb.set_value(100.0);
994    assert_eq!(hsb.value(), 100.0);
995    sb.set_value(50.0);
996    assert_eq!(sb.value(), 50.0);
997    hsb.set_value(0.0);
998    assert_eq!(hsb.value(), 0.0);
999    sb.set_value(0.0);
1000    assert_eq!(sb.value(), 0.0);
1001}
1002
1003//--------------------------------------------------------------------------------------
1004/// Displays an error message.
1005/// **Note**: this does not work unless called from the UI thread.
1006/// If you need cross-thread error boxes, try using a non-fltk
1007/// dialog such as `native_dialog::MessageDialog` instead of `fltk::dialog`
1008fn error_box(msg: String) {
1009    fltk::app::lock().unwrap();
1010    fltk::dialog::message_title("Error");
1011    fltk::dialog::message_set_hotspot(true);
1012    fltk::dialog::message_icon_label("!");
1013    fltk::dialog::message(&msg);
1014    fltk::app::unlock();
1015}