jk_core/command_history/
result.rs1use std::path::PathBuf;
2
3use super::redaction::redact_text;
4
5#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct CommandResultSummary {
8 pub exit_status: Option<ExitStatusSummary>,
10 pub spawn_error: Option<String>,
12 pub stdout: StreamSummary,
14 pub stderr: StreamSummary,
16}
17
18impl Default for CommandResultSummary {
19 fn default() -> Self {
20 Self {
21 exit_status: None,
22 spawn_error: None,
23 stdout: StreamSummary::empty(),
24 stderr: StreamSummary::empty(),
25 }
26 }
27}
28
29impl CommandResultSummary {
30 pub(super) fn apply_retention(&mut self, retention: &OutputRetention) {
31 self.stdout.apply_limit(retention.stdout_limit());
32 self.stderr.apply_limit(retention.stderr_limit());
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct ExitStatusSummary {
39 pub code: Option<i32>,
41 pub signal: Option<i32>,
43 pub success: bool,
45}
46
47impl ExitStatusSummary {
48 #[must_use]
50 pub const fn code(code: i32) -> Self {
51 Self {
52 code: Some(code),
53 signal: None,
54 success: code == 0,
55 }
56 }
57
58 #[must_use]
60 pub const fn signal(signal: i32) -> Self {
61 Self {
62 code: None,
63 signal: Some(signal),
64 success: false,
65 }
66 }
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
71#[non_exhaustive]
72pub enum OutputRetention {
73 SummaryOnly {
75 stdout_limit: usize,
77 stderr_limit: usize,
79 },
80 Artifact {
82 stdout_limit: usize,
84 stderr_limit: usize,
86 full_output_path: PathBuf,
88 },
89}
90
91impl OutputRetention {
92 #[must_use]
94 pub const fn summary_only(stdout_limit: usize, stderr_limit: usize) -> Self {
95 Self::SummaryOnly {
96 stdout_limit,
97 stderr_limit,
98 }
99 }
100
101 const fn stdout_limit(&self) -> usize {
102 match self {
103 Self::SummaryOnly { stdout_limit, .. } | Self::Artifact { stdout_limit, .. } => {
104 *stdout_limit
105 }
106 }
107 }
108
109 const fn stderr_limit(&self) -> usize {
110 match self {
111 Self::SummaryOnly { stderr_limit, .. } | Self::Artifact { stderr_limit, .. } => {
112 *stderr_limit
113 }
114 }
115 }
116}
117
118#[derive(Clone, Debug, Default, Eq, PartialEq)]
120pub struct StreamSummary {
121 pub byte_len: usize,
123 pub line_count: usize,
125 pub snippet: String,
127 pub truncated: bool,
129 pub redacted: bool,
131}
132
133impl StreamSummary {
134 #[must_use]
136 pub const fn empty() -> Self {
137 Self {
138 byte_len: 0,
139 line_count: 0,
140 snippet: String::new(),
141 truncated: false,
142 redacted: false,
143 }
144 }
145
146 #[must_use]
148 pub fn from_bytes(bytes: &[u8], limit: usize) -> Self {
149 Self::from_text(&String::from_utf8_lossy(bytes), limit).with_byte_len(bytes.len())
150 }
151
152 #[must_use]
154 pub fn from_text(text: &str, limit: usize) -> Self {
155 let byte_len = text.len();
156 let line_count = text.lines().count();
157 let (redacted_text, redacted) = redact_text(text);
158 let (snippet, truncated) = truncate_to_boundary(&redacted_text, limit);
159
160 Self {
161 byte_len,
162 line_count,
163 snippet,
164 truncated,
165 redacted,
166 }
167 }
168
169 const fn with_byte_len(mut self, byte_len: usize) -> Self {
170 self.byte_len = byte_len;
171 self
172 }
173
174 fn apply_limit(&mut self, limit: usize) {
175 let (snippet, truncated) = truncate_to_boundary(&self.snippet, limit);
176 self.truncated |= truncated;
177 self.snippet = snippet;
178 }
179}
180
181fn truncate_to_boundary(text: &str, limit: usize) -> (String, bool) {
182 if text.len() <= limit {
183 return (text.to_owned(), false);
184 }
185
186 if limit == 0 {
187 return (String::new(), true);
188 }
189
190 let mut end = limit;
191 while !text.is_char_boundary(end) {
192 end -= 1;
193 }
194 (text[..end].to_owned(), true)
195}