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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! `Output` is the output stream that deals with ANSI Escape codes.
//! normally you should not use it directly.
//!
//! ```
//! use std::io;
//! use tuikit::attr::Color;
//! use tuikit::output::Output;
//!
//! let mut output = Output::new(Box::new(io::stdout())).unwrap();
//! output.set_fg(Color::YELLOW);
//! output.write("YELLOW\n");
//! output.flush();
//!
//! ```

use std::io;
use std::io::Write;
use std::os::unix::io::AsRawFd;

use crate::attr::{Attr, Color, Effect};
use crate::sys::size::terminal_size;

use term::terminfo::parm::{expand, Param, Variables};
use term::terminfo::TermInfo;

// modeled after python-prompt-toolkit
// term info: https://ftp.netbsd.org/pub/NetBSD/NetBSD-release-7/src/share/terminfo/terminfo

const DEFAULT_BUFFER_SIZE: usize = 1024;

/// Output is an abstraction over the ANSI codes.
pub struct Output {
    /// A callable which returns the `Size` of the output terminal.
    buffer: Vec<u8>,
    stdout: Box<dyn WriteAndAsRawFdAndSend>,
    /// The terminal environment variable. (xterm, xterm-256color, linux, ...)
    terminfo: TermInfo,
}

pub trait WriteAndAsRawFdAndSend: Write + AsRawFd + Send {}

impl<T> WriteAndAsRawFdAndSend for T where T: Write + AsRawFd + Send {}

impl Output {
    pub fn new(stdout: Box<dyn WriteAndAsRawFdAndSend>) -> io::Result<Self> {
        Result::Ok(Self {
            buffer: Vec::with_capacity(DEFAULT_BUFFER_SIZE),
            stdout,
            terminfo: TermInfo::from_env()?,
        })
    }

    fn write_cap(&mut self, cmd: &str) {
        self.write_cap_with_params(cmd, &[])
    }

    fn write_cap_with_params(&mut self, cap: &str, params: &[Param]) {
        if let Some(cmd) = self.terminfo.strings.get(cap) {
            if let Ok(s) = expand(cmd, params, &mut Variables::new()) {
                self.buffer.extend(&s);
            }
        }
    }

    /// Write text (Terminal escape sequences will be removed/escaped.)
    pub fn write(&mut self, data: &str) {
        self.buffer.extend(data.replace("\x1b", "?").as_bytes());
    }

    /// Write raw texts to the terminal.
    pub fn write_raw(&mut self, data: &[u8]) {
        self.buffer.extend_from_slice(data);
    }

    /// Return the encoding for this output, e.g. 'utf-8'.
    /// (This is used mainly to know which characters are supported by the
    /// output the data, so that the UI can provide alternatives, when
    /// required.)
    pub fn encoding(&self) -> &str {
        unimplemented!()
    }

    /// Set terminal title.
    pub fn set_title(&mut self, title: &str) {
        if self.terminfo.names.contains(&"linux".to_string())
            || self.terminfo.names.contains(&"eterm-color".to_string())
        {
            return;
        }

        let title = title.replace("\x1b", "").replace("\x07", "");
        self.write_raw(format!("\x1b]2;{}\x07", title).as_bytes());
    }

    /// Clear title again. (or restore previous title.)
    pub fn clear_title(&mut self) {
        self.set_title("");
    }

    /// Write to output stream and flush.
    pub fn flush(&mut self) {
        let _ = self.stdout.write(&self.buffer);
        self.buffer.clear();
        let _ = self.stdout.flush();
    }

    /// Erases the screen with the background colour and moves the cursor to home.
    pub fn erase_screen(&mut self) {
        self.write_cap("clear");
    }

    /// Go to the alternate screen buffer. (For full screen applications).
    pub fn enter_alternate_screen(&mut self) {
        self.write_cap("smcup");
    }

    /// Leave the alternate screen buffer.
    pub fn quit_alternate_screen(&mut self) {
        self.write_cap("rmcup");
    }

    /// Enable mouse.
    pub fn enable_mouse_support(&mut self) {
        self.write_raw("\x1b[?1000h".as_bytes());

        // Enable urxvt Mouse mode. (For terminals that understand this.)
        self.write_raw("\x1b[?1015h".as_bytes());

        // Also enable Xterm SGR mouse mode. (For terminals that understand this.)
        self.write_raw("\x1b[?1006h".as_bytes());

        // Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr extensions.
    }

    /// Disable mouse.
    pub fn disable_mouse_support(&mut self) {
        self.write_raw("\x1b[?1000l".as_bytes());
        self.write_raw("\x1b[?1015l".as_bytes());
        self.write_raw("\x1b[?1006l".as_bytes());
    }

    /// Erases from the current cursor position to the end of the current line.
    pub fn erase_end_of_line(&mut self) {
        self.write_cap("el");
    }

    /// Erases the screen from the current line down to the bottom of the screen.
    pub fn erase_down(&mut self) {
        self.write_cap("ed");
    }

    /// Reset color and styling attributes.
    pub fn reset_attributes(&mut self) {
        self.write_cap("sgr0");
    }

    /// Set current foreground color
    pub fn set_fg(&mut self, color: Color) {
        match color {
            Color::Default => {
                self.write_raw("\x1b[39m".as_bytes());
            }
            Color::AnsiValue(x) => {
                self.write_cap_with_params("setaf", &[Param::Number(x as i32)]);
            }
            Color::Rgb(r, g, b) => {
                self.write_raw(format!("\x1b[38;2;{};{};{}m", r, g, b).as_bytes());
            }
            Color::__Nonexhaustive => unreachable!(),
        }
    }

    /// Set current background color
    pub fn set_bg(&mut self, color: Color) {
        match color {
            Color::Default => {
                self.write_raw("\x1b[49m".as_bytes());
            }
            Color::AnsiValue(x) => {
                self.write_cap_with_params("setab", &[Param::Number(x as i32)]);
            }
            Color::Rgb(r, g, b) => {
                self.write_raw(format!("\x1b[48;2;{};{};{}m", r, g, b).as_bytes());
            }
            Color::__Nonexhaustive => unreachable!(),
        }
    }

    /// Set current effect (underline, bold, etc)
    pub fn set_effect(&mut self, effect: Effect) {
        if effect.contains(Effect::BOLD) {
            self.write_cap("bold");
        }
        if effect.contains(Effect::DIM) {
            self.write_cap("dim");
        }
        if effect.contains(Effect::UNDERLINE) {
            self.write_cap("smul");
        }
        if effect.contains(Effect::BLINK) {
            self.write_cap("blink");
        }
        if effect.contains(Effect::REVERSE) {
            self.write_cap("rev");
        }
    }

    /// Set new color and styling attributes.
    pub fn set_attribute(&mut self, attr: Attr) {
        self.set_fg(attr.fg);
        self.set_bg(attr.bg);
        self.set_effect(attr.effect);
    }

    /// Disable auto line wrapping.
    pub fn disable_autowrap(&mut self) {
        self.write_cap("rmam");
    }

    /// Enable auto line wrapping.
    pub fn enable_autowrap(&mut self) {
        self.write_cap("smam");
    }

    /// Move cursor position.
    pub fn cursor_goto(&mut self, row: usize, column: usize) {
        self.write_cap_with_params(
            "cup",
            &[Param::Number(row as i32), Param::Number(column as i32)],
        );
    }

    /// Move cursor `amount` place up.
    pub fn cursor_up(&mut self, amount: usize) {
        match amount {
            0 => {}
            1 => self.write_cap("cuu1"),
            _ => self.write_cap_with_params("cuu", &[Param::Number(amount as i32)]),
        }
    }

    /// Move cursor `amount` place down.
    pub fn cursor_down(&mut self, amount: usize) {
        match amount {
            0 => {}
            1 => self.write_cap("cud1"),
            _ => self.write_cap_with_params("cud", &[Param::Number(amount as i32)]),
        }
    }

    /// Move cursor `amount` place forward.
    pub fn cursor_forward(&mut self, amount: usize) {
        match amount {
            0 => {}
            1 => self.write_cap("cuf1"),
            _ => self.write_cap_with_params("cuf", &[Param::Number(amount as i32)]),
        }
    }

    /// Move cursor `amount` place backward.
    pub fn cursor_backward(&mut self, amount: usize) {
        match amount {
            0 => {}
            1 => self.write_cap("cub1"),
            _ => self.write_cap_with_params("cub", &[Param::Number(amount as i32)]),
        }
    }

    /// Hide cursor.
    pub fn hide_cursor(&mut self) {
        self.write_cap("civis");
    }

    /// Show cursor.
    pub fn show_cursor(&mut self) {
        self.write_cap("cnorm");
    }

    /// Asks for a cursor position report (CPR). (VT100 only.)
    pub fn ask_for_cpr(&mut self) {
        self.write_raw("\x1b[6n".as_bytes());
        self.flush()
    }

    /// Sound bell.
    pub fn bell(&mut self) {
        self.write_cap("bel");
        self.flush()
    }

    /// get terminal size (width, height)
    pub fn terminal_size(&self) -> io::Result<(usize, usize)> {
        terminal_size(self.stdout.as_raw_fd())
    }

    /// For vt100/xterm etc.
    pub fn enable_bracketed_paste(&mut self) {
        self.write_raw("\x1b[?2004h".as_bytes());
    }

    /// For vt100/xterm etc.
    pub fn disable_bracketed_paste(&mut self) {
        self.write_raw("\x1b[?2004l".as_bytes());
    }

    ///  Execute the command
    pub fn execute(&mut self, cmd: Command) {
        match cmd {
            Command::PutChar(c) => self.write(c.to_string().as_str()),
            Command::Write(content) => self.write(&content),
            Command::SetTitle(title) => self.set_title(&title),
            Command::ClearTitle => self.clear_title(),
            Command::Flush => self.flush(),
            Command::EraseScreen => self.erase_screen(),
            Command::AlternateScreen(enable) => {
                if enable {
                    self.enter_alternate_screen()
                } else {
                    self.quit_alternate_screen()
                }
            }
            Command::MouseSupport(enable) => {
                if enable {
                    self.enable_mouse_support();
                } else {
                    self.disable_mouse_support();
                }
            }
            Command::EraseEndOfLine => self.erase_end_of_line(),
            Command::EraseDown => self.erase_down(),
            Command::ResetAttributes => self.reset_attributes(),
            Command::Fg(fg) => self.set_fg(fg),
            Command::Bg(bg) => self.set_bg(bg),
            Command::Effect(effect) => self.set_effect(effect),
            Command::SetAttribute(attr) => self.set_attribute(attr),
            Command::AutoWrap(enable) => {
                if enable {
                    self.enable_autowrap();
                } else {
                    self.disable_autowrap();
                }
            }
            Command::CursorGoto { row, col } => self.cursor_goto(row, col),
            Command::CursorUp(amount) => self.cursor_up(amount),
            Command::CursorDown(amount) => self.cursor_down(amount),
            Command::CursorLeft(amount) => self.cursor_backward(amount),
            Command::CursorRight(amount) => self.cursor_forward(amount),
            Command::CursorShow(show) => {
                if show {
                    self.show_cursor()
                } else {
                    self.hide_cursor()
                }
            }
            Command::BracketedPaste(enable) => {
                if enable {
                    self.enable_bracketed_paste()
                } else {
                    self.disable_bracketed_paste()
                }
            }
        }
    }
}

/// Instead of calling functions of `Output`, we could send commands.
#[derive(Debug, Clone)]
pub enum Command {
    /// Put a char to screen
    PutChar(char),
    /// Write content to screen (escape codes will be escaped)
    Write(String),
    /// Set the title of the terminal
    SetTitle(String),
    /// Clear the title of the terminal
    ClearTitle,
    /// Flush all the buffered contents
    Flush,
    /// Erase the entire screen
    EraseScreen,
    /// Enter(true)/Quit(false) the alternate screen mode
    AlternateScreen(bool),
    /// Enable(true)/Disable(false) mouse support
    MouseSupport(bool),
    /// Erase contents to the end of current line
    EraseEndOfLine,
    /// Erase contents till the bottom of the screen
    EraseDown,
    /// Reset attributes
    ResetAttributes,
    /// Set the foreground color
    Fg(Color),
    /// Set the background color
    Bg(Color),
    /// Set the effect(e.g. underline, dim, bold, ...)
    Effect(Effect),
    /// Set the fg, bg & effect.
    SetAttribute(Attr),
    /// Enable(true)/Disable(false) autowrap
    AutoWrap(bool),
    /// move the cursor to `(row, col)`
    CursorGoto { row: usize, col: usize },
    /// move cursor up `x` lines
    CursorUp(usize),
    /// move cursor down `x` lines
    CursorDown(usize),
    /// move cursor left `x` characters
    CursorLeft(usize),
    /// move cursor right `x` characters
    CursorRight(usize),
    /// Show(true)/Hide(false) cursor
    CursorShow(bool),
    /// Enable(true)/Disable(false) the bracketed paste mode
    BracketedPaste(bool),
}