1use crate::app::{App, CompPane};
5use crate::markdown::truncate_to_width;
6use crate::theme::{GLYPH_BROKEN, GLYPH_COMPUTATION, GLYPH_OK};
7use crate::ui::widgets::input_line;
8use okf_core::ComputationSource;
9use ratatui::Frame;
10use ratatui::layout::{Constraint, Layout, Rect};
11use ratatui::style::{Modifier, Style};
12use ratatui::text::{Line, Span};
13use ratatui::widgets::{Block, Borders, Paragraph};
14
15pub fn draw(frame: &mut Frame, app: &App, area: Rect) {
22 let theme = &app.theme;
23 let snapshot = app.snapshot.as_ref().expect("drawn only with a snapshot");
24 let block = Block::new()
25 .borders(Borders::ALL)
26 .title(format!(
27 " Computations ── {} contract(s) ",
28 snapshot.contracts.len()
29 ))
30 .border_style(theme.dim());
31 let inner = block.inner(area);
32 frame.render_widget(block, area);
33
34 if snapshot.contracts.is_empty() {
35 frame.render_widget(
36 Paragraph::new("\n no Attested Computation concepts in this bundle"),
37 inner,
38 );
39 return;
40 }
41
42 let [list_area, detail_area] =
43 Layout::horizontal([Constraint::Length(26), Constraint::Min(20)]).areas(inner);
44 draw_list(frame, app, list_area);
45 draw_detail(frame, app, detail_area);
46}
47
48fn draw_list(frame: &mut Frame, app: &App, area: Rect) {
49 let theme = &app.theme;
50 let snapshot = app.snapshot.as_ref().expect("drawn only with a snapshot");
51 let focused = app.computations.pane == CompPane::List;
52 let mut lines = vec![Line::from(Span::styled(
53 "CONTRACTS",
54 if focused {
55 theme.accent().add_modifier(Modifier::BOLD)
56 } else {
57 theme.dim()
58 },
59 ))];
60 for (ix, info) in snapshot.contracts.iter().enumerate() {
61 let selected = ix == app.computations.selected;
62 let marker = if selected { "▸" } else { " " };
63 let (verdict, style) = if info.healthy() {
64 (GLYPH_OK, theme.ok())
65 } else {
66 (GLYPH_BROKEN, theme.error())
67 };
68 let mut line = Line::from(vec![
69 Span::raw(format!("{marker} ")),
70 Span::styled(format!("{GLYPH_COMPUTATION} "), theme.accent()),
71 Span::raw(format!("{:<16}", truncate_to_width(info.id.name(), 16))),
72 Span::styled(verdict.to_string(), style),
73 ]);
74 if selected && focused {
75 line = line.style(theme.selection());
76 }
77 lines.push(line);
78 }
79 frame.render_widget(Paragraph::new(lines), area);
80}
81
82#[allow(clippy::too_many_lines)]
83fn draw_detail(frame: &mut Frame, app: &App, area: Rect) {
84 let theme = &app.theme;
85 let snapshot = app.snapshot.as_ref().expect("drawn only with a snapshot");
86 let Some(info) = snapshot.contracts.get(app.computations.selected) else {
87 return;
88 };
89 let contract = &info.contract;
90 let width = usize::from(area.width).saturating_sub(2);
91 let form_focused = app.computations.pane == CompPane::Form;
92
93 let mut lines: Vec<Line<'static>> = Vec::new();
94 let meta = snapshot.meta(&info.id);
95 lines.push(Line::from(vec![
96 Span::styled(
97 format!("{} ", info.id),
98 Style::default().add_modifier(Modifier::BOLD),
99 ),
100 Span::styled(
101 format!(
102 "· runtime: {} · {}",
103 contract.runtime.clone().unwrap_or_else(|| "?".into()),
104 meta.map_or_else(String::new, |m| format!(
105 "{} {}",
106 crate::theme::status_glyph(&m.status),
107 crate::theme::tier_glyph(m.tier)
108 ))
109 ),
110 theme.dim(),
111 ),
112 ]));
113 lines.push(Line::default());
114
115 lines.push(Line::from(Span::styled("CONTRACT", theme.accent())));
117 if contract.parameters.is_empty() {
118 lines.push(Line::from(Span::styled(" (no parameters)", theme.dim())));
119 }
120 for parameter in &contract.parameters {
121 lines.push(Line::from(vec![
122 Span::styled(" parameter ".to_string(), theme.dim()),
123 Span::raw(parameter.to_string()),
124 ]));
125 }
126 for (field, raw, resolved) in &info.path_checks {
127 let (glyph, style) = if resolved.is_some() {
128 (GLYPH_OK, theme.ok())
129 } else {
130 (GLYPH_BROKEN, theme.error())
131 };
132 lines.push(Line::from(vec![
133 Span::styled(format!(" {field:<11}"), theme.dim()),
134 Span::raw(truncate_to_width(raw, width.saturating_sub(16))),
135 Span::styled(format!(" {glyph}"), style),
136 ]));
137 }
138 if let Some(executor) = &contract.executor
139 && !executor.receipt.is_empty()
140 {
141 lines.push(Line::from(vec![
142 Span::styled(" receipt ".to_string(), theme.dim()),
143 Span::raw(executor.receipt.join(", ")),
144 ]));
145 }
146 for issue in &info.issues {
147 lines.push(Line::from(Span::styled(
148 format!(" {GLYPH_BROKEN} {issue}"),
149 theme.error(),
150 )));
151 }
152 lines.push(Line::default());
153
154 match &contract.computation {
156 ComputationSource::Inline(inline) => {
157 let verdict = match &info.syntax {
158 Some(Ok(())) => format!(" syntax {GLYPH_OK}"),
159 Some(Err(_)) => format!(" syntax {GLYPH_BROKEN}"),
160 None => String::new(),
161 };
162 lines.push(Line::from(Span::styled(
163 format!(
164 "COMPUTATION (inline · {}{verdict})",
165 inline.language.clone().unwrap_or_else(|| "?".into())
166 ),
167 theme.accent(),
168 )));
169 for code_line in inline.code.lines().take(10) {
170 lines.push(Line::from(vec![
171 Span::styled("│ ".to_string(), theme.dim()),
172 Span::raw(truncate_to_width(code_line, width.saturating_sub(2))),
173 ]));
174 }
175 if inline.code.lines().count() > 10 {
176 lines.push(Line::from(Span::styled("│ …", theme.dim())));
177 }
178 }
179 ComputationSource::File(path) => {
180 lines.push(Line::from(vec![
181 Span::styled("COMPUTATION ".to_string(), theme.accent()),
182 Span::raw(format!("file: {path}")),
183 ]));
184 }
185 ComputationSource::Missing => {
186 lines.push(Line::from(Span::styled(
187 format!("COMPUTATION {GLYPH_BROKEN} missing"),
188 theme.error(),
189 )));
190 }
191 }
192 lines.push(Line::default());
193
194 lines.push(Line::from(Span::styled(
196 "PLAYGROUND ── invocation builder",
197 if form_focused {
198 theme.accent().add_modifier(Modifier::BOLD)
199 } else {
200 theme.accent()
201 },
202 )));
203 let mut args: Vec<String> = Vec::new();
204 for (ix, parameter) in contract.parameters.iter().enumerate() {
205 let name = parameter.name.clone().unwrap_or_default();
206 let key = format!("{}\u{0}{}", info.id, name);
207 let value = app
208 .computations
209 .values
210 .get(&key)
211 .cloned()
212 .unwrap_or_default();
213 let type_ = parameter.type_.clone().unwrap_or_else(|| "string".into());
214 let valid = check_type(&type_, &value);
215 let required = parameter.is_required();
216 let verdict = if value.is_empty() {
217 if required {
218 Span::styled(format!(" {GLYPH_BROKEN} required"), app.theme.error())
219 } else {
220 Span::styled(format!(" ({type_}, opt)"), app.theme.dim())
221 }
222 } else if valid {
223 Span::styled(format!(" {GLYPH_OK} {type_}"), app.theme.ok())
224 } else {
225 Span::styled(format!(" {GLYPH_BROKEN} not a {type_}"), app.theme.error())
226 };
227 let active = form_focused && ix == app.computations.field;
228 let mut line = input_line(
229 &format!(" {name:<14}"),
230 &value,
231 active,
232 theme,
233 width.saturating_sub(16),
234 );
235 line.spans.push(verdict);
236 lines.push(line);
237 if !value.is_empty() || required {
238 args.push(format!("{name}={value}"));
239 }
240 }
241 lines.push(Line::default());
242 lines.push(Line::from(vec![
243 Span::styled(" ▶ call sketch: ".to_string(), theme.dim()),
244 Span::styled(
245 format!("{}({})", info.id.name(), args.join(", ")),
246 Style::default().add_modifier(Modifier::BOLD),
247 ),
248 ]));
249 if let Some(executor) = &contract.executor
250 && !executor.receipt.is_empty()
251 {
252 lines.push(Line::from(vec![
253 Span::styled(" ▶ expected receipt: ".to_string(), theme.dim()),
254 Span::raw(executor.receipt.join(", ")),
255 ]));
256 }
257 lines.push(Line::from(Span::styled(
258 " Tab focuses the form · Ctrl+Y copies the sketch",
259 theme.dim(),
260 )));
261
262 let visible: Vec<Line<'static>> = lines.into_iter().take(usize::from(area.height)).collect();
263 frame.render_widget(Paragraph::new(visible), area);
264}
265
266fn check_type(type_: &str, value: &str) -> bool {
268 if value.is_empty() {
269 return false;
270 }
271 match type_.to_ascii_lowercase().as_str() {
272 "number" | "float" | "integer" | "int" => value.parse::<f64>().is_ok(),
273 "boolean" | "bool" => matches!(value, "true" | "false"),
274 _ => true,
275 }
276}