iq_cli/
shell.rs

1//! Terminal handling code
2//!
3//! Portions of this code are borrowed from Cargo
4
5use libc::isatty;
6use std::fmt;
7use std::io;
8use std::io::prelude::*;
9use term::{self, Attr, TerminfoTerminal};
10use term::Terminal as RawTerminal;
11use term::color::{Color, BLACK};
12
13/// Color configuration
14#[derive(Clone, Copy, PartialEq)]
15pub enum ColorConfig {
16    /// Pick colors automatically based on whether we're using a TTY
17    Auto,
18
19    /// Always use colors
20    Always,
21
22    /// Never use colors
23    Never,
24}
25
26impl fmt::Display for ColorConfig {
27    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28        match *self {
29            ColorConfig::Auto => "auto",
30            ColorConfig::Always => "always",
31            ColorConfig::Never => "never",
32        }.fmt(f)
33    }
34}
35
36/// Shell configuration options
37#[derive(Clone, Copy)]
38pub struct ShellConfig {
39    /// Color configuration
40    pub color_config: ColorConfig,
41
42    /// Are we using a TTY?
43    pub tty: bool,
44}
45
46impl Default for ShellConfig {
47    fn default() -> Self {
48        Self {
49            color_config: ColorConfig::Auto,
50            tty: is_tty(),
51        }
52    }
53}
54
55/// Is STDOUT a tty?
56fn is_tty() -> bool {
57    #[allow(unsafe_code)]
58    unsafe {
59        isatty(0) == 1
60    }
61}
62
63enum Terminal {
64    NoColor(Box<Write + Send>),
65    Colored(Box<RawTerminal<Output = Box<Write + Send>> + Send>),
66}
67
68/// Terminal shell we interact with
69pub struct Shell {
70    terminal: Terminal,
71    config: ShellConfig,
72}
73
74impl Shell {
75    /// Create a new shell
76    pub fn create<T: FnMut() -> Box<Write + Send>>(mut out_fn: T, config: ShellConfig) -> Shell {
77        let terminal = Shell::get_term(out_fn()).unwrap_or_else(|_| Terminal::NoColor(out_fn()));
78        Shell { terminal, config }
79    }
80
81    /// Get the shell's Terminal
82    fn get_term(out: Box<Write + Send>) -> term::Result<Terminal> {
83        Ok(Shell::get_terminfo_term(out))
84    }
85
86    /// Get the terminfo Terminal
87    fn get_terminfo_term(out: Box<Write + Send>) -> Terminal {
88        match ::term::terminfo::TermInfo::from_env() {
89            Ok(ti) => {
90                let term = TerminfoTerminal::new_with_terminfo(out, ti);
91                if term.supports_color() {
92                    Terminal::Colored(Box::new(term))
93                } else {
94                    Terminal::NoColor(term.into_inner())
95                }
96            }
97            Err(_) => Terminal::NoColor(out),
98        }
99    }
100
101    /// Say a status message with the given color
102    pub fn status<T, U>(
103        &mut self,
104        color: Color,
105        status: T,
106        message: U,
107        justified: bool,
108    ) -> term::Result<()>
109    where
110        T: fmt::Display,
111        U: fmt::Display,
112    {
113        self.reset()?;
114
115        if color != BLACK {
116            self.fg(color)?;
117        }
118
119        if self.supports_attr(Attr::Bold) {
120            self.attr(Attr::Bold)?;
121        }
122
123        if justified {
124            write!(self, "{:>12}", status.to_string())?;
125        } else {
126            write!(self, "{}", status)?;
127        }
128
129        self.reset()?;
130        write!(self, " {}\n", message)?;
131        self.flush()?;
132
133        Ok(())
134    }
135
136    fn fg(&mut self, color: Color) -> term::Result<bool> {
137        let colored = self.colored();
138
139        match self.terminal {
140            Terminal::Colored(ref mut c) if colored => c.fg(color)?,
141            _ => return Ok(false),
142        }
143
144        Ok(true)
145    }
146
147    fn attr(&mut self, attr: Attr) -> term::Result<bool> {
148        let colored = self.colored();
149
150        match self.terminal {
151            Terminal::Colored(ref mut c) if colored => c.attr(attr)?,
152            _ => return Ok(false),
153        }
154
155        Ok(true)
156    }
157
158    fn supports_attr(&self, attr: Attr) -> bool {
159        let colored = self.colored();
160
161        match self.terminal {
162            Terminal::Colored(ref c) if colored => c.supports_attr(attr),
163            _ => false,
164        }
165    }
166
167    fn reset(&mut self) -> term::Result<()> {
168        let colored = self.colored();
169
170        match self.terminal {
171            Terminal::Colored(ref mut c) if colored => c.reset()?,
172            _ => (),
173        }
174
175        Ok(())
176    }
177
178    fn colored(&self) -> bool {
179        self.config.tty && ColorConfig::Auto == self.config.color_config
180            || ColorConfig::Always == self.config.color_config
181    }
182}
183
184impl Default for Shell {
185    fn default() -> Self {
186        Shell::create(|| Box::new(io::stdout()), ShellConfig::default())
187    }
188}
189
190impl Write for Shell {
191    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
192        match self.terminal {
193            Terminal::Colored(ref mut c) => c.write(buf),
194            Terminal::NoColor(ref mut n) => n.write(buf),
195        }
196    }
197
198    fn flush(&mut self) -> io::Result<()> {
199        match self.terminal {
200            Terminal::Colored(ref mut c) => c.flush(),
201            Terminal::NoColor(ref mut n) => n.flush(),
202        }
203    }
204}