1use std::io::{self, Write};
4
5use urushi_terminal::{ColorLevel, TerminalDetection};
6
7use crate::{Available, RenderSettings, StyledText, View, render, render_text, resolve};
8
9pub fn print_view(view: &View) -> io::Result<()> {
11 let stdout = io::stdout();
12 let detection = urushi_terminal::detect(&stdout)?;
13 write_view_detected(stdout.lock(), view, detection, false, no_color())
14}
15
16pub fn println_view(view: &View) -> io::Result<()> {
18 let stdout = io::stdout();
19 let detection = urushi_terminal::detect(&stdout)?;
20 write_view_detected(stdout.lock(), view, detection, true, no_color())
21}
22
23pub fn eprint_view(view: &View) -> io::Result<()> {
25 let stderr = io::stderr();
26 let detection = urushi_terminal::detect(&stderr)?;
27 write_view_detected(stderr.lock(), view, detection, false, no_color())
28}
29
30pub fn eprintln_view(view: &View) -> io::Result<()> {
32 let stderr = io::stderr();
33 let detection = urushi_terminal::detect(&stderr)?;
34 write_view_detected(stderr.lock(), view, detection, true, no_color())
35}
36
37pub fn print(text: &StyledText) -> io::Result<()> {
39 let stdout = io::stdout();
40 let detection = urushi_terminal::detect(&stdout)?;
41 write_text_detected(stdout.lock(), text, detection, false, no_color())
42}
43
44pub fn println(text: &StyledText) -> io::Result<()> {
46 let stdout = io::stdout();
47 let detection = urushi_terminal::detect(&stdout)?;
48 write_text_detected(stdout.lock(), text, detection, true, no_color())
49}
50
51pub fn eprint(text: &StyledText) -> io::Result<()> {
53 let stderr = io::stderr();
54 let detection = urushi_terminal::detect(&stderr)?;
55 write_text_detected(stderr.lock(), text, detection, false, no_color())
56}
57
58pub fn eprintln(text: &StyledText) -> io::Result<()> {
60 let stderr = io::stderr();
61 let detection = urushi_terminal::detect(&stderr)?;
62 write_text_detected(stderr.lock(), text, detection, true, no_color())
63}
64
65fn no_color() -> bool {
66 std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty())
67}
68
69fn write_view_detected(
70 writer: impl Write,
71 view: &View,
72 detection: TerminalDetection,
73 newline: bool,
74 no_color: bool,
75) -> io::Result<()> {
76 let (available, settings) = output_configuration(detection, no_color);
77 write_configured(writer, view, available, settings, newline)
78}
79
80fn write_text_detected(
81 mut writer: impl Write,
82 text: &StyledText,
83 detection: TerminalDetection,
84 newline: bool,
85 no_color: bool,
86) -> io::Result<()> {
87 let (_, settings) = output_configuration(detection, no_color);
88 writer.write_all(render_text(text, &settings).as_bytes())?;
89 if newline {
90 writer.write_all(b"\n")?;
91 }
92 Ok(())
93}
94
95fn write_configured(
96 mut writer: impl Write,
97 view: &View,
98 available: Available,
99 settings: RenderSettings,
100 newline: bool,
101) -> io::Result<()> {
102 let resolved = resolve(view, available).map_err(io::Error::other)?;
103 writer.write_all(render(&resolved, &settings).as_bytes())?;
104 if newline {
105 writer.write_all(b"\n")?;
106 }
107 Ok(())
108}
109
110fn output_configuration(
111 detection: TerminalDetection,
112 no_color: bool,
113) -> (Available, RenderSettings) {
114 match detection {
115 TerminalDetection::Terminal(info) => {
116 let settings = RenderSettings::from(info.capabilities());
117 let settings = apply_no_color(settings, no_color);
118 (Available::columns(info.size().columns()), settings)
119 }
120 TerminalDetection::NonTerminal => (Available::NONE, RenderSettings::default()),
121 }
122}
123
124fn apply_no_color(settings: RenderSettings, no_color: bool) -> RenderSettings {
125 if no_color {
126 settings.color_level(ColorLevel::None)
127 } else {
128 settings
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 use crate::{Color, TextAttribute, TextStyle};
137
138 #[test]
139 fn terminal_output_uses_its_width_and_capabilities() {
140 let mut output = Vec::new();
141 let view = View::text(
142 "abcdef",
143 TextStyle::new().foreground(Color::RED).bold().italic(),
144 );
145
146 write_configured(
147 &mut output,
148 &view,
149 Available::columns(3),
150 RenderSettings::default()
151 .color_level(ColorLevel::Ansi16)
152 .attributes(TextAttribute::Bold.into()),
153 false,
154 )
155 .unwrap();
156
157 assert_eq!(
158 String::from_utf8(output).unwrap(),
159 "\x1b[1;31mabc\x1b[0m\n\x1b[1;31mdef\x1b[0m"
160 );
161 }
162
163 #[test]
164 fn no_color_narrows_settings_without_disabling_other_features() {
165 let settings = RenderSettings::default()
166 .color_level(ColorLevel::Ansi16)
167 .attributes(TextAttribute::Bold.into());
168
169 let narrowed = apply_no_color(settings, true);
170
171 assert_eq!(narrowed.get_color_level(), ColorLevel::None);
172 assert_eq!(narrowed.get_attributes(), TextAttribute::Bold.into());
173 assert_eq!(apply_no_color(settings, false), settings);
174 }
175
176 #[test]
177 fn redirected_output_is_an_unbounded_plain_dump() {
178 let mut output = Vec::new();
179 let view = View::text("abcdef", TextStyle::new().foreground(Color::RED));
180
181 write_view_detected(
182 &mut output,
183 &view,
184 TerminalDetection::NonTerminal,
185 true,
186 false,
187 )
188 .unwrap();
189
190 assert_eq!(String::from_utf8(output).unwrap(), "abcdef\n");
191 }
192
193 #[test]
194 fn redirected_direct_text_preserves_tabs_without_layout() {
195 let mut output = Vec::new();
196 let text = StyledText::new("name\tvalue", TextStyle::new().bold());
197
198 write_text_detected(
199 &mut output,
200 &text,
201 TerminalDetection::NonTerminal,
202 true,
203 false,
204 )
205 .unwrap();
206
207 assert_eq!(String::from_utf8(output).unwrap(), "name\tvalue\n");
208 }
209}