summaryrefslogtreecommitdiffstats
path: root/src/terminal_win.rs
blob: 89f1e734206c2889c0d7ae812d626d31c02b7c01 (plain)
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
/*!
This module contains a Windows-only *in-memory* implementation of the
`term::Terminal` trait.

This particular implementation is a bit idiosyncratic, and the "in-memory"
specification is to blame. In particular, on Windows, coloring requires
communicating with the console synchronously as data is written to stdout.
This is anathema to how ripgrep fundamentally works: by writing search results
to intermediate thread local buffers in order to maximize parallelism.

Eliminating parallelism on Windows isn't an option, because that would negate
a tremendous performance benefit just for coloring.

We've worked around this by providing an implementation of `term::Terminal`
that records precisely where a color or a reset should be invoked, according
to a byte offset in the in memory buffer. When the buffer is actually printed,
we copy the bytes from the buffer to stdout incrementally while invoking the
corresponding console APIs for coloring at the right location.

(Another approach would be to do ANSI coloring unconditionally, then parse that
and translate it to console commands. The advantage of that approach is that
it doesn't require any additional memory for storing offsets. In practice
though, coloring is only used in the terminal, which tends to correspond to
searches that produce very few results with respect to the corpus searched.
Therefore, this is an acceptable trade off. Namely, we do not pay for it when
coloring is disabled.
*/
use std::io;

use term::{self, Terminal};
use term::color::Color;

/// An in-memory buffer that provides Windows console coloring.
#[derive(Clone, Debug)]
pub struct WindowsBuffer {
    buf: Vec<u8>,
    pos: usize,
    colors: Vec<WindowsColor>,
}

/// A color associated with a particular location in a buffer.
#[derive(Clone, Debug)]
struct WindowsColor {
    pos: usize,
    opt: WindowsOption,
}

/// A color or reset directive that can be translated into an instruction to
/// the Windows console.
#[derive(Clone, Debug)]
enum WindowsOption {
    Foreground(Color),
    Background(Color),
    Reset,
}

impl WindowsBuffer {
    /// Create a new empty buffer for Windows console coloring.
    pub fn new() -> WindowsBuffer {
        WindowsBuffer {
            buf: vec![],
            pos: 0,
            colors: vec![],
        }
    }

    fn push(&mut self, opt: WindowsOption) {
        let pos = self.pos;
        self.colors.push(WindowsColor { pos: pos, opt: opt });
    }

    /// Print the contents to the given terminal.
    pub fn print_stdout<T: Terminal + Send>(&self, tt: &mut T) {
        if !tt.supports_color() {
            let _ = tt.write_all(&self.buf);
            let _ = tt.flush();
            return;
        }
        let mut last = 0;
        for col in &self.colors {
            let _ = tt.write_all(&self.buf[last..col.pos]);
            match col.opt {
                WindowsOption::Foreground(c) => {
                    let _ = tt.fg(c);
                }
                WindowsOption::Background(c) => {
                    let _ = tt.bg(c);
                }
                WindowsOption::Reset => {
                    let _ = tt.reset();
                }
            }
            last = col.pos;
        }
        let _ = tt.write_all(&self.buf[last..]);
        let _ = tt.flush();
    }

    /// Clear the buffer.
    pub fn clear(&mut self) {
        self.buf.clear();
        self.colors.clear();
        self.pos = 0;
    }
}

impl io::Write for WindowsBuffer {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let n = try!(self.buf.write(buf));
        self.pos += n;
        Ok(n)
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

impl Terminal for WindowsBuffer {
    type Output = Vec<u8>;

    fn fg(&mut self, fg: Color) -> term::Result<()> {
        self.push(WindowsOption::Foreground(fg));
        Ok(())
    }

    fn bg(&mut self, bg: Color) -> term::Result<()> {
        self.push(WindowsOption::Background(bg));
        Ok(())
    }

    fn attr(&mut self, _attr: term::Attr) -> term::Result<()> {
        Err(term::Error::NotSupported)
    }

    fn supports_attr(&self, _attr: term::Attr) -> bool {
        false
    }

    fn reset(&mut self) -> term::Result<()> {
        self.push(WindowsOption::Reset);
        Ok(())
    }

    fn supports_reset(&self) -> bool {
        true
    }

    fn supports_color(&self) -> bool {
        true
    }

    fn cursor_up(&mut self) -> term::Result<()> {
        Err(term::Error::NotSupported)
    }

    fn delete_line(&mut self) -> term::Result<()> {
        Err(term::Error::NotSupported)
    }

    fn carriage_return(&mut self) -> term::Result<()> {
        Err(term::Error::NotSupported)
    }

    fn get_ref(&self) -> &Vec<u8> {
        &self.buf
    }

    fn get_mut(&mut self) -> &mut Vec<u8> {
        &mut self.buf
    }

    fn into_inner(self) -> Vec<u8> {
        self.buf
    }
}