use crate::{
cli::{Args, Commands, buffer::handle_clipboard_mode},
config::Config,
error::Result,
printer,
walker::WalkerItem,
};
use crossbeam_channel::{Receiver, bounded};
use std::io::{self, Write};
use std::thread;
const WRITER_CHANNEL_CAPACITY: usize = 16;
enum PrintMessage {
Block(String),
TreeEntry(crate::walker::WalkerEntry),
}
pub fn run(args: &Args) -> Result<()> {
if let Some(Commands::Schema { output }) = args.command() {
return crate::cli::schema::generate_schema(output.as_ref());
}
let config = Config::from_args(args)?;
let output_mode = args.output_mode();
log::debug!("Starting luff v{}", crate::VERSION);
log::debug!("Output mode: {output_mode:?}");
if args.suppress_stdout() && !args.use_clipboard() {
log::warn!(
"--suppress-stdout (-S) has no effect without --clip (-c); \
output will still be written to stdout"
);
}
if let Some(files) = args.files() {
let estimated = files.len();
let walker = crate::walker::Walker::from_file_list(&files, &config)?;
dispatch_output(walker, estimated, &config, output_mode)?;
} else {
log::debug!("Starting directory walk from: {}", config.root().display());
let walker = crate::walker::Walker::from_dir(&config)?;
let estimated_files = walker.size_hint().1.unwrap_or(0);
dispatch_output(walker, estimated_files, &config, output_mode)?;
}
Ok(())
}
fn dispatch_output<I>(
walker: I,
estimated_files: usize,
config: &Config,
output_mode: crate::cli::OutputMode,
) -> Result<()>
where
I: IntoIterator<Item = WalkerItem>,
{
let printer_opts = config.printer_options();
if output_mode.should_buffer() {
handle_clipboard_mode(walker, estimated_files, &printer_opts, config, output_mode)
} else {
debug_assert!(
output_mode.should_show_stdout(),
"Non-buffered mode must show stdout; \
OutputMode::Stdout always returns true from should_show_stdout()"
);
stream_to_stdout(walker, &printer_opts, config.max_files())
}
}
fn write_output(
rx: Receiver<PrintMessage>,
root: &std::path::Path,
max_files: usize,
) -> Result<()> {
let stdout = io::stdout();
let mut handle = io::BufWriter::new(stdout.lock());
let mut tree_printer: Option<printer::TreePrinter> = None;
for msg in rx {
match msg {
PrintMessage::Block(s) => {
handle.write_all(s.as_bytes())?;
}
PrintMessage::TreeEntry(entry) => {
tree_printer
.get_or_insert_with(|| printer::TreePrinter::with_max_entries(max_files))
.add_entry(entry.path)?;
}
}
}
if let Some(tp) = tree_printer {
tp.write_tree(&mut handle, root)?;
}
handle.flush()?;
Ok(())
}
fn spawn_writer_thread(
rx: Receiver<PrintMessage>,
root: std::path::PathBuf,
max_files: usize,
) -> thread::JoinHandle<Result<()>> {
thread::spawn(move || match write_output(rx, &root, max_files) {
Ok(()) => Ok(()),
Err(crate::error::Error::Io(ref e)) if e.kind() == io::ErrorKind::BrokenPipe => {
log::debug!("Writer thread: stdout broken pipe (reader closed)");
Ok(())
}
Err(e) => Err(e),
})
}
fn join_writer(handle: thread::JoinHandle<Result<()>>) -> Result<()> {
match handle.join() {
Ok(result) => result,
Err(panic_payload) => std::panic::resume_unwind(panic_payload),
}
}
fn stream_to_stdout<I>(
walker: I,
printer_opts: &printer::PrinterOptions,
max_files: usize,
) -> Result<()>
where
I: IntoIterator<Item = WalkerItem>,
{
let (tx, rx) = bounded::<PrintMessage>(WRITER_CHANNEL_CAPACITY);
let writer_handle = spawn_writer_thread(rx, printer_opts.root.clone(), max_files);
for item in walker {
match item {
WalkerItem::Entry(entry) => match printer_opts.format {
crate::cli::OutputFormat::Markdown => {
if let Some(formatted) = printer::MarkdownPrinter::format_entry(
&entry,
&printer_opts.patterns,
printer_opts.skip_patterns,
)? {
if tx.send(PrintMessage::Block(formatted)).is_err() {
break; }
}
}
crate::cli::OutputFormat::Tree => {
if tx.send(PrintMessage::TreeEntry(entry)).is_err() {
break; }
}
},
WalkerItem::Error(e) => {
eprintln!("⚠ Walker error: {e}");
}
}
}
drop(tx);
join_writer(writer_handle)
}