1use crate::{cli::Format, error::Error};
2use serde_json::Value;
3use std::io::{self, IsTerminal, Write};
4
5pub fn structured(format: Format) -> bool {
6 format == Format::Json || (format == Format::Auto && !io::stdout().is_terminal())
7}
8pub fn format_from_args(args: &[String]) -> Format {
10 let mut format = Format::Auto;
11 let mut index = 1;
12 while index < args.len() {
13 let arg = &args[index];
14 if arg == "--" {
15 break;
16 }
17 let value = if arg == "--output" || arg == "-o" {
18 index += 1;
19 args.get(index).map(String::as_str)
20 } else {
21 arg.strip_prefix("--output=")
22 .or_else(|| arg.strip_prefix("-o="))
23 .or_else(|| arg.strip_prefix("-o").filter(|s| !s.is_empty()))
24 };
25 if let Some(value) = value {
26 format = match value {
27 "json" => Format::Json,
28 "text" => Format::Text,
29 "auto" => Format::Auto,
30 _ => format,
31 };
32 }
33 index += 1;
34 }
35 format
36}
37pub fn clean(text: &str) -> String {
38 text.chars()
39 .filter(|c| {
40 (!c.is_control() || *c == '\n' || *c == '\t')
41 && !matches!(*c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
42 })
43 .collect()
44}
45pub fn render(value: &Value, color: bool) -> String {
46 let title = |s: &str| {
47 if color {
48 format!("\x1b[1;35m{}\x1b[0m", clean(s))
49 } else {
50 clean(s)
51 }
52 };
53 match value["view"].as_str() {
54 Some("config_path") => return format!("{}\n", clean(value["path"].as_str().unwrap_or(""))),
55 Some("profiles") => {
56 let mut text = format!("{}\n", title("OneNote · profiles"));
57 let profiles = value["profiles"].as_array().cloned().unwrap_or_default();
58 if profiles.is_empty() {
59 text.push_str("\n No saved profiles. Start with onenote init.\n");
60 }
61 for p in profiles {
62 text.push_str(&format!(
63 "\n {} {}{}\n {} · {}\n",
64 if p["active"] == true { "●" } else { "○" },
65 title(p["name"].as_str().unwrap_or("")),
66 if p["selected"] == true && p["active"] != true {
67 " (override)"
68 } else {
69 ""
70 },
71 clean(p["target"].as_str().unwrap_or("")),
72 if p["read_only"] == true {
73 "read-only"
74 } else {
75 "read and write"
76 }
77 ));
78 }
79 text.push_str("\n ● saved default · Select with onenote profile use NAME\n");
80 return text;
81 }
82 Some("init" | "config") => {
83 let name = value["profile"].as_str().unwrap_or("local (implicit)");
84 let connection = &value["connection"];
85 let mut text = format!(
86 "{}\n\n Connection {}\n Access {}\n Config {}\n",
87 title(&format!("OneNote · {name}")),
88 clean(value["target"].as_str().unwrap_or("")),
89 if connection["read_only"] == true {
90 "read-only"
91 } else {
92 "read and write"
93 },
94 clean(value["path"].as_str().unwrap_or(""))
95 );
96 if let Some(key) = connection["identity_file"].as_str() {
97 text.push_str(&format!(" SSH key {}\n", clean(key)));
98 }
99 if let Some(port) = connection["port"].as_u64() {
100 text.push_str(&format!(" SSH port {port}\n"));
101 }
102 if value["view"] == "init" {
103 text.push_str(&format!(
104 "\n {}\n\n Next: onenote {}\n",
105 clean(value["message"].as_str().unwrap_or("")),
106 if value["verified"] == true {
107 "notebooks list"
108 } else {
109 "doctor"
110 }
111 ));
112 } else {
113 text.push_str(&format!(
114 " Selection {}\n",
115 match value["source"].as_str() {
116 Some("active_profile") => "saved default",
117 Some("override") => "--profile or ONENOTE_PROFILE",
118 _ => "local desktop fallback",
119 }
120 ));
121 }
122 return text;
123 }
124 Some("profile_change") => {
125 let mut text = format!("{}\n", clean(value["message"].as_str().unwrap_or("")));
126 if let Some(name) = value["override"].as_str() {
127 text.push_str(&format!("Profile override '{name}' still takes precedence; clear ONENOTE_PROFILE or --profile to use the saved default.\n"));
128 }
129 return text;
130 }
131 _ => (),
132 }
133 if value["dry_run"] == true {
134 return format!(
135 "{}\nProfile: {}\nConnection: {}{}\nTarget: {}\n{}\n{}\n\nNo desktop connection or write was made.\n",
136 title(&format!(
137 "Preview · pages {}",
138 value["operation"].as_str().unwrap_or("write")
139 )),
140 clean(value["profile"].as_str().unwrap_or("local (implicit)")),
141 clean(
142 value["connection"]["host"]
143 .as_str()
144 .unwrap_or("local Windows desktop")
145 ),
146 if value["connection"]["read_only"] == true {
147 " · read-only"
148 } else {
149 ""
150 },
151 clean(value["target"].as_str().unwrap_or("")),
152 value["title"]
153 .as_str()
154 .map(|s| format!("Title: {}\n", clean(s)))
155 .unwrap_or_default(),
156 clean(value["text"].as_str().unwrap_or(""))
157 );
158 }
159 if let Some(items) = value["items"].as_array() {
160 let mut text = format!(
161 "{}\n",
162 title(&format!("OneNote · {} of {}", items.len(), value["total"]))
163 );
164 if items.is_empty() {
165 text.push_str("\n No matching items.\n");
166 }
167 for item in items {
168 text.push_str(&format!(
169 "\n {}\n",
170 title(item["name"].as_str().unwrap_or("Item"))
171 ));
172 for field in ["path", "id", "last_modified", "locked", "kind", "parent_id"] {
173 if let Some(v) = item.get(field).filter(|v| !v.is_null()) {
174 let v = v
175 .as_str()
176 .map(str::to_owned)
177 .unwrap_or_else(|| v.to_string());
178 text.push_str(&format!(" {field}: {}\n", clean(&v)));
179 }
180 }
181 }
182 if !value["next_offset"].is_null() {
183 text.push_str(&format!(
184 "\n Continue with the same command and --offset {}\n",
185 value["next_offset"]
186 ));
187 }
188 if value["indexing_pending"] == true {
189 text.push_str(&format!("\n {} unindexed pages omitted. Search results may be incomplete until Windows Search finishes indexing.\n", value["unindexed_count"]));
190 }
191 if value["search_mode"] == "scan" {
192 text.push_str(&format!(
193 "\n Text scan · {} pages read; {} locked sections and {} pages skipped.\n",
194 value["scanned_pages"],
195 value["skipped_locked_sections"],
196 value["skipped_pages"].as_array().map_or(0, Vec::len)
197 ));
198 if value["incomplete"] == true {
199 text.push_str(" Results are incomplete for this scope.\n");
200 }
201 if !value["next_scan_offset"].is_null() {
202 text.push_str(&format!(" After paging through these matches, continue with --scan-offset {} and --offset 0.\n", value["next_scan_offset"]));
203 }
204 }
205 return text;
206 }
207 if let Some(markdown) = value["markdown"].as_str() {
208 let mut text = format!(
209 "{}\nid: {}\n\n{}\n",
210 title(value["name"].as_str().unwrap_or("Untitled")),
211 clean(value["id"].as_str().unwrap_or("")),
212 clean(markdown)
213 );
214 if value["verified"] == true {
215 text.push_str("\nWrite verified in OneNote desktop.\n");
216 }
217 if value["has_images"] == true || value["has_attachments"] == true {
218 text.push_str("\nThis page also contains images or attachments; these are not included in the text.\n");
219 }
220 if let Some(xml) = value["xml"].as_str() {
221 text.push_str(&format!("\n{}\n{}\n", title("Page XML"), clean(xml)));
222 }
223 return text;
224 }
225 if value.get("verified").is_some() {
226 return format!(
227 "{}\n{}\n",
228 title(if value["available"] == true {
229 "OneNote desktop is ready"
230 } else {
231 "OneNote desktop check"
232 }),
233 clean(value["message"].as_str().unwrap_or(""))
234 );
235 }
236 clean(&serde_json::to_string_pretty(value).unwrap_or_default()) + "\n"
237}
238pub fn print(value: &Value, format: Format, no_color: bool) -> Result<(), Error> {
239 let text = if structured(format) {
240 serde_json::to_string_pretty(value).map_err(|e| Error::desktop(e.to_string()))? + "\n"
241 } else {
242 render(
243 value,
244 !no_color && std::env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(),
245 )
246 };
247 match io::stdout().lock().write_all(text.as_bytes()) {
248 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
249 result => result.map_err(Error::from),
250 }
251}
252pub fn error(error: &Error, format: Format) {
253 let text = if structured(format) {
254 let mut value = serde_json::json!({"error":{"kind":error.kind,"message":error.message,"exit_code":error.code(),"retryable":false}});
255 if let Some(id) = &error.page_id {
256 value["error"]["page_id"] = Value::String(id.clone());
257 }
258 value.to_string()
259 } else {
260 format!("error: {}", clean(&error.message))
261 };
262 let _ = writeln!(io::stderr().lock(), "{text}");
263}