Skip to content

Commit 6a3a990

Browse files
authored
Merge pull request #21809 from Homebrew/rust-prettyprint
brew-rs: add `ohai` function and TTY methods
2 parents 2499920 + 496258c commit 6a3a990

File tree

7 files changed

+300
-4
lines changed

7 files changed

+300
-4
lines changed

Library/Homebrew/rust/brew-rs/src/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use crate::BrewResult;
22
use crate::commands;
33
use crate::delegate;
4+
use crate::utils::formatter;
45
use std::env;
56
use std::process::ExitCode;
67

78
pub(crate) fn main() -> ExitCode {
89
match run() {
910
Ok(code) => code,
1011
Err(error) => {
11-
eprintln!("{error}");
12+
formatter::error(&error.to_string());
1213
ExitCode::FAILURE
1314
}
1415
}

Library/Homebrew/rust/brew-rs/src/delegate.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::BrewResult;
22
use crate::homebrew;
3+
use crate::utils::formatter;
34
use anyhow::Context;
45
use std::process::ExitCode;
56
use std::process::{Command, Stdio};
@@ -9,7 +10,9 @@ pub(crate) fn run(args: &[String]) -> BrewResult<ExitCode> {
910
}
1011

1112
pub(crate) fn run_with_warning(args: &[String], command_name: &str) -> BrewResult<ExitCode> {
12-
eprintln!("Warning: brew-rs is handing {command_name} back to the Ruby backend.");
13+
formatter::warning(
14+
format!("brew-rs is handing {command_name} back to the Ruby backend.").as_str(),
15+
);
1316
run_command(args)
1417
}
1518

Library/Homebrew/rust/brew-rs/src/homebrew.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::BrewResult;
2+
use crate::utils::formatter::ohai;
23
use anyhow::{Context, anyhow};
34
use std::env;
45
use std::fs;
@@ -30,6 +31,14 @@ pub(crate) fn brew_file() -> BrewResult<PathBuf> {
3031
env_path("HOMEBREW_BREW_FILE")
3132
}
3233

34+
pub(crate) fn brew_no_color() -> bool {
35+
env_bool("HOMEBREW_NO_COLOR")
36+
}
37+
38+
pub(crate) fn brew_color() -> bool {
39+
env_bool("HOMEBREW_COLOR")
40+
}
41+
3342
pub(crate) fn read_lines(path: &Path) -> BrewResult<Vec<String>> {
3443
let contents =
3544
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
@@ -68,10 +77,10 @@ pub(crate) fn print_sections(formulae: &[String], casks: &[String]) {
6877
let stdout_is_tty = io::stdout().is_terminal();
6978

7079
if stdout_is_tty && !formulae.is_empty() && !casks.is_empty() {
71-
println!("Formulae");
80+
ohai("Formulae");
7281
println!("{}", formulae.join("\n"));
7382
println!();
74-
println!("Casks");
83+
ohai("Casks");
7584
println!("{}", casks.join("\n"));
7685
return;
7786
}
@@ -93,6 +102,25 @@ fn env_path(name: &str) -> BrewResult<PathBuf> {
93102
.ok_or_else(|| anyhow!("{name} is not set"))
94103
}
95104

105+
fn env_bool(name: &str) -> bool {
106+
env::var_os(name)
107+
.map(|string| {
108+
if string.is_empty() {
109+
return false;
110+
}
111+
112+
if let Some(string) = string.to_str() {
113+
return !matches!(
114+
string.trim().to_lowercase().as_str(),
115+
"0" | "false" | "off" | "no" | "nil"
116+
);
117+
}
118+
119+
false
120+
})
121+
.unwrap_or(false)
122+
}
123+
96124
fn list_directories(path: &Path) -> BrewResult<Vec<String>> {
97125
if !path.exists() {
98126
return Ok(Vec::new());

Library/Homebrew/rust/brew-rs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod commands;
55
mod delegate;
66
mod homebrew;
77
mod matcher;
8+
mod utils;
89

910
use anyhow::Result;
1011
use std::process::ExitCode;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use crate::utils::tty::{self, AnsiBuilder};
2+
use std::io::{self, IsTerminal};
3+
4+
pub fn ohai(string: &str) {
5+
let title = if io::stdout().is_terminal() {
6+
tty::truncate(string)
7+
} else {
8+
string
9+
};
10+
println!("{}", headline(title, &tty::blue()));
11+
}
12+
13+
pub fn warning(string: &str) {
14+
eprintln!(
15+
"{warning}Warning{reset}: {string}",
16+
warning = AnsiBuilder::new().yellow().underline(),
17+
reset = tty::reset(),
18+
);
19+
}
20+
21+
pub fn error(string: &str) {
22+
eprintln!(
23+
"{error}Error{reset}: {string}",
24+
error = tty::red(),
25+
reset = tty::reset(),
26+
);
27+
}
28+
29+
fn arrow(string: &str, escape_sequence: &AnsiBuilder) -> String {
30+
prefix("==>", string, escape_sequence)
31+
}
32+
33+
fn headline(string: &str, escape_sequence: &AnsiBuilder) -> String {
34+
arrow(
35+
format!(
36+
"{bold}{string}{reset}",
37+
bold = tty::bold(),
38+
reset = tty::reset(),
39+
)
40+
.as_str(),
41+
escape_sequence,
42+
)
43+
}
44+
45+
fn prefix(prefix: &str, string: &str, escape_sequence: &AnsiBuilder) -> String {
46+
if prefix.trim().is_empty() {
47+
return format!("{escape_sequence}{string}{reset}", reset = tty::reset());
48+
}
49+
50+
format!(
51+
"{escape_sequence}{prefix}{reset} {string}",
52+
reset = tty::reset()
53+
)
54+
}
55+
56+
#[cfg(test)]
57+
mod test {
58+
use super::*;
59+
60+
#[test]
61+
fn test_prefix() {
62+
let empty_prefix = " ";
63+
let non_empty_prefix = "==>";
64+
let escape_sequence = AnsiBuilder::new().yellow().bold();
65+
assert_eq!(
66+
format!(
67+
"{custom_color}test{reset}",
68+
custom_color = &escape_sequence,
69+
reset = tty::reset()
70+
),
71+
prefix(empty_prefix, "test", &escape_sequence)
72+
);
73+
assert_eq!(
74+
format!(
75+
"{custom_color}{non_empty_prefix}{reset} test",
76+
custom_color = &escape_sequence,
77+
reset = tty::reset()
78+
),
79+
prefix(non_empty_prefix, "test", &escape_sequence)
80+
);
81+
}
82+
83+
#[test]
84+
fn test_headline() {
85+
let test = "1234foobar";
86+
let escape_sequence = AnsiBuilder::new().underline().red();
87+
88+
assert_eq!(
89+
format!(
90+
"{color}==>{reset} {bold}{test}{reset}",
91+
color = &escape_sequence,
92+
bold = tty::bold(),
93+
reset = tty::reset(),
94+
),
95+
headline(test, &escape_sequence)
96+
);
97+
}
98+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod formatter;
2+
pub mod tty;
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use crate::homebrew;
2+
use std::fmt;
3+
use std::io::{self, IsTerminal};
4+
use std::process::Command;
5+
use std::str;
6+
use std::sync::OnceLock;
7+
8+
// Color codes
9+
const RED: &str = "31";
10+
const YELLOW: &str = "33";
11+
const BLUE: &str = "34";
12+
13+
// Style codes
14+
const RESET: &str = "0";
15+
const BOLD: &str = "1";
16+
const UNDERLINE: &str = "4";
17+
18+
static TTY_WIDTH: OnceLock<usize> = OnceLock::new();
19+
20+
pub struct AnsiBuilder {
21+
escape_sequences: Vec<&'static str>,
22+
}
23+
24+
fn colorful_output() -> bool {
25+
if homebrew::brew_no_color() {
26+
return false;
27+
}
28+
if homebrew::brew_color() {
29+
return true;
30+
}
31+
io::stdout().is_terminal()
32+
}
33+
34+
impl fmt::Display for AnsiBuilder {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
if colorful_output() {
37+
write!(f, "\x1B[{}m", self.escape_sequences.join(";"))
38+
} else {
39+
write!(f, "")
40+
}
41+
}
42+
}
43+
44+
impl AnsiBuilder {
45+
pub fn new() -> Self {
46+
AnsiBuilder {
47+
escape_sequences: Vec::new(),
48+
}
49+
}
50+
}
51+
52+
macro_rules! ansi_methods_and_functions {
53+
($( $name:ident => $value:expr ),* $(,)?) => {
54+
impl AnsiBuilder {
55+
$(
56+
#[inline]
57+
pub fn $name(mut self) -> Self {
58+
self.escape_sequences.push($value);
59+
self
60+
}
61+
)*
62+
}
63+
64+
$(
65+
#[inline]
66+
#[allow(unused)]
67+
pub fn $name() -> AnsiBuilder {
68+
AnsiBuilder::new().$name()
69+
}
70+
)*
71+
};
72+
}
73+
74+
ansi_methods_and_functions! {
75+
red => RED,
76+
yellow => YELLOW,
77+
blue => BLUE,
78+
reset => RESET,
79+
bold => BOLD,
80+
underline => UNDERLINE,
81+
}
82+
83+
pub fn width() -> usize {
84+
*TTY_WIDTH.get_or_init(|| {
85+
if let Ok(command) = Command::new("/bin/stty").arg("size").output() {
86+
let mut output = command.stdout.split(|ch| ch.is_ascii_whitespace());
87+
88+
let _ = output.next();
89+
if let Some(width) = output
90+
.next()
91+
.and_then(|string| str::from_utf8(string).ok())
92+
.and_then(|string| string.parse().ok())
93+
{
94+
return width;
95+
}
96+
}
97+
98+
if let Ok(output) = Command::new("/usr/bin/tput").arg("cols").output()
99+
&& let Some(res) = str::from_utf8(&output.stdout)
100+
.ok()
101+
.and_then(|string| string.parse().ok())
102+
{
103+
return res;
104+
}
105+
106+
80
107+
})
108+
}
109+
110+
pub fn truncate(string: &str) -> &str {
111+
let w = width();
112+
113+
if w < 4 {
114+
return string;
115+
}
116+
117+
let mut end = usize::min(string.len(), w - 4);
118+
119+
while end > 0 && !string.is_char_boundary(end) {
120+
end -= 1;
121+
}
122+
123+
&string[..end]
124+
}
125+
126+
#[cfg(test)]
127+
mod test {
128+
use super::*;
129+
130+
#[test]
131+
fn test_ansi_builder() {
132+
let expected = if colorful_output() {
133+
"\x1B[31;1;4m"
134+
} else {
135+
""
136+
};
137+
assert_eq!(
138+
expected,
139+
AnsiBuilder::new()
140+
.red()
141+
.bold()
142+
.underline()
143+
.to_string()
144+
.as_str()
145+
);
146+
}
147+
148+
#[test]
149+
fn test_truncate() {
150+
let w = width();
151+
152+
if w < 4 {
153+
let string = "some string that is longer than 4 characters for sure";
154+
assert_eq!(string.len(), truncate(string).len());
155+
return;
156+
}
157+
158+
let not_trimmed = "a".repeat(w - 4);
159+
assert_eq!(not_trimmed.len(), truncate(&not_trimmed).len());
160+
let trimmed = "a".repeat(w);
161+
assert_eq!(trimmed.len() - 4, truncate(&trimmed).len());
162+
}
163+
}

0 commit comments

Comments
 (0)