headson/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(
3    clippy::unwrap_used,
4    clippy::expect_used,
5    clippy::print_stdout,
6    clippy::print_stderr
7)]
8#![allow(
9    clippy::multiple_crate_versions,
10    reason = "Dependency graph pulls distinct versions (e.g., yaml-rust2)."
11)]
12#![cfg_attr(
13    test,
14    allow(
15        clippy::unwrap_used,
16        clippy::expect_used,
17        reason = "tests may use unwrap/expect for brevity"
18    )
19)]
20
21use anyhow::Result;
22
23mod format;
24mod ingest;
25mod order;
26mod serialization;
27mod utils;
28pub use order::types::{ArrayBias, ArraySamplerStrategy};
29pub use order::{
30    NodeId, NodeKind, PriorityConfig, PriorityOrder, RankedNode, build_order,
31};
32
33pub use serialization::color::resolve_color_enabled;
34pub use serialization::types::{
35    ColorMode, OutputTemplate, RenderConfig, Style,
36};
37
38#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
39pub struct Budgets {
40    pub byte_budget: Option<usize>,
41    pub line_budget: Option<usize>,
42}
43
44pub fn headson(
45    input: Vec<u8>,
46    config: &RenderConfig,
47    priority_cfg: &PriorityConfig,
48    budget: usize,
49) -> Result<String> {
50    let arena = crate::ingest::parse_json_one(input, priority_cfg)?;
51    let order_build = order::build_order(&arena, priority_cfg)?;
52    let out = find_largest_render_under_budgets(
53        &order_build,
54        config,
55        Budgets {
56            byte_budget: Some(budget),
57            line_budget: None,
58        },
59    );
60    Ok(out)
61}
62
63pub fn headson_many(
64    inputs: Vec<(String, Vec<u8>)>,
65    config: &RenderConfig,
66    priority_cfg: &PriorityConfig,
67    budget: usize,
68) -> Result<String> {
69    let arena = crate::ingest::parse_json_many(inputs, priority_cfg)?;
70    let order_build = order::build_order(&arena, priority_cfg)?;
71    let out = find_largest_render_under_budgets(
72        &order_build,
73        config,
74        Budgets {
75            byte_budget: Some(budget),
76            line_budget: None,
77        },
78    );
79    Ok(out)
80}
81
82/// Same as `headson` but using the YAML ingest path.
83pub fn headson_yaml(
84    input: Vec<u8>,
85    config: &RenderConfig,
86    priority_cfg: &PriorityConfig,
87    budget: usize,
88) -> Result<String> {
89    let arena = crate::ingest::parse_yaml_one(input, priority_cfg)?;
90    let order_build = order::build_order(&arena, priority_cfg)?;
91    let out = find_largest_render_under_budgets(
92        &order_build,
93        config,
94        Budgets {
95            byte_budget: Some(budget),
96            line_budget: None,
97        },
98    );
99    Ok(out)
100}
101
102/// Same as `headson_many` but using the YAML ingest path.
103pub fn headson_many_yaml(
104    inputs: Vec<(String, Vec<u8>)>,
105    config: &RenderConfig,
106    priority_cfg: &PriorityConfig,
107    budget: usize,
108) -> Result<String> {
109    let arena = crate::ingest::parse_yaml_many(inputs, priority_cfg)?;
110    let order_build = order::build_order(&arena, priority_cfg)?;
111    let out = find_largest_render_under_budgets(
112        &order_build,
113        config,
114        Budgets {
115            byte_budget: Some(budget),
116            line_budget: None,
117        },
118    );
119    Ok(out)
120}
121
122/// Same as `headson` but using the Text ingest path.
123pub fn headson_text(
124    input: Vec<u8>,
125    config: &RenderConfig,
126    priority_cfg: &PriorityConfig,
127    budget: usize,
128) -> Result<String> {
129    let arena = crate::ingest::parse_text_one(input, priority_cfg)?;
130    let order_build = order::build_order(&arena, priority_cfg)?;
131    let out = find_largest_render_under_budgets(
132        &order_build,
133        config,
134        Budgets {
135            byte_budget: Some(budget),
136            line_budget: None,
137        },
138    );
139    Ok(out)
140}
141
142/// Same as `headson_many` but using the Text ingest path.
143pub fn headson_many_text(
144    inputs: Vec<(String, Vec<u8>)>,
145    config: &RenderConfig,
146    priority_cfg: &PriorityConfig,
147    budget: usize,
148) -> Result<String> {
149    let arena = crate::ingest::parse_text_many(inputs, priority_cfg)?;
150    let order_build = order::build_order(&arena, priority_cfg)?;
151    let out = find_largest_render_under_budgets(
152        &order_build,
153        config,
154        Budgets {
155            byte_budget: Some(budget),
156            line_budget: None,
157        },
158    );
159    Ok(out)
160}
161
162/// New generalized budgeting: enforce optional char and/or line caps.
163fn find_largest_render_under_budgets(
164    order_build: &PriorityOrder,
165    config: &RenderConfig,
166    budgets: Budgets,
167) -> String {
168    // Binary search the largest k in [1, total] whose render
169    // fits within all requested budgets.
170    let total = order_build.total_nodes;
171    if total == 0 {
172        return String::new();
173    }
174    // Each included node contributes at least some output; cap hi by budget.
175    let lo = 1usize;
176    // For the upper bound, when a byte budget is present, we can safely cap by it;
177    // otherwise, cap by total.
178    let hi = match budgets.byte_budget {
179        Some(c) => total.min(c.max(1)),
180        None => total,
181    };
182    // Reuse render-inclusion flags across render attempts to avoid clearing the vector.
183    // A node participates in the current render attempt when inclusion_flags[id] == render_set_id.
184    let mut inclusion_flags: Vec<u32> = vec![0; total];
185    // Each render attempt bumps this non-zero identifier to create a fresh inclusion set.
186    let mut render_set_id: u32 = 1;
187    // Measure length without color so ANSI escapes do not count toward the
188    // byte budget. Then render once more with the requested color setting.
189    let mut best_k: Option<usize> = None;
190    let mut measure_cfg = config.clone();
191    measure_cfg.color_enabled = false;
192
193    let _ = crate::utils::search::binary_search_max(lo, hi, |mid| {
194        let s = crate::serialization::render_top_k(
195            order_build,
196            mid,
197            &mut inclusion_flags,
198            render_set_id,
199            &measure_cfg,
200        );
201        render_set_id = render_set_id.wrapping_add(1).max(1);
202        // Measure output using a unified stats helper and enforce
203        // all provided caps (chars and/or lines).
204        let stats = crate::utils::measure::count_output_stats(&s);
205        let fits_chars = budgets.byte_budget.is_none_or(|c| stats.bytes <= c);
206        let fits_lines = budgets.line_budget.is_none_or(|l| stats.lines <= l);
207        if fits_chars && fits_lines {
208            best_k = Some(mid);
209            true
210        } else {
211            false
212        }
213    });
214
215    if let Some(k) = best_k {
216        // Final render with original color settings
217        crate::serialization::render_top_k(
218            order_build,
219            k,
220            &mut inclusion_flags,
221            render_set_id,
222            config,
223        )
224    } else {
225        // Fallback: always render a single node (k=1) to produce the
226        // shortest possible preview, even if it exceeds the byte budget.
227        crate::serialization::render_top_k(
228            order_build,
229            1,
230            &mut inclusion_flags,
231            render_set_id,
232            config,
233        )
234    }
235}
236
237// Optional new public API that accepts both budgets explicitly.
238pub fn headson_with_budgets(
239    input: Vec<u8>,
240    config: &RenderConfig,
241    priority_cfg: &PriorityConfig,
242    budgets: Budgets,
243) -> Result<String> {
244    let arena = crate::ingest::parse_json_one(input, priority_cfg)?;
245    let order_build = order::build_order(&arena, priority_cfg)?;
246    Ok(find_largest_render_under_budgets(
247        &order_build,
248        config,
249        budgets,
250    ))
251}
252
253pub fn headson_many_with_budgets(
254    inputs: Vec<(String, Vec<u8>)>,
255    config: &RenderConfig,
256    priority_cfg: &PriorityConfig,
257    budgets: Budgets,
258) -> Result<String> {
259    let arena = crate::ingest::parse_json_many(inputs, priority_cfg)?;
260    let order_build = order::build_order(&arena, priority_cfg)?;
261    Ok(find_largest_render_under_budgets(
262        &order_build,
263        config,
264        budgets,
265    ))
266}
267
268pub fn headson_yaml_with_budgets(
269    input: Vec<u8>,
270    config: &RenderConfig,
271    priority_cfg: &PriorityConfig,
272    budgets: Budgets,
273) -> Result<String> {
274    let arena = crate::ingest::parse_yaml_one(input, priority_cfg)?;
275    let order_build = order::build_order(&arena, priority_cfg)?;
276    Ok(find_largest_render_under_budgets(
277        &order_build,
278        config,
279        budgets,
280    ))
281}
282
283pub fn headson_many_yaml_with_budgets(
284    inputs: Vec<(String, Vec<u8>)>,
285    config: &RenderConfig,
286    priority_cfg: &PriorityConfig,
287    budgets: Budgets,
288) -> Result<String> {
289    let arena = crate::ingest::parse_yaml_many(inputs, priority_cfg)?;
290    let order_build = order::build_order(&arena, priority_cfg)?;
291    Ok(find_largest_render_under_budgets(
292        &order_build,
293        config,
294        budgets,
295    ))
296}
297
298pub fn headson_text_with_budgets(
299    input: Vec<u8>,
300    config: &RenderConfig,
301    priority_cfg: &PriorityConfig,
302    budgets: Budgets,
303) -> Result<String> {
304    let arena = crate::ingest::parse_text_one(input, priority_cfg)?;
305    let order_build = order::build_order(&arena, priority_cfg)?;
306    Ok(find_largest_render_under_budgets(
307        &order_build,
308        config,
309        budgets,
310    ))
311}
312
313pub fn headson_many_text_with_budgets(
314    inputs: Vec<(String, Vec<u8>)>,
315    config: &RenderConfig,
316    priority_cfg: &PriorityConfig,
317    budgets: Budgets,
318) -> Result<String> {
319    let arena = crate::ingest::parse_text_many(inputs, priority_cfg)?;
320    let order_build = order::build_order(&arena, priority_cfg)?;
321    Ok(find_largest_render_under_budgets(
322        &order_build,
323        config,
324        budgets,
325    ))
326}