ui.rs
307 lines
| 9.1 KiB
| application/rls-services+xml
|
RustLexer
|
r49584 | use crate::color::ColorConfig; | ||
use crate::color::Effect; | ||||
|
r50876 | use crate::error::CommandError; | ||
|
r46598 | use format_bytes::format_bytes; | ||
|
r49584 | use format_bytes::write_bytes; | ||
|
r49581 | use hg::config::Config; | ||
|
r50408 | use hg::config::PlainInfo; | ||
|
r49582 | use hg::errors::HgError; | ||
|
r50876 | use hg::repo::Repo; | ||
use hg::sparse; | ||||
use hg::utils::files::get_bytes_from_path; | ||||
use hg::PatternFileWarning; | ||||
|
r46099 | use std::borrow::Cow; | ||
|
r45592 | use std::io; | ||
|
r45921 | use std::io::{ErrorKind, Write}; | ||
|
r45592 | |||
|
r45921 | pub struct Ui { | ||
stdout: std::io::Stdout, | ||||
stderr: std::io::Stderr, | ||||
|
r49584 | colors: Option<ColorConfig>, | ||
|
r45921 | } | ||
|
r45592 | |||
/// The kind of user interface error | ||||
pub enum UiError { | ||||
/// The standard output stream cannot be written to | ||||
StdoutError(io::Error), | ||||
/// The standard error stream cannot be written to | ||||
StderrError(io::Error), | ||||
} | ||||
/// The commandline user interface | ||||
impl Ui { | ||||
|
r49584 | pub fn new(config: &Config) -> Result<Self, HgError> { | ||
|
r49582 | Ok(Ui { | ||
|
r49584 | // If using something else, also adapt `isatty()` below. | ||
|
r49582 | stdout: std::io::stdout(), | ||
|
r49584 | |||
|
r49582 | stderr: std::io::stderr(), | ||
|
r49584 | colors: ColorConfig::new(config)?, | ||
|
r49582 | }) | ||
} | ||||
/// Default to no color if color configuration errors. | ||||
/// | ||||
/// Useful when we’re already handling another error. | ||||
|
r49584 | pub fn new_infallible(config: &Config) -> Self { | ||
|
r45921 | Ui { | ||
|
r49584 | // If using something else, also adapt `isatty()` below. | ||
|
r45921 | stdout: std::io::stdout(), | ||
|
r49584 | |||
|
r45921 | stderr: std::io::stderr(), | ||
|
r49584 | colors: ColorConfig::new(config).unwrap_or(None), | ||
|
r45921 | } | ||
} | ||||
/// Returns a buffered handle on stdout for faster batch printing | ||||
/// operations. | ||||
pub fn stdout_buffer(&self) -> StdoutBuffer<std::io::StdoutLock> { | ||||
StdoutBuffer::new(self.stdout.lock()) | ||||
|
r45592 | } | ||
/// Write bytes to stdout | ||||
pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> { | ||||
|
r45921 | let mut stdout = self.stdout.lock(); | ||
|
r45592 | |||
|
r46010 | stdout.write_all(bytes).or_else(handle_stdout_error)?; | ||
|
r45592 | |||
|
r46010 | stdout.flush().or_else(handle_stdout_error) | ||
|
r45592 | } | ||
/// Write bytes to stderr | ||||
pub fn write_stderr(&self, bytes: &[u8]) -> Result<(), UiError> { | ||||
|
r45921 | let mut stderr = self.stderr.lock(); | ||
|
r45592 | |||
|
r46010 | stderr.write_all(bytes).or_else(handle_stderr_error)?; | ||
|
r45592 | |||
|
r46010 | stderr.flush().or_else(handle_stderr_error) | ||
|
r45592 | } | ||
|
r48990 | |||
|
r49584 | /// Write bytes to stdout with the given label | ||
/// | ||||
/// Like the optional `label` parameter in `mercurial/ui.py`, | ||||
/// this label influences the color used for this output. | ||||
pub fn write_stdout_labelled( | ||||
&self, | ||||
bytes: &[u8], | ||||
label: &str, | ||||
) -> Result<(), UiError> { | ||||
if let Some(colors) = &self.colors { | ||||
if let Some(effects) = colors.styles.get(label.as_bytes()) { | ||||
if !effects.is_empty() { | ||||
return self | ||||
.write_stdout_with_effects(bytes, effects) | ||||
.or_else(handle_stdout_error); | ||||
} | ||||
} | ||||
} | ||||
self.write_stdout(bytes) | ||||
} | ||||
fn write_stdout_with_effects( | ||||
&self, | ||||
bytes: &[u8], | ||||
effects: &[Effect], | ||||
) -> io::Result<()> { | ||||
let stdout = &mut self.stdout.lock(); | ||||
let mut write_line = |line: &[u8], first: bool| { | ||||
// `line` does not include the newline delimiter | ||||
if !first { | ||||
stdout.write_all(b"\n")?; | ||||
} | ||||
if line.is_empty() { | ||||
return Ok(()); | ||||
} | ||||
/// 0x1B == 27 == 0o33 | ||||
const ASCII_ESCAPE: &[u8] = b"\x1b"; | ||||
write_bytes!(stdout, b"{}[0", ASCII_ESCAPE)?; | ||||
for effect in effects { | ||||
write_bytes!(stdout, b";{}", effect)?; | ||||
} | ||||
write_bytes!(stdout, b"m")?; | ||||
stdout.write_all(line)?; | ||||
write_bytes!(stdout, b"{}[0m", ASCII_ESCAPE) | ||||
}; | ||||
let mut lines = bytes.split(|&byte| byte == b'\n'); | ||||
if let Some(first) = lines.next() { | ||||
write_line(first, true)?; | ||||
for line in lines { | ||||
write_line(line, false)? | ||||
} | ||||
} | ||||
stdout.flush() | ||||
} | ||||
|
r49580 | } | ||
|
r50408 | // TODO: pass the PlainInfo to call sites directly and | ||
// delete this function | ||||
|
r49584 | pub fn plain(opt_feature: Option<&str>) -> bool { | ||
|
r50408 | let plain_info = PlainInfo::from_env(); | ||
match opt_feature { | ||||
None => plain_info.is_plain(), | ||||
Some(feature) => plain_info.is_feature_plain(feature), | ||||
|
r48990 | } | ||
|
r45592 | } | ||
|
r45921 | |||
/// A buffered stdout writer for faster batch printing operations. | ||||
pub struct StdoutBuffer<W: Write> { | ||||
buf: io::BufWriter<W>, | ||||
} | ||||
impl<W: Write> StdoutBuffer<W> { | ||||
pub fn new(writer: W) -> Self { | ||||
let buf = io::BufWriter::new(writer); | ||||
Self { buf } | ||||
} | ||||
/// Write bytes to stdout buffer | ||||
pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> { | ||||
|
r46010 | self.buf.write_all(bytes).or_else(handle_stdout_error) | ||
|
r45921 | } | ||
/// Flush bytes to stdout | ||||
pub fn flush(&mut self) -> Result<(), UiError> { | ||||
|
r46010 | self.buf.flush().or_else(handle_stdout_error) | ||
|
r45921 | } | ||
} | ||||
|
r45925 | |||
/// Sometimes writing to stdout is not possible, try writing to stderr to | ||||
/// signal that failure, otherwise just bail. | ||||
fn handle_stdout_error(error: io::Error) -> Result<(), UiError> { | ||||
if let ErrorKind::BrokenPipe = error.kind() { | ||||
// This makes `| head` work for example | ||||
return Ok(()); | ||||
} | ||||
let mut stderr = io::stderr(); | ||||
stderr | ||||
|
r46598 | .write_all(&format_bytes!( | ||
b"abort: {}\n", | ||||
error.to_string().as_bytes() | ||||
)) | ||||
|
r46010 | .map_err(UiError::StderrError)?; | ||
|
r45925 | |||
|
r46010 | stderr.flush().map_err(UiError::StderrError)?; | ||
|
r45925 | |||
Err(UiError::StdoutError(error)) | ||||
} | ||||
|
r45926 | |||
/// Sometimes writing to stderr is not possible. | ||||
fn handle_stderr_error(error: io::Error) -> Result<(), UiError> { | ||||
// A broken pipe should not result in a error | ||||
// like with `| head` for example | ||||
if let ErrorKind::BrokenPipe = error.kind() { | ||||
return Ok(()); | ||||
} | ||||
Err(UiError::StdoutError(error)) | ||||
} | ||||
|
r46099 | |||
/// Encode rust strings according to the user system. | ||||
pub fn utf8_to_local(s: &str) -> Cow<[u8]> { | ||||
// TODO encode for the user's system // | ||||
let bytes = s.as_bytes(); | ||||
Cow::Borrowed(bytes) | ||||
} | ||||
|
r49584 | |||
|
r49622 | /// Decode user system bytes to Rust string. | ||
pub fn local_to_utf8(s: &[u8]) -> Cow<str> { | ||||
// TODO decode from the user's system | ||||
String::from_utf8_lossy(s) | ||||
} | ||||
|
r49584 | /// Should formatted output be used? | ||
/// | ||||
/// Note: rhg does not have the formatter mechanism yet, | ||||
/// but this is also used when deciding whether to use color. | ||||
pub fn formatted(config: &Config) -> Result<bool, HgError> { | ||||
if let Some(formatted) = config.get_option(b"ui", b"formatted")? { | ||||
Ok(formatted) | ||||
} else { | ||||
isatty(config) | ||||
} | ||||
} | ||||
|
r51432 | pub enum RelativePaths { | ||
Legacy, | ||||
Bool(bool), | ||||
} | ||||
pub fn relative_paths(config: &Config) -> Result<RelativePaths, HgError> { | ||||
Ok(match config.get(b"ui", b"relative-paths") { | ||||
None | Some(b"legacy") => RelativePaths::Legacy, | ||||
_ => RelativePaths::Bool(config.get_bool(b"ui", b"relative-paths")?), | ||||
}) | ||||
} | ||||
|
r49584 | fn isatty(config: &Config) -> Result<bool, HgError> { | ||
Ok(if config.get_bool(b"ui", b"nontty")? { | ||||
false | ||||
} else { | ||||
atty::is(atty::Stream::Stdout) | ||||
}) | ||||
} | ||||
|
r50876 | |||
/// Return the formatted bytestring corresponding to a pattern file warning, | ||||
/// as expected by the CLI. | ||||
pub(crate) fn format_pattern_file_warning( | ||||
warning: &PatternFileWarning, | ||||
repo: &Repo, | ||||
) -> Vec<u8> { | ||||
match warning { | ||||
PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!( | ||||
b"{}: ignoring invalid syntax '{}'\n", | ||||
get_bytes_from_path(path), | ||||
&*syntax | ||||
), | ||||
PatternFileWarning::NoSuchFile(path) => { | ||||
let path = if let Ok(relative) = | ||||
path.strip_prefix(repo.working_directory_path()) | ||||
{ | ||||
relative | ||||
} else { | ||||
&*path | ||||
}; | ||||
format_bytes!( | ||||
b"skipping unreadable pattern file '{}': \ | ||||
No such file or directory\n", | ||||
get_bytes_from_path(path), | ||||
) | ||||
} | ||||
} | ||||
} | ||||
/// Print with `Ui` the formatted bytestring corresponding to a | ||||
/// sparse/narrow warning, as expected by the CLI. | ||||
pub(crate) fn print_narrow_sparse_warnings( | ||||
narrow_warnings: &[sparse::SparseWarning], | ||||
sparse_warnings: &[sparse::SparseWarning], | ||||
ui: &Ui, | ||||
repo: &Repo, | ||||
) -> Result<(), CommandError> { | ||||
for warning in narrow_warnings.iter().chain(sparse_warnings) { | ||||
match &warning { | ||||
sparse::SparseWarning::RootWarning { context, line } => { | ||||
let msg = format_bytes!( | ||||
b"warning: {} profile cannot use paths \" | ||||
starting with /, ignoring {}\n", | ||||
context, | ||||
line | ||||
); | ||||
ui.write_stderr(&msg)?; | ||||
} | ||||
sparse::SparseWarning::ProfileNotFound { profile, rev } => { | ||||
let msg = format_bytes!( | ||||
b"warning: sparse profile '{}' not found \" | ||||
in rev {} - ignoring it\n", | ||||
profile, | ||||
rev | ||||
); | ||||
ui.write_stderr(&msg)?; | ||||
} | ||||
sparse::SparseWarning::Pattern(e) => { | ||||
ui.write_stderr(&format_pattern_file_warning(e, repo))?; | ||||
} | ||||
} | ||||
} | ||||
Ok(()) | ||||
} | ||||