1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use anyhow::Result;
use crate::hints::{HintContext, generate_hints};
use crate::output::{
CommandOutcome, Format, apply_jq_filter_result, build_envelope_value, format_envelope,
};
/// Error message for `--count` on non-list commands (shared across match arms).
pub(crate) const COUNT_UNSUPPORTED_ERROR: &str = "Error: --count is only supported for list commands (find, tags summary, properties summary, backlinks)";
/// Encapsulates the post-command output pipeline: jq filtering, hint generation,
/// and envelope wrapping.
pub(crate) struct OutputPipeline<'a> {
/// Format the user requested.
pub user_format: Format,
/// Optional jq filter expression (operates on the full envelope).
pub jq_filter: Option<&'a str>,
/// Optional hint context for drill-down commands.
pub hint_ctx: Option<&'a HintContext>,
/// Print only the total count as a bare integer.
pub count: bool,
}
impl OutputPipeline<'_> {
/// Process a command result through the output pipeline.
/// Prints output to stdout/stderr and returns the exit code.
pub fn finalize(&self, result: Result<CommandOutcome>) -> i32 {
match result {
Ok(CommandOutcome::Success { output, total }) => {
// --count: print bare total and exit early.
if self.count {
if let Some(n) = total {
println!("{n}");
return 0;
}
eprintln!("{COUNT_UNSUPPORTED_ERROR}");
return 2;
}
// Commands always produce JSON internally.
let value: serde_json::Value = match serde_json::from_str(&output) {
Ok(v) => v,
Err(e) => {
let msg = crate::output::format_error(
self.user_format,
"internal error: failed to parse command JSON output",
None,
None,
Some(&e.to_string()),
);
eprintln!("{msg}");
return 2;
}
};
// Generate hints when a context is available.
let hints = if let Some(ctx) = self.hint_ctx {
generate_hints(ctx, &value)
} else {
Vec::new()
};
if let Some(filter) = self.jq_filter {
// Build the full envelope first so jq can address any field.
let envelope = build_envelope_value(&value, total, &hints);
match apply_jq_filter_result(filter, &envelope) {
Ok(filtered) => println!("{filtered}"),
Err(e) => {
let msg = crate::output::format_error(
self.user_format,
"jq filter failed",
None,
None,
Some(&e),
);
eprintln!("{msg}");
return 1;
}
}
} else {
let formatted = format_envelope(self.user_format, &value, total, &hints);
println!("{formatted}");
// In text mode, when a list command returns zero results, emit a
// notice on stderr so the user knows the command ran successfully.
// Only fires for list commands (total is Some) with empty arrays.
if self.user_format == Format::Text
&& total == Some(0)
&& value.as_array().is_some_and(Vec::is_empty)
{
eprintln!("No files matched");
}
}
0
}
Ok(CommandOutcome::RawOutput(output)) => {
if self.count {
eprintln!("{COUNT_UNSUPPORTED_ERROR}");
return 2;
}
// Raw output bypasses the JSON pipeline — print directly to stdout.
// Used by the `read` command for text-format content output.
// println! matches pre-refactor behavior: the content string already ends
// with '\n', and the extra newline from println! preserves empty-line
// endings (e.g. `--lines :2` where line 2 is blank).
println!("{output}");
0
}
Ok(CommandOutcome::UserError(output)) => {
// UserError strings are always formatted as JSON internally (effective_format=Json).
// When the user requested text format, re-format the error as human-readable text.
let displayed = if self.user_format == Format::Text {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&output) {
let error = v["error"].as_str().unwrap_or("unknown error");
let path = v["path"].as_str();
let hint = v["hint"].as_str();
let cause = v["cause"].as_str();
crate::output::format_error(Format::Text, error, path, hint, cause)
} else {
output
}
} else {
output
};
eprintln!("{displayed}");
1
}
Err(e) => {
let msg = crate::output::format_error(
self.user_format,
&e.to_string(),
None,
None,
e.chain()
.nth(1)
.map(std::string::ToString::to_string)
.as_deref(),
);
eprintln!("{msg}");
2
}
}
}
}