Files
sausage/userland/fd/src/output.rs
T
surma b9e3e3677d userland: search and exec tools, spawn library (T14)
rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs
(ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r),
fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like
client of the process interface. search-test.nu: 42 cases match native tools.
2026-11-06 17:21:51 +00:00

197 lines
5.6 KiB
Rust

use std::borrow::Cow;
use std::io::{self, Write};
use lscolors::{Indicator, LsColors, Style};
use crate::config::Config;
use crate::dir_entry::DirEntry;
use crate::fmt::FormatTemplate;
use crate::hyperlink::PathUrl;
use crate::sanitize::sanitize_for_term;
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
}
// TODO: this function is performance critical and can probably be optimized
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) -> io::Result<()> {
let mut has_hyperlink = false;
if config.hyperlink
&& let Some(url) = PathUrl::new(entry.path())
{
write!(stdout, "\x1B]8;;{url}\x1B\\")?;
has_hyperlink = true;
}
if let Some(ref format) = config.format {
print_entry_format(stdout, entry, config, format)?;
} else if let Some(ref ls_colors) = config.ls_colors {
print_entry_colorized(stdout, entry, config, ls_colors)?;
} else {
print_entry_uncolorized(stdout, entry, config)?;
};
if has_hyperlink {
write!(stdout, "\x1B]8;;\x1B\\")?;
}
if config.null_separator {
write!(stdout, "\0")
} else {
writeln!(stdout)
}
}
// Display a trailing slash if the path is a directory and the config option is enabled.
// If the path_separator option is set, display that instead.
// The trailing slash will not be colored.
#[inline]
fn print_trailing_slash<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
style: Option<&Style>,
) -> io::Result<()> {
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
write!(
stdout,
"{}",
style
.map(Style::to_nu_ansi_term_style)
.unwrap_or_default()
.paint(&config.actual_path_separator)
)?;
}
Ok(())
}
// TODO: this function is performance critical and can probably be optimized
fn print_entry_format<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
format: &FormatTemplate,
) -> io::Result<()> {
let output = format.generate(
entry.stripped_path(config),
config.path_separator.as_deref(),
);
// TODO: support writing raw bytes on unix?
let s = output.to_string_lossy();
write!(
stdout,
"{}",
sanitize_for_term(&s, config.interactive_terminal)
)
}
// TODO: this function is performance critical and can probably be optimized
fn print_entry_colorized<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
ls_colors: &LsColors,
) -> io::Result<()> {
let mut offset = 0;
let path = entry.stripped_path(config);
let path_str = path.to_string_lossy();
if let Some(parent) = path.parent() {
offset = parent.to_string_lossy().len();
for c in path_str[offset..].chars() {
if std::path::is_separator(c) {
offset += c.len_utf8();
} else {
break;
}
}
}
if offset > 0 {
let mut parent_str = Cow::from(&path_str[..offset]);
if let Some(ref separator) = config.path_separator {
*parent_str.to_mut() = replace_path_separator(&parent_str, separator);
}
let style = ls_colors
.style_for_indicator(Indicator::Directory)
.map(Style::to_nu_ansi_term_style)
.unwrap_or_default();
let safe_parent = sanitize_for_term(&parent_str, config.interactive_terminal);
// We explicitly use prefix/suffix instead of paint so that we can use Sanitized
write!(
stdout,
"{}{}{}",
style.prefix(),
safe_parent,
style.suffix()
)?;
}
let style = entry
.style(ls_colors)
.map(Style::to_nu_ansi_term_style)
.unwrap_or_default();
let safe_basename = sanitize_for_term(&path_str[offset..], config.interactive_terminal);
// We explicitly use prefix/suffix instead of paint so that we can use Sanitized
write!(
stdout,
"{}{}{}",
style.prefix(),
safe_basename,
style.suffix()
)?;
print_trailing_slash(
stdout,
entry,
config,
ls_colors.style_for_indicator(Indicator::Directory),
)?;
Ok(())
}
// TODO: this function is performance critical and can probably be optimized
fn print_entry_uncolorized_base<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
) -> io::Result<()> {
let path = entry.stripped_path(config);
let mut path_string = path.to_string_lossy();
if let Some(ref separator) = config.path_separator {
*path_string.to_mut() = replace_path_separator(&path_string, separator);
}
let safe = sanitize_for_term(&path_string, config.interactive_terminal);
write!(stdout, "{safe}")?;
print_trailing_slash(stdout, entry, config, None)
}
#[cfg(not(unix))]
fn print_entry_uncolorized<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
) -> io::Result<()> {
print_entry_uncolorized_base(stdout, entry, config)
}
#[cfg(unix)]
fn print_entry_uncolorized<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
) -> io::Result<()> {
use std::os::unix::ffi::OsStrExt;
if config.interactive_terminal || config.path_separator.is_some() {
print_entry_uncolorized_base(stdout, entry, config)
} else {
// Piped output: raw bytes so invalid UTF-8 filenames reach downstream tools intact.
stdout.write_all(entry.stripped_path(config).as_os_str().as_bytes())?;
print_trailing_slash(stdout, entry, config, None)
}
}