use crate::ColorArg;
use anyhow::Result;
use holys3_core::{LineKind, SubMatch};
use holys3_index::{DocResult, MatchSink, SinkFlow};
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use termcolor::{BufferedStandardStream, ColorChoice, ColorSpec, WriteColor};
pub(crate) fn resolve_color(arg: ColorArg, is_tty: bool) -> ColorChoice {
match arg {
ColorArg::Never => ColorChoice::Never,
ColorArg::Always => ColorChoice::Always,
ColorArg::Ansi => ColorChoice::AlwaysAnsi,
ColorArg::Auto => {
if is_tty {
ColorChoice::Auto
} else {
ColorChoice::Never
}
}
}
}
fn flow_of(result: std::io::Result<()>) -> Result<SinkFlow> {
match result {
Ok(()) => Ok(SinkFlow::Continue),
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => Ok(SinkFlow::Stop),
Err(err) => Err(err.into()),
}
}
fn lock<T>(mutex: &Mutex<T>) -> Result<std::sync::MutexGuard<'_, T>> {
mutex
.lock()
.map_err(|_| anyhow::anyhow!("output writer poisoned"))
}
fn path_spec() -> ColorSpec {
let mut spec = ColorSpec::new();
spec.set_fg(Some(termcolor::Color::Magenta));
spec
}
fn line_spec() -> ColorSpec {
let mut spec = ColorSpec::new();
spec.set_fg(Some(termcolor::Color::Green));
spec
}
fn match_spec() -> ColorSpec {
let mut spec = ColorSpec::new();
spec.set_fg(Some(termcolor::Color::Red));
spec.set_bold(true);
spec
}
fn write_colored(
wtr: &mut BufferedStandardStream,
spec: &ColorSpec,
bytes: &[u8],
) -> std::io::Result<()> {
wtr.set_color(spec)?;
wtr.write_all(bytes)?;
wtr.reset()
}
fn content_of(text: &[u8]) -> &[u8] {
text.strip_suffix(b"\n").unwrap_or(text)
}
fn write_text_highlighted(
wtr: &mut BufferedStandardStream,
text: &[u8],
submatches: &[SubMatch],
) -> std::io::Result<()> {
let content = content_of(text);
let mut cursor = 0;
for sub in submatches {
let start = sub.start.min(content.len());
let end = sub.end.min(content.len());
wtr.write_all(&content[cursor..start])?;
write_colored(wtr, &match_spec(), &content[start..end])?;
cursor = end;
}
wtr.write_all(&content[cursor..])?;
wtr.write_all(b"\n")
}
pub(crate) struct RenderConfig {
pub(crate) heading: bool,
pub(crate) line_numbers: bool,
pub(crate) column: bool,
pub(crate) context_active: bool,
}
pub(crate) struct StandardSink {
state: Mutex<StandardState>,
config: RenderConfig,
}
struct StandardState {
wtr: BufferedStandardStream,
printed_any_doc: bool,
}
impl StandardSink {
pub(crate) fn new(config: RenderConfig, color: ColorChoice) -> StandardSink {
StandardSink {
state: Mutex::new(StandardState {
wtr: BufferedStandardStream::stdout(color),
printed_any_doc: false,
}),
config,
}
}
}
impl MatchSink for StandardSink {
fn on_doc(&self, key: &str, doc: &DocResult<'_>) -> Result<SinkFlow> {
let mut state = lock(&self.state)?;
let state = &mut *state;
let config = &self.config;
let write =
|wtr: &mut BufferedStandardStream, printed_any_doc: bool| -> std::io::Result<()> {
if config.heading {
if printed_any_doc {
wtr.write_all(b"\n")?;
}
write_colored(wtr, &path_spec(), key.as_bytes())?;
wtr.write_all(b"\n")?;
} else if config.context_active && printed_any_doc {
wtr.write_all(b"--\n")?;
}
let mut prev_line: Option<u64> = None;
for event in doc.events {
if config.context_active && prev_line.is_some_and(|p| event.line > p + 1) {
wtr.write_all(b"--\n")?;
}
prev_line = Some(event.line);
let sep: &[u8] = match event.kind {
LineKind::Match => b":",
LineKind::Context => b"-",
};
if !config.heading {
write_colored(wtr, &path_spec(), key.as_bytes())?;
wtr.write_all(sep)?;
}
if config.line_numbers {
write_colored(wtr, &line_spec(), event.line.to_string().as_bytes())?;
wtr.write_all(sep)?;
}
if config.column && event.kind == LineKind::Match {
let col = event.submatches.first().map_or(0, |s| s.start) + 1;
wtr.write_all(col.to_string().as_bytes())?;
wtr.write_all(sep)?;
}
write_text_highlighted(wtr, &event.text, &event.submatches)?;
}
wtr.flush()
};
let printed_any_doc = state.printed_any_doc;
state.printed_any_doc = true;
flow_of(write(&mut state.wtr, printed_any_doc))
}
}
pub(crate) struct PathSink {
state: Mutex<BufferedStandardStream>,
}
impl PathSink {
pub(crate) fn new(color: ColorChoice) -> PathSink {
PathSink {
state: Mutex::new(BufferedStandardStream::stdout(color)),
}
}
}
impl MatchSink for PathSink {
fn wants_matches(&self) -> bool {
false }
fn on_doc(&self, key: &str, _doc: &DocResult<'_>) -> Result<SinkFlow> {
let mut wtr = lock(&self.state)?;
flow_of(
write_colored(&mut wtr, &path_spec(), key.as_bytes())
.and_then(|()| wtr.write_all(b"\n"))
.and_then(|()| wtr.flush()),
)
}
}
pub(crate) struct CountSink {
state: Mutex<BufferedStandardStream>,
count_matches: bool,
}
impl CountSink {
pub(crate) fn new(count_matches: bool, color: ColorChoice) -> CountSink {
CountSink {
state: Mutex::new(BufferedStandardStream::stdout(color)),
count_matches,
}
}
}
impl MatchSink for CountSink {
fn on_doc(&self, key: &str, doc: &DocResult<'_>) -> Result<SinkFlow> {
let n = if self.count_matches {
doc.events.iter().map(|e| e.submatches.len()).sum::<usize>()
} else {
doc.events
.iter()
.filter(|e| e.kind == LineKind::Match)
.count()
};
let mut wtr = lock(&self.state)?;
flow_of(
write_colored(&mut wtr, &path_spec(), key.as_bytes())
.and_then(|()| writeln!(wtr, ":{n}"))
.and_then(|()| wtr.flush()),
)
}
}
pub(crate) struct QuietSink {
stop: bool,
matched: AtomicBool,
}
impl QuietSink {
pub(crate) fn new(stop: bool) -> QuietSink {
QuietSink {
stop,
matched: AtomicBool::new(false),
}
}
pub(crate) fn matched(&self) -> bool {
self.matched.load(Ordering::Relaxed)
}
}
impl MatchSink for QuietSink {
fn wants_matches(&self) -> bool {
false
}
fn on_doc(&self, _key: &str, _doc: &DocResult<'_>) -> Result<SinkFlow> {
self.matched.store(true, Ordering::Relaxed);
Ok(if self.stop {
SinkFlow::Stop
} else {
SinkFlow::Continue
})
}
}