1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright (c) The Diem Core Contributors
// SPDX-License-Identifier: Apache-2.0

#![forbid(unsafe_code)]

pub mod text_builder;
pub mod tui_interface;

use crate::tui::tui_interface::TUIInterface;
use std::{
    error::Error,
    io::{self, Stdin, Write},
};
use termion::{
    event::Key,
    input::{Keys, TermRead},
};
use tui::{
    backend::TermionBackend,
    layout::{Constraint, Direction, Layout},
    style::Style,
    widgets::{Block, Borders, Paragraph},
    Frame,
};

pub struct TUI<Interface: TUIInterface> {
    current_line_number: u16,
    current_column: u16,
    interface: Interface,
    keys: Keys<Stdin>,
}

impl<Interface: TUIInterface> TUI<Interface> {
    pub fn new(interface: Interface) -> Self {
        let keys = io::stdin().keys();
        Self {
            current_line_number: 0,
            current_column: 0,
            interface,
            keys,
        }
    }

    pub fn redraw<W: Write>(&mut self, f: &mut Frame<TermionBackend<W>>) {
        // Create a split window, each pane using 50% of the screen
        let window = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
            .split(f.size());

        // Get the bottom offset of the window that the cursor will be displayed
        let window_size = window[0].bottom();
        // The window will need to be scrolled if the cursor is more than halfway down the screen.
        let scroll = if self.current_line_number > window_size / 2 {
            self.current_line_number
                .checked_sub(window_size / 2)
                .unwrap()
        } else {
            0
        };

        // Get the output for the current line/column of the cursore
        let current_interface = self
            .interface
            .on_redraw(self.current_line_number, self.current_column);

        // Left window logic
        // Create a paragraph for our text, and scroll it if need be (by the amount computed above)
        let input = Paragraph::new(current_interface.left_screen)
            .style(Style::default())
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title(Interface::LEFT_TITLE),
            )
            .scroll((scroll, 0));

        // Set the cursor position in the left window. Numbers incremented by 1 since the screen
        // border is at position 0 in both x and y coordinates. If we scrolled the text, we need
        // to subtract that from the line number so that the cursor is over the correct line.
        f.set_cursor(
            self.current_column.checked_add(1).unwrap(),
            self.current_line_number
                .checked_add(1)
                .unwrap()
                .checked_sub(scroll)
                .unwrap(),
        );
        f.render_widget(input, window[0]);

        // Right window logic
        // Render the output text. No other logic needed.
        let output = Paragraph::new(current_interface.right_screen)
            .style(Style::default())
            .block(
                Block::default()
                    .title(Interface::RIGHT_TITLE)
                    .borders(Borders::ALL),
            );
        f.render_widget(output, window[1])
    }

    /// Handles keyboard input, and updates state according to those key presses.
    /// Down, Up => move the cursor up or down a line
    /// Left, Right => move the cursor to the previous (resp. next) character on the current line
    /// ESC, q => exit
    pub fn handle_input(&mut self) -> Result<bool, Box<dyn Error>> {
        if let Some(key) = self.keys.next() {
            match key.unwrap() {
                // Exit
                Key::Esc | Key::Char('q') => {
                    return Ok(true);
                }
                // Update current line number to move the cursor up a line. If the column number is
                // past the end of the new line we're on, bound the column number so it isn't past
                // the last character on the new line.
                Key::Up => {
                    self.current_line_number = self.current_line_number.saturating_sub(1);
                    self.current_column = self
                        .interface
                        .bound_column(self.current_line_number, self.current_column);
                }
                // Update current line number to move the cursor down a line. If the column number is
                // past the end of the new line we're on, bound the column number so it isn't past
                // the last character on the new line.
                Key::Down => {
                    self.current_line_number = self
                        .interface
                        .bound_line(self.current_line_number.checked_add(1).unwrap());
                    self.current_column = self
                        .interface
                        .bound_column(self.current_line_number, self.current_column);
                }
                // Move the cursor to the next character on the current line. Number is bounded by
                // the length of the current line.
                Key::Right => {
                    self.current_column = self.interface.bound_column(
                        self.current_line_number,
                        self.current_column.checked_add(1).unwrap(),
                    );
                }
                // Move the cursor to the previous character on the current line. Number is bounded by
                // the length of the current line.
                Key::Left => {
                    self.current_column = self.current_column.saturating_sub(1);
                }
                _ => {}
            }
        }
        Ok(false)
    }
}