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 if value["dry_run"] == true {
54 return format!(
55 "{}\nTarget: {}\n{}\n{}\n\nNo desktop connection or write was made.\n",
56 title(&format!(
57 "Preview · pages {}",
58 value["operation"].as_str().unwrap_or("write")
59 )),
60 clean(value["target"].as_str().unwrap_or("")),
61 value["title"]
62 .as_str()
63 .map(|s| format!("Title: {}\n", clean(s)))
64 .unwrap_or_default(),
65 clean(value["text"].as_str().unwrap_or(""))
66 );
67 }
68 if let Some(items) = value["items"].as_array() {
69 let mut text = format!(
70 "{}\n",
71 title(&format!("OneNote · {} of {}", items.len(), value["total"]))
72 );
73 if items.is_empty() {
74 text.push_str("\n No matching items.\n");
75 }
76 for item in items {
77 text.push_str(&format!(
78 "\n {}\n",
79 title(item["name"].as_str().unwrap_or("Item"))
80 ));
81 for field in ["path", "id", "last_modified", "locked", "kind", "parent_id"] {
82 if let Some(v) = item.get(field).filter(|v| !v.is_null()) {
83 let v = v
84 .as_str()
85 .map(str::to_owned)
86 .unwrap_or_else(|| v.to_string());
87 text.push_str(&format!(" {field}: {}\n", clean(&v)));
88 }
89 }
90 }
91 if !value["next_offset"].is_null() {
92 text.push_str(&format!(
93 "\n Continue with the same command and --offset {}\n",
94 value["next_offset"]
95 ));
96 }
97 if value["indexing_pending"] == true {
98 text.push_str(&format!("\n {} unindexed pages omitted. Search results may be incomplete until Windows Search finishes indexing.\n", value["unindexed_count"]));
99 }
100 if value["search_mode"] == "scan" {
101 text.push_str(&format!(
102 "\n Text scan · {} pages read; {} locked sections and {} pages skipped.\n",
103 value["scanned_pages"],
104 value["skipped_locked_sections"],
105 value["skipped_pages"].as_array().map_or(0, Vec::len)
106 ));
107 if value["incomplete"] == true {
108 text.push_str(" Results are incomplete for this scope.\n");
109 }
110 if !value["next_scan_offset"].is_null() {
111 text.push_str(&format!(" After paging through these matches, continue with --scan-offset {} and --offset 0.\n", value["next_scan_offset"]));
112 }
113 }
114 return text;
115 }
116 if let Some(markdown) = value["markdown"].as_str() {
117 let mut text = format!(
118 "{}\nid: {}\n\n{}\n",
119 title(value["name"].as_str().unwrap_or("Untitled")),
120 clean(value["id"].as_str().unwrap_or("")),
121 clean(markdown)
122 );
123 if value["verified"] == true {
124 text.push_str("\nWrite verified in OneNote desktop.\n");
125 }
126 if value["has_images"] == true || value["has_attachments"] == true {
127 text.push_str("\nThis page also contains images or attachments; these are not included in the text.\n");
128 }
129 if let Some(xml) = value["xml"].as_str() {
130 text.push_str(&format!("\n{}\n{}\n", title("Page XML"), clean(xml)));
131 }
132 return text;
133 }
134 if value.get("verified").is_some() {
135 return format!(
136 "{}\n{}\n",
137 title(if value["available"] == true {
138 "OneNote desktop is ready"
139 } else {
140 "OneNote desktop check"
141 }),
142 clean(value["message"].as_str().unwrap_or(""))
143 );
144 }
145 clean(&serde_json::to_string_pretty(value).unwrap_or_default()) + "\n"
146}
147pub fn print(value: &Value, format: Format, no_color: bool) -> Result<(), Error> {
148 let text = if structured(format) {
149 serde_json::to_string_pretty(value).map_err(|e| Error::desktop(e.to_string()))? + "\n"
150 } else {
151 render(
152 value,
153 !no_color && std::env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(),
154 )
155 };
156 match io::stdout().lock().write_all(text.as_bytes()) {
157 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
158 result => result.map_err(Error::from),
159 }
160}
161pub fn error(error: &Error, format: Format) {
162 let text = if structured(format) {
163 let mut value = serde_json::json!({"error":{"kind":error.kind,"message":error.message,"exit_code":error.code(),"retryable":false}});
164 if let Some(id) = &error.page_id {
165 value["error"]["page_id"] = Value::String(id.clone());
166 }
167 value.to_string()
168 } else {
169 format!("error: {}", clean(&error.message))
170 };
171 let _ = writeln!(io::stderr().lock(), "{text}");
172}