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
use std::io::{self, Write, Read, ErrorKind};
use std::fs::File;
use std::error;

use unicode_width::UnicodeWidthChar;

use crate::input_parser::{parse_input, InputKeyIter};
use crate::Buf;

pub trait Ext: Write {
    fn cursor_backspace(&mut self) -> io::Result<()> {
        self.write_all(b"\x08")?;

        Ok(())
    }

    fn cursor_up(&mut self) -> io::Result<()> {
        self.write_all(b"\x1b[A")?;

        Ok(())
    }

    fn cursor_up_by(&mut self, by: usize) -> io::Result<()> {
        if by == 0 { return Ok(()) };
        self.write_all(b"\x1b[")?;
        itoa::write(&mut *self, by)?; // TODO: should we check the bytes written?
        self.write_all(b"A")?;

        Ok(())
    }

    fn cursor_down_by(&mut self, by: usize) -> io::Result<()> {
        // If the amount is less than 4, it's cheaper to just print line breaks.
        if by < 4 {
            self.write_all(&[b'\n'; 3][0..by])?;
        } else {
            self.write_all(b"\x1b[")?;
            itoa::write(&mut *self, by)?; // TODO: should we check the bytes written?
            self.write_all(b"B")?;
        }

        Ok(())
    }

    fn hide_cursor(&mut self) -> io::Result<()> {
        Ok(self.write_all(b"\x1b[?25l")?)
    }

    fn show_cursor(&mut self) -> io::Result<()> {
        Ok(self.write_all(b"\x1b[?25h")?)
    }
}

impl<W: Write> Ext for W {}

pub struct TermIn {
    tty: File,
    input_buf: Vec<u8>,
}

pub struct TermOut {
    tty: File,
    output_buf: Vec<u8>,
    square_context: bool,
}

impl TermOut {
    pub fn write_frame(&mut self, buf: &Buf) -> io::Result<()> {
        for line in buf.lines() {
            let mut utf8_len = 0;
            for character in line {
                utf8_len += character.encode_utf8(&mut self.output_buf[utf8_len..]).len();
                if self.square_context && character.width_cjk() == Some(2) {
                    self.output_buf[utf8_len] = b'\x08';
                    utf8_len += 1;
                }
            }
            self.tty.write_all(&self.output_buf[..utf8_len])?;
            self.tty.write_all(b"\r\n")?;
        }
        self.tty.cursor_up_by(buf.height())?;
        self.tty.flush()?;
        Ok(())
    }

    pub fn cleanup(&mut self, buf: &Buf) -> io::Result<()> {
        self.tty.cursor_down_by(buf.height())?;
        self.tty.show_cursor()?;
        self.tty.flush()?;
        Ok(())
    }
}

impl TermIn {
    pub fn read_input(&mut self) -> Result<InputKeyIter<'_>, Box<dyn error::Error>> {
        let bytes_read = match self.tty.read(&mut self.input_buf) {
            Ok(n) => Ok(n),
            Err(e) => match e.kind() {
                ErrorKind::WouldBlock => Ok(0),
                _ => Err(e),
            },
        }?;
        let input_buf = &self.input_buf[0..bytes_read];

        Ok(parse_input(input_buf)?)
    }
}

pub fn get_default_term() -> io::Result<(TermIn, TermOut)> {
    use std::os::unix::io::AsRawFd;
    use termios::{self, Termios};
    use std::fs::OpenOptions;
    use std::os::unix::fs::OpenOptionsExt;

    let mut tty_in = OpenOptions::new()
        .read(true)
        .write(false)
        .custom_flags(libc::O_NONBLOCK)
        .open("/dev/tty")?;

    let orig_in = Termios::from_fd(tty_in.as_raw_fd())?;
    let mut raw_in = orig_in;
    termios::cfmakeraw(&mut raw_in);
    termios::tcsetattr(tty_in.as_raw_fd(),termios::TCSANOW,&raw_in)?;
    tty_in.flush()?;

    let mut tty_out = OpenOptions::new()
        .read(false)
        .write(true)
        .open("/dev/tty")?;

    tty_out.hide_cursor()?;

    Ok((TermIn { tty: tty_in, input_buf: vec![b'\0'; 256] },
        TermOut { tty: tty_out, output_buf: vec![b'\0'; 256], square_context: true }))
}