1use crate::fuser::FusionPlan;
16use crate::ir::Circuit;
17
18mod card;
19mod diagram;
20mod draw;
21pub mod glyphs;
22mod labels;
23mod rows;
24mod segments;
25mod strip;
26
27pub use glyphs::Glyphs;
28
29pub const COMPACT_BELOW: usize = 72;
31pub const MIN_WIDTH: usize = 40;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum Style {
37 Plain,
39 Heading,
41 Accent,
43 Dim,
45 Border,
47 OpHeavy,
49 OpLight,
51 EdgeMaterialized,
53 EdgeFused,
55 FusedFrame,
57 Format,
59 NumericsBitIdentical,
61 NumericsReference,
63 NumericsDiffers,
65 LayerGdn,
67 LayerAttn,
69 LayerDense,
71 LayerMoe,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Span {
78 pub text: String,
80 pub style: Style,
82}
83
84#[derive(Debug, Clone, Default, PartialEq, Eq)]
86pub struct Line {
87 pub spans: Vec<Span>,
89}
90
91impl Line {
92 pub fn width(&self) -> usize {
94 self.spans.iter().map(|s| s.text.chars().count()).sum()
95 }
96
97 pub fn plain(&self) -> String {
99 self.spans.iter().map(|s| s.text.as_str()).collect()
100 }
101
102 pub(crate) fn push(&mut self, text: impl Into<String>, style: Style) -> &mut Self {
103 let text = text.into();
104 if !text.is_empty() {
105 self.spans.push(Span { text, style });
106 }
107 self
108 }
109
110 pub(crate) fn pad_to(&mut self, width: usize) -> &mut Self {
111 let w = self.width();
112 if w < width {
113 self.push(" ".repeat(width - w), Style::Plain);
114 }
115 self
116 }
117}
118
119#[derive(Debug, Clone, Default, PartialEq, Eq)]
121pub struct Document {
122 pub lines: Vec<Line>,
124}
125
126impl Document {
127 pub fn plain(&self) -> String {
129 let mut out = String::new();
130 for l in &self.lines {
131 out.push_str(l.plain().trim_end());
132 out.push('\n');
133 }
134 out
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum Expand {
141 Summary,
143 Layer(usize),
145 AllLayers,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct DisplayOpts {
152 pub width: usize,
154 pub glyphs: Glyphs,
156 pub expand: Expand,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct DisplayInfo {
163 pub checkpoint: String,
165 pub recipe: String,
167 pub bytes: Option<(u64, u64)>,
170 pub pipelines: Vec<Option<Vec<String>>>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
177pub enum DisplayError {
178 #[error("layer {layer} is out of range: this model has layers 0..={last}")]
180 LayerOutOfRange {
181 layer: usize,
183 last: usize,
185 },
186 #[error("the {0} plan has no numbered layers; drop --layer")]
188 NoLayers(&'static str),
189 #[error("width {0} is below the {MIN_WIDTH}-column minimum")]
191 TooNarrow(usize),
192}
193
194pub fn display(
196 circuit: &Circuit,
197 plan: &FusionPlan,
198 info: &DisplayInfo,
199 opts: &DisplayOpts,
200) -> Result<Document, DisplayError> {
201 if opts.width < MIN_WIDTH {
202 return Err(DisplayError::TooNarrow(opts.width));
203 }
204 let layers = diagram::layers_in_plan(circuit, plan);
205 if let Expand::Layer(n) = opts.expand {
206 let Some(&last) = layers.last() else {
207 return Err(DisplayError::NoLayers(plan.mode.name()));
208 };
209 if !layers.contains(&n) {
210 return Err(DisplayError::LayerOutOfRange { layer: n, last });
211 }
212 }
213 let g = opts.glyphs.set();
214 let mut doc = Document::default();
215 card::header(&mut doc, circuit, plan, info, opts.width, &g);
216 if !layers.is_empty() {
217 strip::strip(&mut doc, circuit, &layers, opts.width, &g);
218 }
219 diagram::diagrams(&mut doc, circuit, plan, opts, &g, &info.pipelines);
220 card::footer(&mut doc, plan, info, opts.width, &g);
221 for l in &mut doc.lines {
222 fit(l, opts.width, &g);
223 }
224 Ok(doc)
225}
226
227pub(crate) fn fit(line: &mut Line, width: usize, g: &glyphs::Set) {
230 if line.width() <= width {
231 return;
232 }
233 let keep = width.saturating_sub(g.ellipsis.chars().count());
234 let mut used = 0;
235 let mut out = Vec::new();
236 for s in line.spans.drain(..) {
237 let n = s.text.chars().count();
238 if used + n <= keep {
239 used += n;
240 out.push(s);
241 continue;
242 }
243 let text: String = s.text.chars().take(keep - used).collect();
244 out.push(Span {
245 text,
246 style: s.style,
247 });
248 out.push(Span {
249 text: g.ellipsis.to_string(),
250 style: Style::Dim,
251 });
252 break;
253 }
254 line.spans = out;
255}
256
257pub(crate) fn clip(text: &str, max: usize, g: &glyphs::Set) -> String {
259 if text.chars().count() <= max {
260 return text.to_string();
261 }
262 let e = g.ellipsis.chars().count();
263 let mut s: String = text.chars().take(max.saturating_sub(e)).collect();
264 s.push_str(g.ellipsis);
265 s.chars().take(max).collect()
266}