headson 0.5.4__tar.gz → 0.6.0__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.5.4 → headson-0.6.0}/Cargo.lock +1 -1
  2. {headson-0.5.4 → headson-0.6.0}/Cargo.toml +1 -1
  3. {headson-0.5.4 → headson-0.6.0}/PKG-INFO +1 -1
  4. headson-0.6.0/docs/assets/tapes/demo.gif +0 -0
  5. {headson-0.5.4 → headson-0.6.0}/pyproject.toml +1 -1
  6. {headson-0.5.4 → headson-0.6.0}/python/Cargo.lock +2 -2
  7. {headson-0.5.4 → headson-0.6.0}/python/Cargo.toml +1 -1
  8. {headson-0.5.4 → headson-0.6.0}/python/src/lib.rs +2 -2
  9. {headson-0.5.4 → headson-0.6.0}/src/json_ingest/samplers/default.rs +0 -2
  10. {headson-0.5.4 → headson-0.6.0}/src/lib.rs +18 -6
  11. {headson-0.5.4 → headson-0.6.0}/src/main.rs +41 -6
  12. headson-0.6.0/src/serialization/color.rs +113 -0
  13. {headson-0.5.4 → headson-0.6.0}/src/serialization/fileset.rs +2 -2
  14. {headson-0.5.4 → headson-0.6.0}/src/serialization/mod.rs +248 -41
  15. headson-0.6.0/src/serialization/output.rs +98 -0
  16. headson-0.6.0/src/serialization/snapshots/headson__serialization__tests__array_omitted_js_head.snap +9 -0
  17. headson-0.6.0/src/serialization/snapshots/headson__serialization__tests__array_omitted_js_tail.snap +9 -0
  18. headson-0.6.0/src/serialization/snapshots/headson__serialization__tests__array_omitted_pseudo_head.snap +9 -0
  19. headson-0.6.0/src/serialization/snapshots/headson__serialization__tests__array_omitted_pseudo_tail.snap +9 -0
  20. headson-0.6.0/src/serialization/snapshots/headson__serialization__tests__inline_open_array_in_object_json.snap +10 -0
  21. headson-0.6.0/src/serialization/templates/core.rs +124 -0
  22. headson-0.6.0/src/serialization/templates/js.rs +75 -0
  23. headson-0.6.0/src/serialization/templates/json.rs +29 -0
  24. headson-0.6.0/src/serialization/templates/mod.rs +53 -0
  25. headson-0.6.0/src/serialization/templates/pseudo.rs +69 -0
  26. headson-0.6.0/src/serialization/types.rs +41 -0
  27. headson-0.6.0/src/utils/json.rs +11 -0
  28. headson-0.5.4/docs/assets/tapes/demo.gif +0 -0
  29. headson-0.5.4/src/serialization/templates/core.rs +0 -92
  30. headson-0.5.4/src/serialization/templates/js.rs +0 -83
  31. headson-0.5.4/src/serialization/templates/json.rs +0 -22
  32. headson-0.5.4/src/serialization/templates/mod.rs +0 -45
  33. headson-0.5.4/src/serialization/templates/pseudo.rs +0 -60
  34. headson-0.5.4/src/serialization/types.rs +0 -18
  35. headson-0.5.4/src/utils/json.rs +0 -6
  36. {headson-0.5.4 → headson-0.6.0}/JSONTestSuite/LICENSE +0 -0
  37. {headson-0.5.4 → headson-0.6.0}/JSONTestSuite/README.md +0 -0
  38. {headson-0.5.4 → headson-0.6.0}/LICENSE +0 -0
  39. {headson-0.5.4 → headson-0.6.0}/README.md +0 -0
  40. {headson-0.5.4 → headson-0.6.0}/docs/assets/algorithm.svg +0 -0
  41. {headson-0.5.4 → headson-0.6.0}/docs/assets/logo.png +0 -0
  42. {headson-0.5.4 → headson-0.6.0}/docs/assets/logo.svg +0 -0
  43. {headson-0.5.4 → headson-0.6.0}/python/README.md +0 -0
  44. {headson-0.5.4 → headson-0.6.0}/python/headson/__init__.py +0 -0
  45. {headson-0.5.4 → headson-0.6.0}/src/json_ingest/builder.rs +0 -0
  46. {headson-0.5.4 → headson-0.6.0}/src/json_ingest/mod.rs +0 -0
  47. {headson-0.5.4 → headson-0.6.0}/src/json_ingest/samplers/head.rs +0 -0
  48. {headson-0.5.4 → headson-0.6.0}/src/json_ingest/samplers/mod.rs +0 -0
  49. {headson-0.5.4 → headson-0.6.0}/src/json_ingest/samplers/tail.rs +0 -0
  50. {headson-0.5.4 → headson-0.6.0}/src/order/build.rs +0 -0
  51. {headson-0.5.4 → headson-0.6.0}/src/order/mod.rs +0 -0
  52. {headson-0.5.4 → headson-0.6.0}/src/order/scoring.rs +0 -0
  53. {headson-0.5.4 → headson-0.6.0}/src/order/snapshots/headson__order__build__tests__order_empty_array_order.snap +0 -0
  54. {headson-0.5.4 → headson-0.6.0}/src/order/snapshots/headson__order__build__tests__order_single_string_array_order.snap +0 -0
  55. {headson-0.5.4 → headson-0.6.0}/src/order/types.rs +0 -0
  56. {headson-0.5.4 → headson-0.6.0}/src/serialization/snapshots/headson__serialization__tests__arena_render_empty.snap +0 -0
  57. {headson-0.5.4 → headson-0.6.0}/src/serialization/snapshots/headson__serialization__tests__arena_render_single.snap +0 -0
  58. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__order__tests__order_empty_array_order.snap +0 -0
  59. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__order__tests__order_single_string_array_order.snap +0 -0
  60. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__order__tests__pq_empty_array_queue.snap +0 -0
  61. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__order__tests__pq_single_string_array_queue.snap +0 -0
  62. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__queue__tests__pq_empty_array_queue.snap +0 -0
  63. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__queue__tests__pq_single_string_array_queue.snap +0 -0
  64. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__tree__tests__build_tree_empty.snap +0 -0
  65. {headson-0.5.4 → headson-0.6.0}/src/snapshots/headson__tree__tests__build_tree_single.snap +0 -0
  66. {headson-0.5.4 → headson-0.6.0}/src/utils/graph.rs +0 -0
  67. {headson-0.5.4 → headson-0.6.0}/src/utils/mod.rs +0 -0
  68. {headson-0.5.4 → headson-0.6.0}/src/utils/search.rs +0 -0
  69. {headson-0.5.4 → headson-0.6.0}/src/utils/text.rs +0 -0
  70. {headson-0.5.4 → headson-0.6.0}/src/utils/tree_arena.rs +0 -0
@@ -266,7 +266,7 @@ dependencies = [
266
266
 
267
267
  [[package]]
268
268
  name = "headson"
269
- version = "0.5.4"
269
+ version = "0.6.0"
270
270
  dependencies = [
271
271
  "anyhow",
272
272
  "assert_cmd",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "headson"
3
- version = "0.5.4"
3
+ version = "0.6.0"
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.5.4
3
+ Version: 0.6.0
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Rust
Binary file
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "headson"
7
- version = "0.5.4"
7
+ version = "0.6.0"
8
8
  description = "Budget‑constrained JSON preview renderer (Python bindings)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -169,7 +169,7 @@ dependencies = [
169
169
 
170
170
  [[package]]
171
171
  name = "headson"
172
- version = "0.5.4"
172
+ version = "0.6.0"
173
173
  dependencies = [
174
174
  "anyhow",
175
175
  "clap",
@@ -182,7 +182,7 @@ dependencies = [
182
182
 
183
183
  [[package]]
184
184
  name = "headson-python"
185
- version = "0.5.4"
185
+ version = "0.6.0"
186
186
  dependencies = [
187
187
  "anyhow",
188
188
  "headson",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "headson-python"
3
- version = "0.5.4"
3
+ version = "0.6.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
  readme = "README.md"
@@ -2,7 +2,7 @@ use anyhow::{bail, Result};
2
2
  use pyo3::exceptions::PyRuntimeError;
3
3
  use pyo3::prelude::*;
4
4
  use pyo3::types::PyModule;
5
- use headson_core::{ArraySamplerStrategy, OutputTemplate, PriorityConfig, RenderConfig};
5
+ use headson_core::{ArraySamplerStrategy, ColorMode, OutputTemplate, PriorityConfig, RenderConfig};
6
6
 
7
7
  fn to_template(s: &str) -> Result<OutputTemplate> {
8
8
  match s.to_ascii_lowercase().as_str() {
@@ -22,7 +22,7 @@ fn render_config_with_sampler(
22
22
  let newline = "\n".to_string();
23
23
  let indent_unit = " ".to_string();
24
24
  let prefer_tail_arrays = matches!(sampler, ArraySamplerStrategy::Tail);
25
- Ok(RenderConfig { template: t, indent_unit, space, newline, prefer_tail_arrays })
25
+ Ok(RenderConfig { template: t, indent_unit, space, newline, prefer_tail_arrays, color_mode: ColorMode::Auto, color_enabled: false })
26
26
  }
27
27
 
28
28
  fn parse_skew(skew: &str) -> Result<ArraySamplerStrategy> {
@@ -21,7 +21,6 @@ struct SampleAccumulator<'a> {
21
21
  indices: &'a mut Vec<usize>,
22
22
  }
23
23
 
24
- #[inline]
25
24
  fn mix64(mut x: u64) -> u64 {
26
25
  x ^= x >> 30;
27
26
  x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
@@ -30,7 +29,6 @@ fn mix64(mut x: u64) -> u64 {
30
29
  x ^ (x >> 31)
31
30
  }
32
31
 
33
- #[inline]
34
32
  fn accept_index(i: u64) -> bool {
35
33
  let h = mix64(i ^ RANDOM_ACCEPT_SEED);
36
34
  ((h >> 32) as u32) < RANDOM_ACCEPT_THRESHOLD
@@ -25,7 +25,8 @@ pub use order::{
25
25
  NodeId, NodeKind, PriorityConfig, PriorityOrder, RankedNode, build_order,
26
26
  };
27
27
 
28
- pub use serialization::types::{OutputTemplate, RenderConfig};
28
+ pub use serialization::color::resolve_color_enabled;
29
+ pub use serialization::types::{ColorMode, OutputTemplate, RenderConfig};
29
30
 
30
31
  pub fn headson(
31
32
  input: Vec<u8>,
@@ -76,7 +77,11 @@ fn find_largest_render_under_budget(
76
77
  let mut inclusion_flags: Vec<u32> = vec![0; total];
77
78
  // Each render attempt bumps this non-zero identifier to create a fresh inclusion set.
78
79
  let mut render_set_id: u32 = 1;
79
- let mut best_str: Option<String> = None;
80
+ // Measure length without color so ANSI escapes do not count toward the
81
+ // character budget. Then render once more with the requested color setting.
82
+ let mut best_k: Option<usize> = None;
83
+ let mut measure_cfg = config.clone();
84
+ measure_cfg.color_enabled = false;
80
85
 
81
86
  let _ = crate::utils::search::binary_search_max(lo, hi, |mid| {
82
87
  let s = crate::serialization::render_top_k(
@@ -84,19 +89,26 @@ fn find_largest_render_under_budget(
84
89
  mid,
85
90
  &mut inclusion_flags,
86
91
  render_set_id,
87
- config,
92
+ &measure_cfg,
88
93
  );
89
94
  render_set_id = render_set_id.wrapping_add(1).max(1);
90
95
  if s.len() <= char_budget {
91
- best_str = Some(s);
96
+ best_k = Some(mid);
92
97
  true
93
98
  } else {
94
99
  false
95
100
  }
96
101
  });
97
102
 
98
- if let Some(s) = best_str {
99
- s
103
+ if let Some(k) = best_k {
104
+ // Final render with original color settings
105
+ crate::serialization::render_top_k(
106
+ order_build,
107
+ k,
108
+ &mut inclusion_flags,
109
+ render_set_id,
110
+ config,
111
+ )
100
112
  } else {
101
113
  // Fallback: always render a single node (k=1) to produce the
102
114
  // shortest possible preview, even if it exceeds the byte budget.
@@ -1,9 +1,10 @@
1
1
  use std::fs::File;
2
+ use std::io::IsTerminal as _;
2
3
  use std::io::{self, Read};
3
4
  use std::path::{Path, PathBuf};
4
5
 
5
6
  use anyhow::{Context, Result};
6
- use clap::{Parser, ValueEnum};
7
+ use clap::{ArgAction, Parser, ValueEnum};
7
8
  use content_inspector::{ContentType, inspect};
8
9
 
9
10
  type InputEntry = (String, Vec<u8>);
@@ -65,6 +66,20 @@ struct Cli {
65
66
  help = "Prefer the beginning of arrays when truncating (keep first N)."
66
67
  )]
67
68
  head: bool,
69
+ #[arg(
70
+ long = "color",
71
+ action = ArgAction::SetTrue,
72
+ conflicts_with = "no_color",
73
+ help = "Force enable ANSI colors in output"
74
+ )]
75
+ color: bool,
76
+ #[arg(
77
+ long = "no-color",
78
+ action = ArgAction::SetTrue,
79
+ conflicts_with = "color",
80
+ help = "Disable ANSI colors in output"
81
+ )]
82
+ no_color: bool,
68
83
  #[arg(
69
84
  value_name = "INPUT",
70
85
  value_hint = clap::ValueHint::FilePath,
@@ -85,6 +100,9 @@ fn main() -> Result<()> {
85
100
  let cli = Cli::parse();
86
101
 
87
102
  let render_cfg = get_render_config_from(&cli);
103
+ // Resolve color auto-detection now (stdout is the surface for user output).
104
+ let _color_enabled =
105
+ render_cfg.color_mode.effective(io::stdout().is_terminal());
88
106
  let (output, ignore_notices) = if cli.inputs.is_empty() {
89
107
  (run_from_stdin(&cli, &render_cfg)?, Vec::new())
90
108
  } else {
@@ -222,11 +240,24 @@ fn ingest_paths(paths: &[PathBuf]) -> Result<(InputEntries, IgnoreNotices)> {
222
240
  }
223
241
 
224
242
  fn get_render_config_from(cli: &Cli) -> headson::RenderConfig {
225
- let template = match cli.template {
226
- Template::Json => headson::OutputTemplate::Json,
227
- Template::Pseudo => headson::OutputTemplate::Pseudo,
228
- Template::Js => headson::OutputTemplate::Js,
229
- };
243
+ fn to_output_template(t: Template) -> headson::OutputTemplate {
244
+ match t {
245
+ Template::Json => headson::OutputTemplate::Json,
246
+ Template::Pseudo => headson::OutputTemplate::Pseudo,
247
+ Template::Js => headson::OutputTemplate::Js,
248
+ }
249
+ }
250
+ fn color_mode_from_flags(cli: &Cli) -> headson::ColorMode {
251
+ if cli.color {
252
+ headson::ColorMode::On
253
+ } else if cli.no_color {
254
+ headson::ColorMode::Off
255
+ } else {
256
+ headson::ColorMode::Auto
257
+ }
258
+ }
259
+
260
+ let template = to_output_template(cli.template);
230
261
  let space = if cli.compact || cli.no_space { "" } else { " " }.to_string();
231
262
  let newline = if cli.compact || cli.no_newline {
232
263
  ""
@@ -239,6 +270,8 @@ fn get_render_config_from(cli: &Cli) -> headson::RenderConfig {
239
270
  } else {
240
271
  cli.indent.clone()
241
272
  };
273
+ let color_mode = color_mode_from_flags(cli);
274
+ let color_enabled = headson::resolve_color_enabled(color_mode);
242
275
 
243
276
  headson::RenderConfig {
244
277
  template,
@@ -246,6 +279,8 @@ fn get_render_config_from(cli: &Cli) -> headson::RenderConfig {
246
279
  space,
247
280
  newline,
248
281
  prefer_tail_arrays: cli.tail,
282
+ color_mode,
283
+ color_enabled,
249
284
  }
250
285
  }
251
286
 
@@ -0,0 +1,113 @@
1
+ use std::io::IsTerminal as _;
2
+
3
+ use crate::serialization::types::ColorMode;
4
+
5
+ // ANSI SGR fragments
6
+ const RESET: &str = "\u{001b}[0m";
7
+ const BOLD_BLUE: &str = "\u{001b}[1;34m";
8
+ const GREEN: &str = "\u{001b}[32m";
9
+ const CYAN: &str = "\u{001b}[36m";
10
+ const BRIGHT_GRAY: &str = "\u{001b}[37m";
11
+ const DARK_GRAY: &str = "\u{001b}[90m";
12
+
13
+ #[derive(Copy, Clone, Debug, Eq, PartialEq)]
14
+ pub enum ColorRole {
15
+ Key,
16
+ String,
17
+ Number,
18
+ Bool,
19
+ Null,
20
+ }
21
+
22
+ pub fn wrap_role<S: Into<String>>(
23
+ s: S,
24
+ role: ColorRole,
25
+ enabled: bool,
26
+ ) -> String {
27
+ let s: String = s.into();
28
+ if !enabled {
29
+ return s;
30
+ }
31
+ let prefix = match role {
32
+ ColorRole::Key => BOLD_BLUE,
33
+ ColorRole::String => GREEN,
34
+ ColorRole::Number => CYAN,
35
+ ColorRole::Bool | ColorRole::Null => BRIGHT_GRAY,
36
+ };
37
+ let mut out = String::with_capacity(8 + 8 + s.len());
38
+ out.push_str(prefix);
39
+ out.push_str(&s);
40
+ out.push_str(RESET);
41
+ out
42
+ }
43
+
44
+ pub fn omission_marker(enabled: bool) -> &'static str {
45
+ if enabled {
46
+ "\u{001b}[90m…\u{001b}[0m"
47
+ } else {
48
+ "…"
49
+ }
50
+ }
51
+
52
+ pub fn color_comment<S: Into<String>>(body: S, enabled: bool) -> String {
53
+ if !enabled {
54
+ return body.into();
55
+ }
56
+ let b = body.into();
57
+ let mut out = String::with_capacity(b.len() + 8 + 4);
58
+ out.push_str(DARK_GRAY);
59
+ out.push_str(&b);
60
+ out.push_str(RESET);
61
+ out
62
+ }
63
+
64
+ fn env_bool(var: &str) -> Option<bool> {
65
+ std::env::var_os(var).map(|v| {
66
+ let s = v.to_string_lossy();
67
+ !(s == "0" || s.is_empty())
68
+ })
69
+ }
70
+
71
+ struct ColorEnv {
72
+ force: bool, // CLICOLOR_FORCE=1
73
+ force_color: bool, // FORCE_COLOR=1
74
+ no_color: bool, // NO_COLOR present
75
+ dumb: bool, // TERM=dumb
76
+ clicolor: Option<bool>, // CLICOLOR=0/1
77
+ is_tty: bool,
78
+ }
79
+
80
+ fn read_color_env() -> ColorEnv {
81
+ let term_dumb = std::env::var_os("TERM")
82
+ .map(|t| t.to_string_lossy() == "dumb")
83
+ .unwrap_or(false);
84
+ ColorEnv {
85
+ force: matches!(env_bool("CLICOLOR_FORCE"), Some(true)),
86
+ force_color: matches!(env_bool("FORCE_COLOR"), Some(true)),
87
+ no_color: std::env::var_os("NO_COLOR").is_some(),
88
+ dumb: term_dumb,
89
+ clicolor: env_bool("CLICOLOR"),
90
+ is_tty: std::io::stdout().is_terminal(),
91
+ }
92
+ }
93
+
94
+ fn auto_color_enabled(env: &ColorEnv) -> bool {
95
+ if env.force || env.force_color {
96
+ return true;
97
+ }
98
+ if env.no_color || env.dumb {
99
+ return false;
100
+ }
101
+ if let Some(b) = env.clicolor {
102
+ return b && env.is_tty;
103
+ }
104
+ env.is_tty
105
+ }
106
+
107
+ pub fn resolve_color_enabled(mode: ColorMode) -> bool {
108
+ match mode {
109
+ ColorMode::On => true,
110
+ ColorMode::Off => false,
111
+ ColorMode::Auto => auto_color_enabled(&read_color_env()),
112
+ }
113
+ }
@@ -17,7 +17,7 @@ impl<'a> RenderScope<'a> {
17
17
  out.push_str("// ");
18
18
  out.push_str(raw_key);
19
19
  out.push_str(nl);
20
- let rendered = self.serialize_node(child_pq_id, depth, false);
20
+ let rendered = self.render_node_to_string(child_pq_id, depth, false);
21
21
  out.push_str(&rendered);
22
22
  out.push(';');
23
23
  out.push_str(nl);
@@ -58,7 +58,7 @@ impl<'a> RenderScope<'a> {
58
58
  out.push_str(raw_key);
59
59
  out.push_str(" <==");
60
60
  out.push_str(nl);
61
- let rendered = self.serialize_node(child_pq_id, depth, false);
61
+ let rendered = self.render_node_to_string(child_pq_id, depth, false);
62
62
  out.push_str(&rendered);
63
63
  }
64
64