use crate::{
cli::{OutputFormat, clipboard, estimation},
config::Config,
error::Result,
printer,
walker::WalkerItem,
};
use log::debug;
#[derive(Debug)]
#[must_use]
pub struct BufferResult {
pub output: String,
pub processed: usize,
pub total_seen: usize,
pub size_exceeded: bool,
}
#[derive(Debug, Clone, Copy)]
struct FenceGuard {
open: bool,
}
impl FenceGuard {
const fn new() -> Self {
Self { open: false }
}
const fn mark_open(&mut self) {
self.open = true;
}
const fn mark_closed(&mut self) {
self.open = false;
}
fn close_if_needed(self, output: &mut String) {
if self.open {
output.push_str("\n```\n\n");
}
}
}
fn buffer_entries_with_limit<I>(
entries: I,
printer_opts: &crate::printer::PrinterOptions,
max_bytes: usize,
estimated_files: usize,
) -> Result<BufferResult>
where
I: IntoIterator<Item = WalkerItem>,
{
let estimated_size = estimation::estimate_output_size(estimated_files);
let initial_capacity = if max_bytes > 0 {
estimated_size.min(max_bytes)
} else {
estimated_size
};
debug!(
"Pre-allocating {initial_capacity} bytes (estimate from {estimated_files} files, effective max: {max_bytes})"
);
let mut output = String::with_capacity(initial_capacity);
let mut processed = 0usize;
let mut total_seen = 0usize;
let mut size_exceeded = false;
let mut fence = FenceGuard::new();
for item in entries {
let entry = match item {
WalkerItem::Entry(e) => e,
WalkerItem::Error(e) => {
debug!("Skipping walker error in clipboard mode: {e}");
continue;
}
};
total_seen += 1;
if max_bytes > 0 && output.len() >= max_bytes {
debug!(
"Clipboard size limit reached before formatting {}: {} >= {} bytes",
entry.relative_path.display(),
output.len(),
max_bytes
);
size_exceeded = true;
break;
}
let size_before = output.len();
let fence_at_safe_point = fence;
fence.mark_open();
let formatted = printer::MarkdownPrinter::format_entry_into(
&entry,
&mut output,
&printer_opts.patterns,
printer_opts.skip_patterns,
)?;
if formatted {
fence.mark_closed();
}
if !formatted {
output.truncate(size_before);
fence = fence_at_safe_point;
continue;
}
let size_after = output.len();
let bytes_added = size_after.saturating_sub(size_before);
debug!(
"Formatted {}: {bytes_added} bytes (total now: {size_after})",
entry.relative_path.display(),
);
if max_bytes > 0 && size_after > max_bytes {
debug!(
"Size limit exceeded after formatting {}: {size_after} > {max_bytes} bytes",
entry.relative_path.display(),
);
output.truncate(size_before);
fence = fence_at_safe_point;
fence.close_if_needed(&mut output);
size_exceeded = true;
break;
}
processed += 1;
}
debug!(
"Buffering complete: {} bytes, {processed}/{total_seen} files processed, size_exceeded: {size_exceeded}",
output.len()
);
Ok(BufferResult {
output,
processed,
total_seen,
size_exceeded,
})
}
fn buffer_tree_entries<I>(
entries: I,
printer_opts: &crate::printer::PrinterOptions,
max_bytes: usize,
max_files: usize,
) -> Result<BufferResult>
where
I: IntoIterator<Item = WalkerItem>,
{
let mut tree_printer = printer::TreePrinter::with_max_entries(max_files);
let mut processed = 0usize;
let mut total_seen = 0usize;
for item in entries {
let entry = match item {
WalkerItem::Entry(e) => e,
WalkerItem::Error(e) => {
debug!("Skipping walker error in clipboard mode: {e}");
continue;
}
};
total_seen += 1;
tree_printer.add_entry(entry.path)?;
processed += 1;
}
let mut buf = Vec::new();
tree_printer.write_tree(&mut buf, &printer_opts.root)?;
let output = String::from_utf8(buf).map_err(|e| {
crate::error::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Tree output contained invalid UTF-8: {e}"),
))
})?;
let size_exceeded = max_bytes > 0 && output.len() > max_bytes;
if size_exceeded {
debug!(
"Tree output ({} bytes) exceeds clipboard limit ({max_bytes} bytes)",
output.len()
);
Ok(BufferResult {
output: String::new(),
processed: 0,
total_seen,
size_exceeded: true,
})
} else {
debug!(
"Tree buffering complete: {} bytes, {processed} files",
output.len()
);
Ok(BufferResult {
output,
processed,
total_seen,
size_exceeded: false,
})
}
}
pub fn handle_clipboard_mode<I>(
walker: I,
estimated_files: usize,
printer_opts: &crate::printer::PrinterOptions,
config: &Config,
output_mode: crate::cli::OutputMode,
) -> Result<()>
where
I: IntoIterator<Item = WalkerItem>,
{
let max_clipboard_bytes = config.max_clipboard_bytes();
if estimated_files > 0 {
let estimated = estimation::estimate_output_size(estimated_files);
if max_clipboard_bytes > 0 && estimated > max_clipboard_bytes.saturating_mul(2) {
eprintln!(
"\n⚠ Warning: Estimated output size ({estimated} bytes) exceeds clipboard limit ({max_clipboard_bytes} bytes)"
);
eprintln!(" Consider increasing --max-clipboard-mb or using streaming mode");
}
}
let result = match printer_opts.format {
OutputFormat::Markdown => {
buffer_entries_with_limit(walker, printer_opts, max_clipboard_bytes, estimated_files)?
}
OutputFormat::Tree => buffer_tree_entries(
walker,
printer_opts,
max_clipboard_bytes,
config.max_files(),
)?,
};
if result.size_exceeded {
eprintln!("\n⚠ Clipboard size limit exceeded ({max_clipboard_bytes} bytes)");
eprintln!(
" Processed {processed} of {seen}+ files before limit was reached",
processed = result.processed,
seen = result.total_seen,
);
eprintln!(" Output has been truncated");
eprintln!(" Consider using --max-clipboard-mb to increase the limit");
eprintln!(" or remove --clip to stream output directly\n");
}
if output_mode.should_show_stdout() {
use std::io::{self, Write};
let stdout = io::stdout();
let mut handle = stdout.lock();
match handle.write_all(result.output.as_bytes()) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
log::debug!("Stdout broken pipe in clipboard mode; continuing to clipboard copy");
}
Err(e) => return Err(e.into()),
}
}
if result.size_exceeded {
eprintln!(" Skipping clipboard copy due to size limit");
} else if let Err(e) = clipboard::copy_to_clipboard(&result.output) {
eprintln!("✗ Clipboard error: {e}");
} else {
eprintln!("✓ Copied to clipboard ({} bytes)", result.output.len());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fence_guard_lifecycle() {
let mut guard = FenceGuard::new();
assert!(!guard.open);
guard.mark_open();
assert!(guard.open);
guard.mark_open();
assert!(guard.open);
guard.mark_closed();
assert!(!guard.open);
}
#[test]
fn test_fence_guard_close_if_needed() {
let guard = FenceGuard::new();
let mut output = String::from("existing");
let len_before = output.len();
guard.close_if_needed(&mut output);
assert_eq!(output.len(), len_before);
let mut guard = FenceGuard::new();
guard.mark_open();
let mut output = String::new();
guard.close_if_needed(&mut output);
assert_eq!(output, "\n```\n\n");
}
#[test]
fn test_fence_guard_snapshot_independence() {
let mut guard = FenceGuard::new();
guard.mark_open();
let snapshot = guard; guard.mark_closed();
assert!(
snapshot.open,
"Snapshot should retain state at time of copy"
);
assert!(!guard.open, "Original should reflect later mutation");
}
#[test]
fn test_buffer_result_creation() {
let result = BufferResult {
output: "test".to_string(),
processed: 10,
total_seen: 15,
size_exceeded: true,
};
assert_eq!(result.output, "test");
assert_eq!(result.processed, 10);
assert_eq!(result.total_seen, 15);
assert!(result.size_exceeded);
}
}