Skip to main content

onenote_cli/
cli.rs

1use clap::{Args, Parser, Subcommand, ValueEnum};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
4pub enum Format {
5    Auto,
6    Text,
7    Json,
8}
9
10#[derive(Debug, Parser)]
11#[command(name = "onenote", version, about = "Your OneNote notebooks, from the terminal",
12    styles = styles(),
13    after_help = "Start with your Windows desktop session:\n  onenote doctor\n  onenote notebooks list\n  onenote sections list --notebook ID\n  onenote pages list --section ID\n  onenote search 'quarterly review'\n  onenote pages read ID\n  onenote pages create --section ID --title Notes --file notes.txt --dry-run\n  onenote pages append ID --text 'Follow up tomorrow'\n\nFor automation:\n  onenote notebooks list -o json\n  onenote schema\n\nRequires Windows OneNote desktop; WSL can use Windows PowerShell interop.\nNo app registration or separate sign-in. Writes use explicit pages create/append commands; preview with --dry-run.")]
14pub struct Cli {
15    /// Auto: text in a terminal, JSON when piped
16    #[arg(short = 'o', long, global = true, value_enum, default_value = "auto")]
17    pub output: Format,
18    /// Disable color (also respects NO_COLOR)
19    #[arg(long, global = true)]
20    pub no_color: bool,
21    #[command(subcommand)]
22    pub command: Option<Command>,
23}
24
25#[derive(Debug, Subcommand)]
26pub enum Command {
27    /// List notebooks open in the desktop application
28    Notebooks {
29        #[command(subcommand)]
30        command: Notebooks,
31    },
32    /// Explore sections, including those inside section groups
33    Sections {
34        #[command(subcommand)]
35        command: Sections,
36    },
37    /// Read, create, and append to pages
38    Pages {
39        #[command(subcommand)]
40        command: Pages,
41    },
42    /// Search pages with OneNote's index or a literal text scan
43    Search {
44        #[arg(value_parser = query)]
45        query: String,
46        /// Limit search to a notebook, section group, or section ID
47        #[arg(long, value_parser = id)]
48        scope: Option<String>,
49        /// Read page titles and text directly; case-insensitive literal matching
50        #[arg(long)]
51        scan: bool,
52        /// Maximum pages to attempt per scan (default 100, maximum 500)
53        #[arg(long, requires = "scan", value_parser = clap::value_parser!(u16).range(1..=500))]
54        scan_limit: Option<u16>,
55        /// Continue from next_scan_offset; independent of result --offset
56        #[arg(long, requires = "scan", value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
57        scan_offset: Option<u32>,
58        #[command(flatten)]
59        page: PageArgs,
60    },
61    /// Check Windows PowerShell and the OneNote COM connection
62    Doctor {
63        /// Check the local platform without launching OneNote
64        #[arg(long)]
65        offline: bool,
66    },
67    /// Describe supported capabilities and requirements
68    Capabilities,
69    /// Emit the offline CLI Spec v0.3 contract
70    Schema {
71        #[arg(long)]
72        command: Option<String>,
73    },
74    /// Generate shell completions
75    Completions { shell: clap_complete::Shell },
76}
77#[derive(Debug, Subcommand)]
78pub enum Notebooks {
79    List(PageArgs),
80}
81#[derive(Debug, Subcommand)]
82pub enum Sections {
83    List {
84        /// Notebook ID returned by notebooks list
85        #[arg(long, value_parser = id)]
86        notebook: String,
87        #[command(flatten)]
88        page: PageArgs,
89    },
90}
91#[derive(Debug, Subcommand)]
92pub enum Pages {
93    List {
94        /// Section ID returned by sections list
95        #[arg(long, value_parser = id)]
96        section: String,
97        #[command(flatten)]
98        page: PageArgs,
99    },
100    /// Create a page with a title and plain-text content
101    Create {
102        #[arg(long, value_parser = id)]
103        section: String,
104        #[arg(long)]
105        title: String,
106        #[command(flatten)]
107        content: ContentArgs,
108    },
109    /// Add a new text block below existing page content
110    Append {
111        #[arg(value_parser = id)]
112        id: String,
113        #[command(flatten)]
114        content: ContentArgs,
115    },
116    Read {
117        /// Page ID returned by pages list or search
118        #[arg(value_parser = id)]
119        id: String,
120        /// Include original page XML in the result (without binary payloads)
121        #[arg(long)]
122        xml: bool,
123    },
124}
125#[derive(Debug, Args)]
126#[group(skip)]
127pub struct ContentArgs {
128    /// Plain text (not Markdown or HTML)
129    #[arg(long, required_unless_present = "file", conflicts_with = "file")]
130    pub text: Option<String>,
131    /// Read UTF-8 text from a file; - reads piped stdin (maximum 1 MiB)
132    #[arg(long)]
133    pub file: Option<std::path::PathBuf>,
134    /// Preview the request without connecting to OneNote or writing anything
135    #[arg(long)]
136    pub dry_run: bool,
137}
138#[derive(Debug, Args)]
139pub struct PageArgs {
140    /// Maximum records to return
141    #[arg(long, default_value_t = 25, value_parser = clap::value_parser!(u16).range(1..=100))]
142    pub limit: u16,
143    /// Position from next_offset in the previous result
144    #[arg(long, default_value_t = 0, value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
145    pub offset: u32,
146    /// Comma-separated fields to include in each record
147    #[arg(long, value_delimiter = ',')]
148    pub fields: Vec<String>,
149}
150pub fn id(value: &str) -> Result<String, String> {
151    if value.trim().is_empty() || value.len() > 4096 || value.chars().any(char::is_control) {
152        Err("use a nonempty OneNote desktop ID returned by this CLI (maximum 4096 bytes)".into())
153    } else {
154        Ok(value.into())
155    }
156}
157fn query(value: &str) -> Result<String, String> {
158    if value.trim().is_empty() || value.len() > 8192 || value.contains('\0') {
159        Err("search must contain 1–8192 bytes of text".into())
160    } else {
161        Ok(value.into())
162    }
163}
164fn styles() -> clap::builder::Styles {
165    use clap::builder::styling::AnsiColor;
166    clap::builder::Styles::styled()
167        .header(AnsiColor::Magenta.on_default().bold())
168        .literal(AnsiColor::Magenta.on_default())
169        .usage(AnsiColor::Magenta.on_default().bold())
170        .placeholder(AnsiColor::Cyan.on_default())
171}