#![forbid(unsafe_code)]
use crate::TerminalWriter;
use ftui_render::sanitize::sanitize;
use std::io::{self, Write};
pub struct LogSink<'a, W: Write> {
writer: &'a mut TerminalWriter<W>,
buffer: Vec<u8>,
}
impl<'a, W: Write> LogSink<'a, W> {
pub fn new(writer: &'a mut TerminalWriter<W>) -> Self {
Self {
writer,
buffer: Vec::with_capacity(1024),
}
}
}
impl<W: Write> Write for LogSink<'_, W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for &byte in buf {
if byte == b'\n' {
let line = String::from_utf8_lossy(&self.buffer);
let safe_line = sanitize(&line);
self.writer.write_log(&format!("{}\n", safe_line))?;
self.buffer.clear();
} else {
self.buffer.push(byte);
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
if !self.buffer.is_empty() {
let line = String::from_utf8_lossy(&self.buffer);
let safe_line = sanitize(&line);
self.writer.write_log(&safe_line)?;
self.buffer.clear();
}
self.writer.flush()
}
}
impl<W: Write> Drop for LogSink<'_, W> {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal_writer::{ScreenMode, UiAnchor};
use ftui_core::terminal_capabilities::TerminalCapabilities;
fn create_writer() -> TerminalWriter<Vec<u8>> {
TerminalWriter::new(
Vec::new(),
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
TerminalCapabilities::basic(),
)
}
#[test]
fn log_sink_buffers_lines() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
write!(sink, "Hello").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(
output_str.contains("Hello"),
"partial content should be flushed on drop"
);
}
#[test]
fn log_sink_sanitizes_output() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
writeln!(sink, "Unsafe \x1b[31mred\x1b[0m text").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains("Unsafe red text"));
assert!(!output_str.contains("\x1b[31m"));
}
#[test]
fn log_sink_flushes_partial_line() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
write!(sink, "Partial").unwrap();
sink.flush().unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains("Partial"));
}
#[test]
fn log_sink_multiple_lines() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
writeln!(sink, "Line1").unwrap();
writeln!(sink, "Line2").unwrap();
writeln!(sink, "Line3").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains("Line1"));
assert!(output_str.contains("Line2"));
assert!(output_str.contains("Line3"));
}
#[test]
fn log_sink_empty_write() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
let n = sink.write(b"").unwrap();
assert_eq!(n, 0);
}
}
#[test]
fn log_sink_newline_only() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
sink.write_all(b"\n").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains('\n'));
}
#[test]
fn log_sink_multiple_newlines_in_one_write() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
sink.write_all(b"A\nB\nC\n").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains('A'));
assert!(output_str.contains('B'));
assert!(output_str.contains('C'));
}
#[test]
fn log_sink_sanitizes_multiple_escapes() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
writeln!(sink, "\x1b[31mRed\x1b[0m \x1b[1mBold\x1b[0m").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains("Red"));
assert!(output_str.contains("Bold"));
assert!(!output_str.contains("\x1b[31m"));
assert!(!output_str.contains("\x1b[1m"));
}
#[test]
fn log_sink_invalid_utf8_lossy() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
sink.write_all(&[0xFF, 0xFE, b'\n']).unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains('\u{FFFD}') || !output_str.is_empty());
}
#[test]
fn log_sink_drop_without_flush_writes_partial() {
let mut writer = create_writer();
{
let mut sink = LogSink::new(&mut writer);
write!(sink, "NoNewline").unwrap();
}
let output = writer.into_inner().unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(
output_str.contains("NoNewline"),
"partial line should be written on drop"
);
}
#[test]
fn log_sink_write_returns_full_length() {
let mut writer = create_writer();
let mut sink = LogSink::new(&mut writer);
let data = b"Hello World\n";
let n = sink.write(data).unwrap();
assert_eq!(n, data.len());
}
}