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