ftui_runtime/
evidence_sink.rs1#![forbid(unsafe_code)]
2
3use std::fs::OpenOptions;
11use std::io::{self, BufWriter, Write};
12use std::path::PathBuf;
13use std::sync::{Arc, Mutex};
14
15pub const EVIDENCE_SCHEMA_VERSION: &str = "ftui-evidence-v1";
17
18#[derive(Debug, Clone)]
20pub enum EvidenceSinkDestination {
21 Stdout,
23 File(PathBuf),
25}
26
27impl EvidenceSinkDestination {
28 #[must_use]
30 pub fn file(path: impl Into<PathBuf>) -> Self {
31 Self::File(path.into())
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct EvidenceSinkConfig {
38 pub enabled: bool,
40 pub destination: EvidenceSinkDestination,
42 pub flush_on_write: bool,
44}
45
46impl Default for EvidenceSinkConfig {
47 fn default() -> Self {
48 Self {
49 enabled: false,
50 destination: EvidenceSinkDestination::Stdout,
51 flush_on_write: true,
52 }
53 }
54}
55
56impl EvidenceSinkConfig {
57 #[must_use]
59 pub fn disabled() -> Self {
60 Self::default()
61 }
62
63 #[must_use]
65 pub fn enabled_stdout() -> Self {
66 Self {
67 enabled: true,
68 destination: EvidenceSinkDestination::Stdout,
69 flush_on_write: true,
70 }
71 }
72
73 #[must_use]
75 pub fn enabled_file(path: impl Into<PathBuf>) -> Self {
76 Self {
77 enabled: true,
78 destination: EvidenceSinkDestination::file(path),
79 flush_on_write: true,
80 }
81 }
82
83 #[must_use]
85 pub fn with_enabled(mut self, enabled: bool) -> Self {
86 self.enabled = enabled;
87 self
88 }
89
90 #[must_use]
92 pub fn with_destination(mut self, destination: EvidenceSinkDestination) -> Self {
93 self.destination = destination;
94 self
95 }
96
97 #[must_use]
99 pub fn with_flush_on_write(mut self, enabled: bool) -> Self {
100 self.flush_on_write = enabled;
101 self
102 }
103}
104
105struct EvidenceSinkInner {
106 writer: BufWriter<Box<dyn Write + Send>>,
107 flush_on_write: bool,
108}
109
110#[derive(Clone)]
112pub struct EvidenceSink {
113 inner: Arc<Mutex<EvidenceSinkInner>>,
114}
115
116impl std::fmt::Debug for EvidenceSink {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("EvidenceSink").finish()
119 }
120}
121
122impl EvidenceSink {
123 pub fn from_config(config: &EvidenceSinkConfig) -> io::Result<Option<Self>> {
125 if !config.enabled {
126 return Ok(None);
127 }
128
129 let writer: Box<dyn Write + Send> = match &config.destination {
130 EvidenceSinkDestination::Stdout => Box::new(io::stdout()),
131 EvidenceSinkDestination::File(path) => {
132 let file = OpenOptions::new().create(true).append(true).open(path)?;
133 Box::new(file)
134 }
135 };
136
137 let inner = EvidenceSinkInner {
138 writer: BufWriter::new(writer),
139 flush_on_write: config.flush_on_write,
140 };
141
142 Ok(Some(Self {
143 inner: Arc::new(Mutex::new(inner)),
144 }))
145 }
146
147 pub fn write_jsonl(&self, line: &str) -> io::Result<()> {
149 let mut inner = match self.inner.lock() {
150 Ok(guard) => guard,
151 Err(poisoned) => poisoned.into_inner(),
152 };
153 inner.writer.write_all(line.as_bytes())?;
154 inner.writer.write_all(b"\n")?;
155 if inner.flush_on_write {
156 inner.writer.flush()?;
157 }
158 Ok(())
159 }
160
161 pub fn flush(&self) -> io::Result<()> {
163 let mut inner = match self.inner.lock() {
164 Ok(guard) => guard,
165 Err(poisoned) => poisoned.into_inner(),
166 };
167 inner.writer.flush()
168 }
169}