tuikit/
term.rs

1//! Term is a thread-safe "terminal".
2//!
3//! It allows you to:
4//! - Listen to key stroke events
5//! - Output contents to the terminal
6//!
7//! ```no_run
8//! use tuikit::prelude::*;
9//!
10//! let term = Term::<()>::new().unwrap();
11//!
12//! while let Ok(ev) = term.poll_event() {
13//!     if let Event::Key(Key::Char('q')) = ev {
14//!         break;
15//!     }
16//!
17//!     term.print(0, 0, format!("got event: {:?}", ev).as_str());
18//!     term.present();
19//! }
20//! ```
21//!
22//! Term is modeled after [termbox](https://github.com/nsf/termbox). The main idea is viewing
23//! terminals as a table of fixed-size cells and input being a stream of structured messages
24
25use std::cmp::{max, min};
26use std::sync::atomic::{AtomicUsize, Ordering};
27use std::sync::mpsc::{channel, Receiver, Sender};
28use std::sync::Arc;
29use std::thread;
30use std::time::Duration;
31
32use crate::attr::Attr;
33use crate::canvas::Canvas;
34use crate::cell::Cell;
35use crate::draw::Draw;
36use crate::error::TuikitError;
37use crate::event::Event;
38use crate::input::{KeyBoard, KeyboardHandler};
39use crate::key::Key;
40use crate::output::Command;
41use crate::output::Output;
42use crate::raw::{get_tty, IntoRawMode};
43use crate::screen::Screen;
44use crate::spinlock::SpinLock;
45use crate::sys::signal::{initialize_signals, notify_on_sigwinch, unregister_sigwinch};
46use crate::Result;
47
48const MIN_HEIGHT: usize = 1;
49const WAIT_TIMEOUT: Duration = Duration::from_millis(300);
50const POLLING_TIMEOUT: Duration = Duration::from_millis(10);
51
52#[derive(Debug, Copy, Clone)]
53pub enum TermHeight {
54    Fixed(usize),
55    Percent(usize),
56}
57
58pub struct Term<UserEvent: Send + 'static = ()> {
59    components_to_stop: Arc<AtomicUsize>,
60    keyboard_handler: SpinLock<Option<KeyboardHandler>>,
61    resize_signal_id: Arc<AtomicUsize>,
62    term_lock: SpinLock<TermLock>,
63    event_rx: SpinLock<Receiver<Event<UserEvent>>>,
64    event_tx: Arc<SpinLock<Sender<Event<UserEvent>>>>,
65    raw_mouse: bool, // to produce raw mouse event or the parsed event(e.g. DoubleClick)
66}
67
68pub struct TermOptions {
69    max_height: TermHeight,
70    min_height: TermHeight,
71    height: TermHeight,
72    clear_on_exit: bool,
73    clear_on_start: bool,
74    mouse_enabled: bool,
75    raw_mouse: bool,
76    hold: bool, // to start term or not on creation
77    disable_alternate_screen: bool,
78}
79
80impl Default for TermOptions {
81    fn default() -> Self {
82        Self {
83            max_height: TermHeight::Percent(100),
84            min_height: TermHeight::Fixed(3),
85            height: TermHeight::Percent(100),
86            clear_on_exit: true,
87            clear_on_start: true,
88            mouse_enabled: false,
89            raw_mouse: false,
90            hold: false,
91            disable_alternate_screen: false,
92        }
93    }
94}
95
96// Builder
97impl TermOptions {
98    pub fn max_height(mut self, max_height: TermHeight) -> Self {
99        self.max_height = max_height;
100        self
101    }
102
103    pub fn min_height(mut self, min_height: TermHeight) -> Self {
104        self.min_height = min_height;
105        self
106    }
107    pub fn height(mut self, height: TermHeight) -> Self {
108        self.height = height;
109        self
110    }
111    pub fn clear_on_exit(mut self, clear: bool) -> Self {
112        self.clear_on_exit = clear;
113        self
114    }
115    pub fn clear_on_start(mut self, clear: bool) -> Self {
116        self.clear_on_start = clear;
117        self
118    }
119    pub fn mouse_enabled(mut self, enabled: bool) -> Self {
120        self.mouse_enabled = enabled;
121        self
122    }
123    pub fn raw_mouse(mut self, enabled: bool) -> Self {
124        self.raw_mouse = enabled;
125        self
126    }
127    pub fn hold(mut self, hold: bool) -> Self {
128        self.hold = hold;
129        self
130    }
131    pub fn disable_alternate_screen(mut self, disable_alternate_screen: bool) -> Self {
132        self.disable_alternate_screen = disable_alternate_screen;
133        self
134    }
135}
136
137impl<UserEvent: Send + 'static> Term<UserEvent> {
138    /// Create a Term with height specified.
139    ///
140    /// Internally if the calculated height would fill the whole screen, `Alternate Screen` will
141    /// be enabled, otherwise only part of the screen will be used.
142    ///
143    /// If the preferred height is larger than the current screen, whole screen is used.
144    ///
145    /// ```no_run
146    /// use tuikit::term::{Term, TermHeight};
147    ///
148    /// let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); // 30% of the terminal height
149    /// let term: Term<()> = Term::with_height(TermHeight::Fixed(20)).unwrap(); // fixed 20 lines
150    /// ```
151    pub fn with_height(height: TermHeight) -> Result<Term<UserEvent>> {
152        Term::with_options(TermOptions::default().height(height))
153    }
154
155    /// Create a Term (with 100% height)
156    ///
157    /// ```no_run
158    /// use tuikit::term::{Term, TermHeight};
159    ///
160    /// let term: Term<()> = Term::new().unwrap();
161    /// let term: Term<()> = Term::with_height(TermHeight::Percent(100)).unwrap();
162    /// ```
163    pub fn new() -> Result<Term<UserEvent>> {
164        Term::with_options(TermOptions::default())
165    }
166
167    /// Create a Term with custom options
168    ///
169    /// ```no_run
170    /// use tuikit::term::{Term, TermHeight, TermOptions};
171    ///
172    /// let term: Term<()> = Term::with_options(TermOptions::default().height(TermHeight::Percent(100))).unwrap();
173    /// ```
174    pub fn with_options(options: TermOptions) -> Result<Term<UserEvent>> {
175        initialize_signals();
176
177        let (event_tx, event_rx) = channel();
178        let raw_mouse = options.raw_mouse;
179        let ret = Term {
180            components_to_stop: Arc::new(AtomicUsize::new(0)),
181            keyboard_handler: SpinLock::new(None),
182            resize_signal_id: Arc::new(AtomicUsize::new(0)),
183            term_lock: SpinLock::new(TermLock::with_options(&options)),
184            event_tx: Arc::new(SpinLock::new(event_tx)),
185            event_rx: SpinLock::new(event_rx),
186            raw_mouse,
187        };
188        if options.hold {
189            Ok(ret)
190        } else {
191            ret.restart().map(|_| ret)
192        }
193    }
194
195    fn ensure_not_stopped(&self) -> Result<()> {
196        if self.components_to_stop.load(Ordering::SeqCst) == 2 {
197            Ok(())
198        } else {
199            Err(TuikitError::TerminalNotStarted)
200        }
201    }
202
203    fn get_cursor_pos(
204        &self,
205        keyboard: &mut KeyBoard,
206        output: &mut Output,
207    ) -> Result<(usize, usize)> {
208        output.ask_for_cpr();
209
210        if let Ok(key) = keyboard.next_key_timeout(WAIT_TIMEOUT) {
211            if let Key::CursorPos(row, col) = key {
212                return Ok((row as usize, col as usize));
213            }
214        }
215
216        Ok((0, 0))
217    }
218
219    /// restart the terminal if it had been stopped
220    pub fn restart(&self) -> Result<()> {
221        let mut termlock = self.term_lock.lock();
222        if self.components_to_stop.load(Ordering::SeqCst) == 2 {
223            return Ok(());
224        }
225
226        let ttyout = get_tty()?.into_raw_mode()?;
227        let mut output = Output::new(Box::new(ttyout))?;
228        let mut keyboard = KeyBoard::new_with_tty().raw_mouse(self.raw_mouse);
229        self.keyboard_handler
230            .lock()
231            .replace(keyboard.get_interrupt_handler());
232        let cursor_pos = self.get_cursor_pos(&mut keyboard, &mut output)?;
233        termlock.restart(output, cursor_pos)?;
234
235        // start two listener
236        self.start_key_listener(keyboard);
237        self.start_size_change_listener();
238
239        // wait for components to start
240        while self.components_to_stop.load(Ordering::SeqCst) < 2 {
241            debug!(
242                "restart: components: {}",
243                self.components_to_stop.load(Ordering::SeqCst)
244            );
245            thread::sleep(POLLING_TIMEOUT);
246        }
247
248        let event_tx = self.event_tx.lock();
249        let _ = event_tx.send(Event::Restarted);
250
251        Ok(())
252    }
253
254    /// Pause the Term
255    ///
256    /// This function will cause the Term to give away the control to the terminal(such as listening
257    /// to the key strokes). After the Term was "paused", `poll_event` will block indefinitely and
258    /// recover after the Term was `restart`ed.
259    pub fn pause(&self) -> Result<()> {
260        self.pause_internal(false)
261    }
262
263    fn pause_internal(&self, exiting: bool) -> Result<()> {
264        debug!("pause");
265        let mut termlock = self.term_lock.lock();
266
267        if self.components_to_stop.load(Ordering::SeqCst) == 0 {
268            return Ok(());
269        }
270
271        // wait for the components to stop
272        // i.e. key_listener & size_change_listener
273        self.keyboard_handler.lock().take().map(|h| h.interrupt());
274        unregister_sigwinch(self.resize_signal_id.load(Ordering::Relaxed)).map(|tx| tx.send(()));
275
276        termlock.pause(exiting)?;
277
278        // wait for the components to stop
279        while self.components_to_stop.load(Ordering::SeqCst) > 0 {
280            debug!(
281                "pause: components: {}",
282                self.components_to_stop.load(Ordering::SeqCst)
283            );
284            thread::sleep(POLLING_TIMEOUT);
285        }
286
287        Ok(())
288    }
289
290    fn start_key_listener(&self, mut keyboard: KeyBoard) {
291        let event_tx_clone = self.event_tx.clone();
292        let components_to_stop = self.components_to_stop.clone();
293        thread::spawn(move || {
294            components_to_stop.fetch_add(1, Ordering::SeqCst);
295            debug!("key listener start");
296            loop {
297                let next_key = keyboard.next_key();
298                trace!("next key: {:?}", next_key);
299                match next_key {
300                    Ok(key) => {
301                        let event_tx = event_tx_clone.lock();
302                        let _ = event_tx.send(Event::Key(key));
303                    }
304                    Err(TuikitError::Interrupted) => break,
305                    _ => {} // ignored
306                }
307            }
308            components_to_stop.fetch_sub(1, Ordering::SeqCst);
309            debug!("key listener stop");
310        });
311    }
312
313    fn start_size_change_listener(&self) {
314        let event_tx_clone = self.event_tx.clone();
315        let resize_signal_id = self.resize_signal_id.clone();
316        let components_to_stop = self.components_to_stop.clone();
317
318        thread::spawn(move || {
319            let (id, sigwinch_rx) = notify_on_sigwinch();
320            resize_signal_id.store(id, Ordering::Relaxed);
321
322            components_to_stop.fetch_add(1, Ordering::SeqCst);
323            debug!("size change listener started");
324            loop {
325                if let Ok(_) = sigwinch_rx.recv() {
326                    let event_tx = event_tx_clone.lock();
327                    let _ = event_tx.send(Event::Resize {
328                        width: 0,
329                        height: 0,
330                    });
331                } else {
332                    break;
333                }
334            }
335            components_to_stop.fetch_sub(1, Ordering::SeqCst);
336            debug!("size change listener stop");
337        });
338    }
339
340    fn filter_event(&self, event: Event<UserEvent>) -> Event<UserEvent> {
341        match event {
342            Event::Resize { .. } => {
343                {
344                    let mut termlock = self.term_lock.lock();
345                    let _ = termlock.on_resize();
346                }
347                let (width, height) = self.term_size().unwrap_or((0, 0));
348                Event::Resize { width, height }
349            }
350            Event::Key(Key::MousePress(button, row, col)) => {
351                // adjust mouse event position
352                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
353                if row < cursor_row {
354                    Event::__Nonexhaustive
355                } else {
356                    Event::Key(Key::MousePress(button, row - cursor_row, col))
357                }
358            }
359            Event::Key(Key::MouseRelease(row, col)) => {
360                // adjust mouse event position
361                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
362                if row < cursor_row {
363                    Event::__Nonexhaustive
364                } else {
365                    Event::Key(Key::MouseRelease(row - cursor_row, col))
366                }
367            }
368            Event::Key(Key::MouseHold(row, col)) => {
369                // adjust mouse event position
370                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
371                if row < cursor_row {
372                    Event::__Nonexhaustive
373                } else {
374                    Event::Key(Key::MouseHold(row - cursor_row, col))
375                }
376            }
377            Event::Key(Key::SingleClick(button, row, col)) => {
378                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
379                if row < cursor_row {
380                    Event::__Nonexhaustive
381                } else {
382                    Event::Key(Key::SingleClick(button, row - cursor_row, col))
383                }
384            }
385            Event::Key(Key::DoubleClick(button, row, col)) => {
386                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
387                if row < cursor_row {
388                    Event::__Nonexhaustive
389                } else {
390                    Event::Key(Key::DoubleClick(button, row - cursor_row, col))
391                }
392            }
393            Event::Key(Key::WheelUp(row, col, num)) => {
394                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
395                if row < cursor_row {
396                    Event::__Nonexhaustive
397                } else {
398                    Event::Key(Key::WheelUp(row - cursor_row, col, num))
399                }
400            }
401            Event::Key(Key::WheelDown(row, col, num)) => {
402                let cursor_row = self.term_lock.lock().get_term_start_row() as u16;
403                if row < cursor_row {
404                    Event::__Nonexhaustive
405                } else {
406                    Event::Key(Key::WheelDown(row - cursor_row, col, num))
407                }
408            }
409            ev => ev,
410        }
411    }
412
413    /// Wait an event up to `timeout` and return it
414    pub fn peek_event(&self, timeout: Duration) -> Result<Event<UserEvent>> {
415        let event_rx = self.event_rx.lock();
416        event_rx
417            .recv_timeout(timeout)
418            .map(|ev| self.filter_event(ev))
419            .map_err(|_| TuikitError::Timeout(timeout))
420    }
421
422    /// Wait for an event indefinitely and return it
423    pub fn poll_event(&self) -> Result<Event<UserEvent>> {
424        let event_rx = self.event_rx.lock();
425        event_rx
426            .recv()
427            .map(|ev| self.filter_event(ev))
428            .map_err(|err| TuikitError::ChannelReceiveError(err))
429    }
430
431    /// An interface to inject event to the terminal's event queue
432    pub fn send_event(&self, event: Event<UserEvent>) -> Result<()> {
433        let event_tx = self.event_tx.lock();
434        event_tx
435            .send(event)
436            .map_err(|err| TuikitError::SendEventError(err.to_string()))
437    }
438
439    /// Sync internal buffer with terminal
440    pub fn present(&self) -> Result<()> {
441        self.ensure_not_stopped()?;
442        let mut termlock = self.term_lock.lock();
443        termlock.present()
444    }
445
446    /// Return the printable size(width, height) of the term
447    pub fn term_size(&self) -> Result<(usize, usize)> {
448        self.ensure_not_stopped()?;
449        let termlock = self.term_lock.lock();
450        Ok(termlock.term_size()?)
451    }
452
453    /// Clear internal buffer
454    pub fn clear(&self) -> Result<()> {
455        self.ensure_not_stopped()?;
456        let mut termlock = self.term_lock.lock();
457        termlock.clear()
458    }
459
460    /// Change a cell of position `(row, col)` to `cell`
461    pub fn put_cell(&self, row: usize, col: usize, cell: Cell) -> Result<usize> {
462        self.ensure_not_stopped()?;
463        let mut termlock = self.term_lock.lock();
464        termlock.put_cell(row, col, cell)
465    }
466
467    /// Print `content` starting with position `(row, col)`
468    pub fn print(&self, row: usize, col: usize, content: &str) -> Result<usize> {
469        self.print_with_attr(row, col, content, Attr::default())
470    }
471
472    /// print `content` starting with position `(row, col)` with `attr`
473    pub fn print_with_attr(
474        &self,
475        row: usize,
476        col: usize,
477        content: &str,
478        attr: impl Into<Attr>,
479    ) -> Result<usize> {
480        self.ensure_not_stopped()?;
481        let mut termlock = self.term_lock.lock();
482        termlock.print_with_attr(row, col, content, attr)
483    }
484
485    /// Set cursor position to (row, col), and show the cursor
486    pub fn set_cursor(&self, row: usize, col: usize) -> Result<()> {
487        self.ensure_not_stopped()?;
488        let mut termlock = self.term_lock.lock();
489        termlock.set_cursor(row, col)
490    }
491
492    /// show/hide cursor, set `show` to `false` to hide the cursor
493    pub fn show_cursor(&self, show: bool) -> Result<()> {
494        self.ensure_not_stopped()?;
495        let mut termlock = self.term_lock.lock();
496        termlock.show_cursor(show)
497    }
498
499    /// Enable mouse support
500    pub fn enable_mouse_support(&self) -> Result<()> {
501        self.ensure_not_stopped()?;
502        let mut termlock = self.term_lock.lock();
503        termlock.enable_mouse_support()
504    }
505
506    /// Disable mouse support
507    pub fn disable_mouse_support(&self) -> Result<()> {
508        self.ensure_not_stopped()?;
509        let mut termlock = self.term_lock.lock();
510        termlock.disable_mouse_support()
511    }
512
513    /// Whether to clear the terminal upon exiting. Defaults to true.
514    pub fn clear_on_exit(&self, clear: bool) -> Result<()> {
515        self.ensure_not_stopped()?;
516        let mut termlock = self.term_lock.lock();
517        termlock.clear_on_exit(clear);
518        Ok(())
519    }
520
521    pub fn draw(&self, draw: &dyn Draw) -> Result<()> {
522        let mut canvas = TermCanvas { term: &self };
523        draw.draw(&mut canvas)
524            .map_err(|err| TuikitError::DrawError(err))
525    }
526
527    pub fn draw_mut(&self, draw: &mut dyn Draw) -> Result<()> {
528        let mut canvas = TermCanvas { term: &self };
529        draw.draw_mut(&mut canvas)
530            .map_err(|err| TuikitError::DrawError(err))
531    }
532}
533
534impl<'a, UserEvent: Send + 'static> Drop for Term<UserEvent> {
535    fn drop(&mut self) {
536        let _ = self.pause_internal(true);
537    }
538}
539
540pub struct TermCanvas<'a, UserEvent: Send + 'static> {
541    term: &'a Term<UserEvent>,
542}
543
544impl<'a, UserEvent: Send + 'static> Canvas for TermCanvas<'a, UserEvent> {
545    fn size(&self) -> Result<(usize, usize)> {
546        self.term.term_size()
547    }
548
549    fn clear(&mut self) -> Result<()> {
550        self.term.clear()
551    }
552
553    fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result<usize> {
554        self.term.put_cell(row, col, cell)
555    }
556
557    fn print_with_attr(
558        &mut self,
559        row: usize,
560        col: usize,
561        content: &str,
562        attr: Attr,
563    ) -> Result<usize> {
564        self.term.print_with_attr(row, col, content, attr)
565    }
566
567    fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> {
568        self.term.set_cursor(row, col)
569    }
570
571    fn show_cursor(&mut self, show: bool) -> Result<()> {
572        self.term.show_cursor(show)
573    }
574}
575
576struct TermLock {
577    prefer_height: TermHeight,
578    max_height: TermHeight,
579    min_height: TermHeight,
580    // keep bottom intact when resize?
581    bottom_intact: bool,
582    clear_on_exit: bool,
583    clear_on_start: bool,
584    mouse_enabled: bool,
585    alternate_screen: bool,
586    disable_alternate_screen: bool,
587    cursor_row: usize,
588    screen_height: usize,
589    screen_width: usize,
590    screen: Screen,
591    output: Option<Output>,
592}
593
594impl Default for TermLock {
595    fn default() -> Self {
596        Self {
597            prefer_height: TermHeight::Percent(100),
598            max_height: TermHeight::Percent(100),
599            min_height: TermHeight::Fixed(3),
600            bottom_intact: false,
601            alternate_screen: false,
602            disable_alternate_screen: false,
603            cursor_row: 0,
604            screen_height: 0,
605            screen_width: 0,
606            screen: Screen::new(0, 0),
607            output: None,
608            clear_on_exit: true,
609            clear_on_start: true,
610            mouse_enabled: false,
611        }
612    }
613}
614
615impl TermLock {
616    pub fn with_options(options: &TermOptions) -> Self {
617        let mut term = TermLock::default();
618        term.prefer_height = options.height;
619        term.max_height = options.max_height;
620        term.min_height = options.min_height;
621        term.clear_on_exit = options.clear_on_exit;
622        term.clear_on_start = options.clear_on_start;
623        term.screen.clear_on_start(options.clear_on_start);
624        term.disable_alternate_screen = options.disable_alternate_screen;
625        term.mouse_enabled = options.mouse_enabled;
626        term
627    }
628
629    /// Present the content to the terminal
630    pub fn present(&mut self) -> Result<()> {
631        let output = self
632            .output
633            .as_mut()
634            .ok_or(TuikitError::TerminalNotStarted)?;
635        let mut commands = self.screen.present();
636
637        let cursor_row = self.cursor_row;
638        // add cursor_row to all CursorGoto commands
639        for cmd in commands.iter_mut() {
640            if let Command::CursorGoto { row, col } = *cmd {
641                *cmd = Command::CursorGoto {
642                    row: row + cursor_row,
643                    col,
644                }
645            }
646        }
647
648        for cmd in commands.into_iter() {
649            output.execute(cmd);
650        }
651        output.flush();
652        Ok(())
653    }
654
655    /// Resize the internal buffer to according to new terminal size
656    pub fn on_resize(&mut self) -> Result<()> {
657        let output = self
658            .output
659            .as_mut()
660            .ok_or(TuikitError::TerminalNotStarted)?;
661        let (screen_width, screen_height) = output
662            .terminal_size()
663            .expect("term:restart get terminal size failed");
664        self.screen_height = screen_height;
665        self.screen_width = screen_width;
666
667        let width = screen_width;
668        let height = Self::calc_preferred_height(
669            &self.min_height,
670            &self.max_height,
671            &self.prefer_height,
672            screen_height,
673        );
674
675        // update the cursor position
676        if self.cursor_row + height >= screen_height {
677            self.bottom_intact = true;
678        }
679
680        if self.bottom_intact {
681            self.cursor_row = screen_height - height;
682        }
683
684        // clear the screen
685        let _ = output.cursor_goto(self.cursor_row, 0);
686        if self.clear_on_start {
687            let _ = output.erase_down();
688        }
689
690        // clear the screen buffer
691        self.screen.resize(width, height);
692        Ok(())
693    }
694
695    fn calc_height(height_spec: &TermHeight, actual_height: usize) -> usize {
696        match *height_spec {
697            TermHeight::Fixed(h) => h,
698            TermHeight::Percent(p) => actual_height * min(p, 100) / 100,
699        }
700    }
701
702    fn calc_preferred_height(
703        min_height: &TermHeight,
704        max_height: &TermHeight,
705        prefer_height: &TermHeight,
706        height: usize,
707    ) -> usize {
708        let max_height = Self::calc_height(max_height, height);
709        let min_height = Self::calc_height(min_height, height);
710        let prefer_height = Self::calc_height(prefer_height, height);
711
712        // ensure the calculated height is in range (MIN_HEIGHT, height)
713        let max_height = max(min(max_height, height), MIN_HEIGHT);
714        let min_height = max(min(min_height, height), MIN_HEIGHT);
715        max(min(prefer_height, max_height), min_height)
716    }
717
718    /// Pause the terminal
719    fn pause(&mut self, exiting: bool) -> Result<()> {
720        self.disable_mouse()?;
721        self.output.take().map(|mut output| {
722            output.show_cursor();
723            if self.clear_on_exit || !exiting {
724                // clear drawn contents
725                if !self.disable_alternate_screen {
726                    output.quit_alternate_screen();
727                } else {
728                    output.cursor_goto(self.cursor_row, 0);
729                    output.erase_down();
730                }
731            } else {
732                output.cursor_goto(self.cursor_row + self.screen.height(), 0);
733                if self.bottom_intact {
734                    output.write("\n");
735                }
736            }
737            output.flush();
738        });
739        Ok(())
740    }
741
742    /// ensure the screen had enough height
743    /// If the prefer height is full screen, it will enter alternate screen
744    /// otherwise it will ensure there are enough lines at the bottom
745    fn ensure_height(&mut self, cursor_pos: (usize, usize)) -> Result<()> {
746        let output = self
747            .output
748            .as_mut()
749            .ok_or(TuikitError::TerminalNotStarted)?;
750
751        // initialize
752
753        let (screen_width, screen_height) = output
754            .terminal_size()
755            .expect("termlock:ensure_height get terminal size failed");
756        let height_to_be = Self::calc_preferred_height(
757            &self.min_height,
758            &self.max_height,
759            &self.prefer_height,
760            screen_height,
761        );
762
763        self.alternate_screen = false;
764        let (mut cursor_row, cursor_col) = cursor_pos;
765        if height_to_be >= screen_height {
766            // whole screen
767            self.alternate_screen = true;
768            self.bottom_intact = false;
769            self.cursor_row = 0;
770            if !self.disable_alternate_screen {
771                output.enter_alternate_screen();
772            }
773        } else {
774            // only use part of the screen
775
776            // go to a new line so that existing line won't be messed up
777            if cursor_col > 0 {
778                output.write("\n");
779                cursor_row += 1;
780            }
781
782            if (cursor_row + height_to_be) <= screen_height {
783                self.bottom_intact = false;
784                self.cursor_row = cursor_row;
785            } else {
786                for _ in 0..(height_to_be - 1) {
787                    output.write("\n");
788                }
789                self.bottom_intact = true;
790                self.cursor_row = min(cursor_row, screen_height - height_to_be);
791            }
792        }
793
794        output.cursor_goto(self.cursor_row, 0);
795        output.flush();
796        self.screen_height = screen_height;
797        self.screen_width = screen_width;
798        Ok(())
799    }
800
801    /// get the start row of the terminal
802    pub fn get_term_start_row(&self) -> usize {
803        self.cursor_row
804    }
805
806    /// restart the terminal
807    pub fn restart(&mut self, output: Output, cursor_pos: (usize, usize)) -> Result<()> {
808        // ensure the output area had enough height
809        self.output.replace(output);
810        self.ensure_height(cursor_pos)?;
811        self.on_resize()?;
812        if self.mouse_enabled {
813            self.enable_mouse()?;
814        }
815        Ok(())
816    }
817
818    /// return the printable size(width, height) of the term
819    pub fn term_size(&self) -> Result<(usize, usize)> {
820        self.screen.size()
821    }
822
823    /// clear internal buffer
824    pub fn clear(&mut self) -> Result<()> {
825        self.screen.clear()
826    }
827
828    /// change a cell of position `(row, col)` to `cell`
829    pub fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result<usize> {
830        self.screen.put_cell(row, col, cell)
831    }
832
833    /// print `content` starting with position `(row, col)`
834    pub fn print_with_attr(
835        &mut self,
836        row: usize,
837        col: usize,
838        content: &str,
839        attr: impl Into<Attr>,
840    ) -> Result<usize> {
841        self.screen.print_with_attr(row, col, content, attr.into())
842    }
843
844    /// set cursor position to (row, col)
845    pub fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> {
846        self.screen.set_cursor(row, col)
847    }
848
849    /// show/hide cursor, set `show` to `false` to hide the cursor
850    pub fn show_cursor(&mut self, show: bool) -> Result<()> {
851        self.screen.show_cursor(show)
852    }
853
854    /// Enable mouse support
855    pub fn enable_mouse_support(&mut self) -> Result<()> {
856        self.mouse_enabled = true;
857        self.enable_mouse()
858    }
859
860    /// Disable mouse support
861    pub fn disable_mouse_support(&mut self) -> Result<()> {
862        self.mouse_enabled = false;
863        self.disable_mouse()
864    }
865
866    pub fn clear_on_exit(&mut self, clear: bool) {
867        self.clear_on_exit = clear;
868    }
869
870    /// Enable mouse (send ANSI codes to enable mouse)
871    fn enable_mouse(&mut self) -> Result<()> {
872        let output = self
873            .output
874            .as_mut()
875            .ok_or(TuikitError::TerminalNotStarted)?;
876        output.enable_mouse_support();
877        Ok(())
878    }
879
880    /// Disable mouse (send ANSI codes to disable mouse)
881    fn disable_mouse(&mut self) -> Result<()> {
882        let output = self
883            .output
884            .as_mut()
885            .ok_or(TuikitError::TerminalNotStarted)?;
886        output.disable_mouse_support();
887        Ok(())
888    }
889}
890
891impl Drop for TermLock {
892    fn drop(&mut self) {
893        let _ = self.pause(true);
894    }
895}