headson 0.6.5__tar.gz → 0.6.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of headson might be problematic. Click here for more details.

Files changed (70) hide show
  1. {headson-0.6.5 → headson-0.6.6}/Cargo.lock +1 -1
  2. {headson-0.6.5 → headson-0.6.6}/Cargo.toml +1 -1
  3. {headson-0.6.5 → headson-0.6.6}/PKG-INFO +22 -2
  4. {headson-0.6.5 → headson-0.6.6}/README.md +21 -1
  5. {headson-0.6.5 → headson-0.6.6}/pyproject.toml +1 -1
  6. {headson-0.6.5 → headson-0.6.6}/python/Cargo.lock +2 -2
  7. {headson-0.6.5 → headson-0.6.6}/python/Cargo.toml +1 -1
  8. {headson-0.6.5 → headson-0.6.6}/src/lib.rs +14 -3
  9. {headson-0.6.5 → headson-0.6.6}/src/main.rs +54 -23
  10. {headson-0.6.5 → headson-0.6.6}/src/order/build.rs +1 -1
  11. {headson-0.6.5 → headson-0.6.6}/src/utils/measure.rs +20 -12
  12. {headson-0.6.5 → headson-0.6.6}/docs/assets/algorithm.svg +0 -0
  13. {headson-0.6.5 → headson-0.6.6}/docs/assets/logo.png +0 -0
  14. {headson-0.6.5 → headson-0.6.6}/docs/assets/logo.svg +0 -0
  15. {headson-0.6.5 → headson-0.6.6}/docs/assets/tapes/demo.gif +0 -0
  16. {headson-0.6.5 → headson-0.6.6}/python/README.md +0 -0
  17. {headson-0.6.5 → headson-0.6.6}/python/headson/__init__.py +0 -0
  18. {headson-0.6.5 → headson-0.6.6}/python/src/lib.rs +0 -0
  19. {headson-0.6.5 → headson-0.6.6}/src/format.rs +0 -0
  20. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/json/builder.rs +0 -0
  21. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/json/mod.rs +0 -0
  22. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/json/samplers/default.rs +0 -0
  23. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/json/samplers/head.rs +0 -0
  24. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/json/samplers/mod.rs +0 -0
  25. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/json/samplers/tail.rs +0 -0
  26. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/mod.rs +0 -0
  27. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/text/mod.rs +0 -0
  28. {headson-0.6.5 → headson-0.6.6}/src/ingest/formats/yaml/mod.rs +0 -0
  29. {headson-0.6.5 → headson-0.6.6}/src/ingest/mod.rs +0 -0
  30. {headson-0.6.5 → headson-0.6.6}/src/ingest/sampling/mod.rs +0 -0
  31. {headson-0.6.5 → headson-0.6.6}/src/order/mod.rs +0 -0
  32. {headson-0.6.5 → headson-0.6.6}/src/order/scoring.rs +0 -0
  33. {headson-0.6.5 → headson-0.6.6}/src/order/snapshots/headson__order__build__tests__order_empty_array_order.snap +0 -0
  34. {headson-0.6.5 → headson-0.6.6}/src/order/snapshots/headson__order__build__tests__order_single_string_array_order.snap +0 -0
  35. {headson-0.6.5 → headson-0.6.6}/src/order/types.rs +0 -0
  36. {headson-0.6.5 → headson-0.6.6}/src/serialization/color.rs +0 -0
  37. {headson-0.6.5 → headson-0.6.6}/src/serialization/fileset.rs +0 -0
  38. {headson-0.6.5 → headson-0.6.6}/src/serialization/mod.rs +0 -0
  39. {headson-0.6.5 → headson-0.6.6}/src/serialization/output.rs +0 -0
  40. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__arena_render_empty.snap +0 -0
  41. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__arena_render_empty_yaml.snap +0 -0
  42. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__arena_render_single.snap +0 -0
  43. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__arena_render_single_yaml.snap +0 -0
  44. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_internal_gaps_yaml.snap +0 -0
  45. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_omitted_js_head.snap +0 -0
  46. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_omitted_js_tail.snap +0 -0
  47. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_omitted_pseudo_head.snap +0 -0
  48. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_omitted_pseudo_tail.snap +0 -0
  49. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_omitted_yaml_head.snap +0 -0
  50. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__array_omitted_yaml_tail.snap +0 -0
  51. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__inline_open_array_in_object_json.snap +0 -0
  52. {headson-0.6.5 → headson-0.6.6}/src/serialization/snapshots/headson__serialization__tests__inline_open_array_in_object_yaml.snap +0 -0
  53. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/core.rs +0 -0
  54. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/js.rs +0 -0
  55. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/json.rs +0 -0
  56. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/mod.rs +0 -0
  57. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/pseudo.rs +0 -0
  58. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/text.rs +0 -0
  59. {headson-0.6.5 → headson-0.6.6}/src/serialization/templates/yaml.rs +0 -0
  60. {headson-0.6.5 → headson-0.6.6}/src/serialization/types.rs +0 -0
  61. {headson-0.6.5 → headson-0.6.6}/src/snapshots/headson__order__tests__order_empty_array_order.snap +0 -0
  62. {headson-0.6.5 → headson-0.6.6}/src/snapshots/headson__order__tests__order_single_string_array_order.snap +0 -0
  63. {headson-0.6.5 → headson-0.6.6}/src/utils/graph.rs +0 -0
  64. {headson-0.6.5 → headson-0.6.6}/src/utils/json.rs +0 -0
  65. {headson-0.6.5 → headson-0.6.6}/src/utils/mod.rs +0 -0
  66. {headson-0.6.5 → headson-0.6.6}/src/utils/search.rs +0 -0
  67. {headson-0.6.5 → headson-0.6.6}/src/utils/text.rs +0 -0
  68. {headson-0.6.5 → headson-0.6.6}/src/utils/tree_arena.rs +0 -0
  69. {headson-0.6.5 → headson-0.6.6}/tests/fixtures/json/JSONTestSuite/LICENSE +0 -0
  70. {headson-0.6.5 → headson-0.6.6}/tests/fixtures/json/JSONTestSuite/README.md +0 -0
@@ -298,7 +298,7 @@ dependencies = [
298
298
 
299
299
  [[package]]
300
300
  name = "headson"
301
- version = "0.6.5"
301
+ version = "0.6.6"
302
302
  dependencies = [
303
303
  "anyhow",
304
304
  "assert_cmd",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "headson"
3
- version = "0.6.5"
3
+ version = "0.6.6"
4
4
  edition = "2024"
5
5
  description = "Budget‑constrained JSON preview renderer"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: headson
3
- Version: 0.6.5
3
+ Version: 0.6.6
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Rust
@@ -70,7 +70,8 @@ If you’re comfortable with tools like `head` and `tail`, use `headson` when yo
70
70
 
71
71
  Common flags:
72
72
 
73
- - `-c, --bytes <BYTES>`: per‑file output budget. For multiple inputs, default total budget is `<BYTES> * number_of_inputs`.
73
+ - `-c, --bytes <BYTES>`: per‑file output budget (bytes). For multiple inputs, default total budget is `<BYTES> * number_of_inputs`.
74
+ - `-u, --chars <CHARS>`: per‑file output budget (Unicode code points). Behaves like `--bytes` but counts characters instead of bytes.
74
75
  - `-C, --global-bytes <BYTES>`: total output budget across all inputs. With `--bytes`, the effective total is the smaller of the two.
75
76
  - `-f, --format <auto|json|yaml|text>`: output format (default: `auto`).
76
77
  - Auto: stdin → JSON family; filesets → per‑file based on extension (`.json` → JSON family, `.yaml`/`.yml` → YAML, unknown → Text).
@@ -97,6 +98,25 @@ Notes:
97
98
  - Directories and binary files are ignored; a notice is printed to stderr for each. Stdin reads the stream as‑is.
98
99
  - Head vs Tail sampling: these options bias which part of arrays are kept before rendering. Display styles may still insert internal gap markers to honor very small budgets; strict JSON stays unannotated.
99
100
 
101
+ ## Budget Modes
102
+
103
+ - Bytes (`-c/--bytes`, `-C/--global-bytes`)
104
+ - Measures UTF‑8 bytes in the output.
105
+ - Default per‑file budget is 500 bytes when neither `--lines` nor `--chars` is provided.
106
+ - Multiple inputs: total default budget is `<BYTES> * number_of_inputs`; `--global-bytes` caps the total.
107
+
108
+ - Characters (`-u/--chars`)
109
+ - Measures Unicode code points (not grapheme clusters).
110
+
111
+ - Lines (`-n/--lines`, `-N/--global-lines`)
112
+ - Caps the number of lines in the output.
113
+ - Incompatible with `--no-newline`.
114
+ - Multiple inputs: defaults to `<LINES> * number_of_inputs`; `--global-lines` caps the total.
115
+
116
+ - Interactions and precedence
117
+ - All active budgets are enforced simultaneously. The render must satisfy all of: bytes (if set), chars (if set), and lines (if set). The strictest cap wins.
118
+ - When only lines are specified, no implicit byte cap applies. When neither lines nor chars are specified, a 500‑byte default applies.
119
+
100
120
  Quick one‑liners:
101
121
 
102
122
  - Peek a big JSON stream (keeps structure):
@@ -56,7 +56,8 @@ If you’re comfortable with tools like `head` and `tail`, use `headson` when yo
56
56
 
57
57
  Common flags:
58
58
 
59
- - `-c, --bytes <BYTES>`: per‑file output budget. For multiple inputs, default total budget is `<BYTES> * number_of_inputs`.
59
+ - `-c, --bytes <BYTES>`: per‑file output budget (bytes). For multiple inputs, default total budget is `<BYTES> * number_of_inputs`.
60
+ - `-u, --chars <CHARS>`: per‑file output budget (Unicode code points). Behaves like `--bytes` but counts characters instead of bytes.
60
61
  - `-C, --global-bytes <BYTES>`: total output budget across all inputs. With `--bytes`, the effective total is the smaller of the two.
61
62
  - `-f, --format <auto|json|yaml|text>`: output format (default: `auto`).
62
63
  - Auto: stdin → JSON family; filesets → per‑file based on extension (`.json` → JSON family, `.yaml`/`.yml` → YAML, unknown → Text).
@@ -83,6 +84,25 @@ Notes:
83
84
  - Directories and binary files are ignored; a notice is printed to stderr for each. Stdin reads the stream as‑is.
84
85
  - Head vs Tail sampling: these options bias which part of arrays are kept before rendering. Display styles may still insert internal gap markers to honor very small budgets; strict JSON stays unannotated.
85
86
 
87
+ ## Budget Modes
88
+
89
+ - Bytes (`-c/--bytes`, `-C/--global-bytes`)
90
+ - Measures UTF‑8 bytes in the output.
91
+ - Default per‑file budget is 500 bytes when neither `--lines` nor `--chars` is provided.
92
+ - Multiple inputs: total default budget is `<BYTES> * number_of_inputs`; `--global-bytes` caps the total.
93
+
94
+ - Characters (`-u/--chars`)
95
+ - Measures Unicode code points (not grapheme clusters).
96
+
97
+ - Lines (`-n/--lines`, `-N/--global-lines`)
98
+ - Caps the number of lines in the output.
99
+ - Incompatible with `--no-newline`.
100
+ - Multiple inputs: defaults to `<LINES> * number_of_inputs`; `--global-lines` caps the total.
101
+
102
+ - Interactions and precedence
103
+ - All active budgets are enforced simultaneously. The render must satisfy all of: bytes (if set), chars (if set), and lines (if set). The strictest cap wins.
104
+ - When only lines are specified, no implicit byte cap applies. When neither lines nor chars are specified, a 500‑byte default applies.
105
+
86
106
  Quick one‑liners:
87
107
 
88
108
  - Peek a big JSON stream (keeps structure):
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "headson"
7
- version = "0.6.5"
7
+ version = "0.6.6"
8
8
  description = "Budget‑constrained JSON preview renderer (Python bindings)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -214,7 +214,7 @@ dependencies = [
214
214
 
215
215
  [[package]]
216
216
  name = "headson"
217
- version = "0.6.5"
217
+ version = "0.6.6"
218
218
  dependencies = [
219
219
  "anyhow",
220
220
  "clap",
@@ -228,7 +228,7 @@ dependencies = [
228
228
 
229
229
  [[package]]
230
230
  name = "headson-python"
231
- version = "0.6.5"
231
+ version = "0.6.6"
232
232
  dependencies = [
233
233
  "anyhow",
234
234
  "headson",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "headson-python"
3
- version = "0.6.5"
3
+ version = "0.6.6"
4
4
  edition = "2021"
5
5
  publish = false
6
6
  readme = "README.md"
@@ -38,6 +38,7 @@ pub use serialization::types::{
38
38
  #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
39
39
  pub struct Budgets {
40
40
  pub byte_budget: Option<usize>,
41
+ pub char_budget: Option<usize>,
41
42
  pub line_budget: Option<usize>,
42
43
  }
43
44
 
@@ -54,6 +55,7 @@ pub fn headson(
54
55
  config,
55
56
  Budgets {
56
57
  byte_budget: Some(budget),
58
+ char_budget: None,
57
59
  line_budget: None,
58
60
  },
59
61
  );
@@ -73,6 +75,7 @@ pub fn headson_many(
73
75
  config,
74
76
  Budgets {
75
77
  byte_budget: Some(budget),
78
+ char_budget: None,
76
79
  line_budget: None,
77
80
  },
78
81
  );
@@ -93,6 +96,7 @@ pub fn headson_yaml(
93
96
  config,
94
97
  Budgets {
95
98
  byte_budget: Some(budget),
99
+ char_budget: None,
96
100
  line_budget: None,
97
101
  },
98
102
  );
@@ -113,6 +117,7 @@ pub fn headson_many_yaml(
113
117
  config,
114
118
  Budgets {
115
119
  byte_budget: Some(budget),
120
+ char_budget: None,
116
121
  line_budget: None,
117
122
  },
118
123
  );
@@ -133,6 +138,7 @@ pub fn headson_text(
133
138
  config,
134
139
  Budgets {
135
140
  byte_budget: Some(budget),
141
+ char_budget: None,
136
142
  line_budget: None,
137
143
  },
138
144
  );
@@ -153,6 +159,7 @@ pub fn headson_many_text(
153
159
  config,
154
160
  Budgets {
155
161
  byte_budget: Some(budget),
162
+ char_budget: None,
156
163
  line_budget: None,
157
164
  },
158
165
  );
@@ -201,10 +208,14 @@ fn find_largest_render_under_budgets(
201
208
  render_set_id = render_set_id.wrapping_add(1).max(1);
202
209
  // Measure output using a unified stats helper and enforce
203
210
  // 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);
211
+ let stats = crate::utils::measure::count_output_stats(
212
+ &s,
213
+ budgets.char_budget.is_some(),
214
+ );
215
+ let fits_bytes = budgets.byte_budget.is_none_or(|c| stats.bytes <= c);
216
+ let fits_chars = budgets.char_budget.is_none_or(|c| stats.chars <= c);
206
217
  let fits_lines = budgets.line_budget.is_none_or(|l| stats.lines <= l);
207
- if fits_chars && fits_lines {
218
+ if fits_bytes && fits_chars && fits_lines {
208
219
  best_k = Some(mid);
209
220
  true
210
221
  } else {
@@ -23,6 +23,13 @@ type IgnoreNotices = Vec<String>;
23
23
  struct Cli {
24
24
  #[arg(short = 'c', long = "bytes")]
25
25
  bytes: Option<usize>,
26
+ #[arg(
27
+ short = 'u',
28
+ long = "chars",
29
+ value_name = "CHARS",
30
+ help = "Per-file Unicode character budget (adds up across files if no global chars limit)"
31
+ )]
32
+ chars: Option<usize>,
26
33
  #[arg(
27
34
  short = 'n',
28
35
  long = "lines",
@@ -53,7 +60,8 @@ struct Cli {
53
60
  #[arg(
54
61
  long = "no-newline",
55
62
  default_value_t = false,
56
- help = "Do not add newlines in the output. With --lines/--global-lines, line counts reflect actual breaks; without newlines, any non-empty output counts as a single line."
63
+ conflicts_with_all = ["lines", "global_lines"],
64
+ help = "Do not add newlines in the output. Incompatible with --lines/--global-lines."
57
65
  )]
58
66
  no_newline: bool,
59
67
  #[arg(
@@ -170,21 +178,28 @@ fn main() -> Result<()> {
170
178
 
171
179
  // Build budgets from CLI flags. If only line caps are provided, avoid imposing
172
180
  // the default byte cap; keep the 500-byte default only when neither lines nor
173
- // bytes are specified. If any byte-related flag is present, enforce bytes.
181
+ // chars nor bytes are specified. If any byte-related flag is present, enforce bytes.
174
182
  fn make_budgets(
175
183
  cli: &Cli,
176
184
  eff_bytes: usize,
177
185
  eff_lines: Option<usize>,
186
+ eff_chars: Option<usize>,
178
187
  ) -> headson::Budgets {
179
- let byte_budget = if cli.bytes.is_some() || cli.global_bytes.is_some() {
188
+ let any_bytes = cli.bytes.is_some() || cli.global_bytes.is_some();
189
+ let any_lines = cli.lines.is_some() || cli.global_lines.is_some();
190
+ let any_chars = cli.chars.is_some();
191
+
192
+ // Apply default 500-byte only when no explicit budgets provided.
193
+ let byte_budget = if any_bytes {
180
194
  Some(eff_bytes)
181
- } else if cli.lines.is_some() || cli.global_lines.is_some() {
195
+ } else if any_lines || any_chars {
182
196
  None
183
197
  } else {
184
198
  Some(eff_bytes)
185
199
  };
186
200
  headson::Budgets {
187
201
  byte_budget,
202
+ char_budget: if any_chars { eff_chars } else { None },
188
203
  line_budget: eff_lines,
189
204
  }
190
205
  }
@@ -198,6 +213,10 @@ fn compute_effective_bytes(cli: &Cli, input_count: usize) -> usize {
198
213
  }
199
214
  }
200
215
 
216
+ fn compute_effective_chars(cli: &Cli, input_count: usize) -> Option<usize> {
217
+ cli.chars.map(|n| n.saturating_mul(input_count))
218
+ }
219
+
201
220
  fn compute_effective_lines(cli: &Cli, input_count: usize) -> Option<usize> {
202
221
  match (cli.global_lines, cli.lines) {
203
222
  (Some(g), Some(n)) => Some(g.min(n.saturating_mul(input_count))),
@@ -209,18 +228,19 @@ fn compute_effective_lines(cli: &Cli, input_count: usize) -> Option<usize> {
209
228
 
210
229
  fn compute_priority(
211
230
  cli: &Cli,
212
- effective_budget: usize,
231
+ effective_bytes: usize,
232
+ effective_chars: Option<usize>,
213
233
  input_count: usize,
214
234
  ) -> headson::PriorityConfig {
215
- let per_file_for_priority =
216
- if cli.global_bytes.is_some() && cli.bytes.is_some() {
217
- // When both limits are provided, base per-file heuristics on the per-file
218
- // budget but also respect the effective per-file slice of the final global.
219
- let eff_per_file = (effective_budget / input_count.max(1)).max(1);
220
- cli.bytes.unwrap().min(eff_per_file).max(1)
221
- } else {
222
- (effective_budget / input_count.max(1)).max(1)
223
- };
235
+ // Choose a unit for heuristics: prefer bytes if present; else chars if present; else default bytes.
236
+ let chosen_global = if cli.bytes.is_some() || cli.global_bytes.is_some() {
237
+ effective_bytes
238
+ } else if let Some(c) = effective_chars {
239
+ c
240
+ } else {
241
+ effective_bytes
242
+ };
243
+ let per_file_for_priority = (chosen_global / input_count.max(1)).max(1);
224
244
  get_priority_config(per_file_for_priority, cli)
225
245
  }
226
246
 
@@ -274,14 +294,18 @@ fn run_from_stdin(
274
294
  let input_bytes = read_stdin()?;
275
295
  let input_count = 1usize;
276
296
  let eff = compute_effective_bytes(cli, input_count);
297
+ let eff_chars = compute_effective_chars(cli, input_count);
277
298
  let eff_lines = compute_effective_lines(cli, input_count);
278
- let prio = compute_priority(cli, eff, input_count);
299
+ let prio = compute_priority(cli, eff, eff_chars, input_count);
279
300
  let mut cfg = render_cfg.clone();
280
301
  // Resolve effective output template for stdin:
281
302
  cfg.template = resolve_effective_template_for_stdin(cli.format, cfg.style);
282
- let budgets = make_budgets(cli, eff, eff_lines);
303
+ let budgets = make_budgets(cli, eff, eff_lines, eff_chars);
283
304
  // Enable free string prefix when in line-only mode
284
- if budgets.byte_budget.is_none() && budgets.line_budget.is_some() {
305
+ if budgets.byte_budget.is_none()
306
+ && budgets.char_budget.is_none()
307
+ && budgets.line_budget.is_some()
308
+ {
285
309
  cfg.string_free_prefix_graphemes = Some(40);
286
310
  }
287
311
  match cli.input_format {
@@ -315,15 +339,19 @@ fn run_from_paths(
315
339
  let included = entries.len();
316
340
  let input_count = included.max(1);
317
341
  let eff = compute_effective_bytes(cli, input_count);
342
+ let eff_chars = compute_effective_chars(cli, input_count);
318
343
  let eff_lines = compute_effective_lines(cli, input_count);
319
- let prio = compute_priority(cli, eff, input_count);
344
+ let prio = compute_priority(cli, eff, eff_chars, input_count);
320
345
  if cli.inputs.len() > 1 {
321
346
  let chosen_input = choose_input_format_fileset(cli, &entries);
322
347
  let mut cfg = render_cfg.clone();
323
348
  // For filesets: if format=auto, enable per-file template selection.
324
349
  cfg.template = effective_fileset_template(cli, cfg.style);
325
- let budgets = make_budgets(cli, eff, eff_lines);
326
- if budgets.byte_budget.is_none() && budgets.line_budget.is_some() {
350
+ let budgets = make_budgets(cli, eff, eff_lines, eff_chars);
351
+ if budgets.byte_budget.is_none()
352
+ && budgets.char_budget.is_none()
353
+ && budgets.line_budget.is_some()
354
+ {
327
355
  cfg.string_free_prefix_graphemes = Some(40);
328
356
  }
329
357
  let out = match chosen_input {
@@ -361,8 +389,11 @@ fn run_from_paths(
361
389
  cfg.template = resolve_effective_template_for_single(
362
390
  cli.format, cfg.style, &lower,
363
391
  );
364
- let budgets = make_budgets(cli, eff, eff_lines);
365
- if budgets.byte_budget.is_none() && budgets.line_budget.is_some() {
392
+ let budgets = make_budgets(cli, eff, eff_lines, eff_chars);
393
+ if budgets.byte_budget.is_none()
394
+ && budgets.char_budget.is_none()
395
+ && budgets.line_budget.is_some()
396
+ {
366
397
  cfg.string_free_prefix_graphemes = Some(40);
367
398
  }
368
399
  let out = match chosen_input {
@@ -496,7 +527,7 @@ fn get_priority_config(
496
527
  per_file_budget: usize,
497
528
  cli: &Cli,
498
529
  ) -> headson::PriorityConfig {
499
- // Detect line-only mode: lines flag present and no explicit bytes flags.
530
+ // Detect line-only mode: lines flag present and no explicit bytes/chars flags.
500
531
  let line_only = (cli.lines.is_some() || cli.global_lines.is_some())
501
532
  && cli.bytes.is_none()
502
533
  && cli.global_bytes.is_none();
@@ -14,7 +14,7 @@ struct Entry {
14
14
  priority_index: usize,
15
15
  depth: usize,
16
16
  // When present, we can read kind from the arena (parsed JSON) node.
17
- // When None, this is a synthetic entry (currently only string grapheme).
17
+ // When None, this is a synthetic entry (used for string grapheme entries).
18
18
  arena_index: Option<usize>,
19
19
  }
20
20
  impl PartialEq for Entry {
@@ -1,21 +1,15 @@
1
1
  #[derive(Copy, Clone, Debug, Eq, PartialEq)]
2
2
  pub(crate) struct OutputStats {
3
3
  pub bytes: usize,
4
+ pub chars: usize,
4
5
  pub lines: usize,
5
6
  }
6
7
 
7
- /// Count bytes and logical lines in a string, normalizing CRLF/CR/LF.
8
- ///
9
- /// Rules:
10
- /// - An empty string has 0 lines.
11
- /// - Otherwise, lines = number of line break sequences + 1.
12
- /// - A CRLF pair counts as a single line break.
13
- pub(crate) fn count_output_stats(s: &str) -> OutputStats {
14
- let bytes = s.len();
15
- if bytes == 0 {
16
- return OutputStats { bytes, lines: 0 };
8
+ #[inline]
9
+ fn count_lines_from_bytes(b: &[u8]) -> usize {
10
+ if b.is_empty() {
11
+ return 0;
17
12
  }
18
- let b = s.as_bytes();
19
13
  let mut i = 0usize;
20
14
  let mut breaks = 0usize;
21
15
  while i < b.len() {
@@ -35,8 +29,22 @@ pub(crate) fn count_output_stats(s: &str) -> OutputStats {
35
29
  _ => i += 1,
36
30
  }
37
31
  }
32
+ breaks + 1
33
+ }
34
+
35
+ /// Count bytes and logical lines in a string, normalizing CRLF/CR/LF.
36
+ ///
37
+ /// Rules:
38
+ /// - An empty string has 0 lines.
39
+ /// - Otherwise, lines = number of line break sequences + 1.
40
+ /// - A CRLF pair counts as a single line break.
41
+ pub(crate) fn count_output_stats(s: &str, want_chars: bool) -> OutputStats {
42
+ let bytes = s.len();
43
+ let chars = if want_chars { s.chars().count() } else { 0 };
44
+ let lines = count_lines_from_bytes(s.as_bytes());
38
45
  OutputStats {
39
46
  bytes,
40
- lines: breaks + 1,
47
+ chars,
48
+ lines,
41
49
  }
42
50
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes