Skip to main content

onenote_cli/
output.rs

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}
8// Honor the last output selection even when clap cannot parse the command.
9pub 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}