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 = "Get started:\n  onenote init\n  onenote init --profile kiosk --backend ssh --host kiosk\n  onenote tui\n  onenote profile list\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. Connect locally on Windows/WSL or over SSH from any supported platform.\nNo app registration or separate sign-in. Writes use explicit pages create/append commands; preview with --dry-run.")]
14pub struct Cli {
15    /// Use a saved connection profile (also ONENOTE_PROFILE)
16    #[arg(long, global = true, env = "ONENOTE_PROFILE", value_parser = crate::config::profile_name)]
17    pub profile: Option<String>,
18    /// Auto: text in a terminal, JSON when piped
19    #[arg(short = 'o', long, global = true, value_enum, default_value = "auto")]
20    pub output: Format,
21    /// Disable color (also respects NO_COLOR)
22    #[arg(long, global = true)]
23    pub no_color: bool,
24    #[command(subcommand)]
25    pub command: Option<Command>,
26}
27
28#[derive(Debug, Subcommand)]
29pub enum Command {
30    /// Explore notebooks, sections, and pages in an interactive terminal
31    Tui,
32    /// Configure and check a local desktop or SSH connection
33    Init(InitArgs),
34    /// List, select, or remove saved connection profiles
35    Profile {
36        #[command(subcommand)]
37        command: ProfileCommand,
38    },
39    /// Inspect connection settings and their storage location
40    Config {
41        #[command(subcommand)]
42        command: ConfigCommand,
43    },
44    /// List notebooks open in the desktop application
45    Notebooks {
46        #[command(subcommand)]
47        command: Notebooks,
48    },
49    /// Explore sections, including those inside section groups
50    Sections {
51        #[command(subcommand)]
52        command: Sections,
53    },
54    /// Read, create, and append to pages
55    Pages {
56        #[command(subcommand)]
57        command: Pages,
58    },
59    /// Search pages with OneNote's index or a literal text scan
60    Search {
61        #[arg(value_parser = query)]
62        query: String,
63        /// Limit search to a notebook, section group, or section ID
64        #[arg(long, value_parser = id)]
65        scope: Option<String>,
66        /// Read page titles and text directly; case-insensitive literal matching
67        #[arg(long)]
68        scan: bool,
69        /// Maximum pages to attempt per scan (default 100, maximum 500)
70        #[arg(long, requires = "scan", value_parser = clap::value_parser!(u16).range(1..=500))]
71        scan_limit: Option<u16>,
72        /// Continue from next_scan_offset; independent of result --offset
73        #[arg(long, requires = "scan", value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
74        scan_offset: Option<u32>,
75        #[command(flatten)]
76        page: PageArgs,
77    },
78    /// Check the selected connection and OneNote access
79    Doctor {
80        /// Check configuration and local transport availability without connecting
81        #[arg(long)]
82        offline: bool,
83    },
84    /// Describe supported capabilities and requirements
85    Capabilities,
86    /// Emit the offline CLI Spec v0.3 contract
87    Schema {
88        #[arg(long)]
89        command: Option<String>,
90    },
91    /// Generate shell completions
92    Completions { shell: clap_complete::Shell },
93}
94#[derive(Debug, Subcommand)]
95pub enum Notebooks {
96    List(PageArgs),
97}
98#[derive(Debug, Subcommand)]
99pub enum Sections {
100    List {
101        /// Notebook ID returned by notebooks list
102        #[arg(long, value_parser = id)]
103        notebook: String,
104        #[command(flatten)]
105        page: PageArgs,
106    },
107}
108#[derive(Debug, Subcommand)]
109pub enum Pages {
110    List {
111        /// Section ID returned by sections list
112        #[arg(long, value_parser = id)]
113        section: String,
114        #[command(flatten)]
115        page: PageArgs,
116    },
117    /// Create a page with a title and plain-text content
118    Create {
119        #[arg(long, value_parser = id)]
120        section: String,
121        #[arg(long)]
122        title: String,
123        #[command(flatten)]
124        content: ContentArgs,
125    },
126    /// Add a new text block below existing page content
127    Append {
128        #[arg(value_parser = id)]
129        id: String,
130        #[command(flatten)]
131        content: ContentArgs,
132    },
133    Read {
134        /// Page ID returned by pages list or search
135        #[arg(value_parser = id)]
136        id: String,
137        /// Include original page XML in the result (without binary payloads)
138        #[arg(long)]
139        xml: bool,
140    },
141}
142#[derive(Debug, Args)]
143#[group(skip)]
144pub struct ContentArgs {
145    /// Plain text (not Markdown or HTML)
146    #[arg(long, required_unless_present = "file", conflicts_with = "file")]
147    pub text: Option<String>,
148    /// Read UTF-8 text from a file; - reads piped stdin (maximum 1 MiB)
149    #[arg(long)]
150    pub file: Option<std::path::PathBuf>,
151    /// Preview the request without connecting to OneNote or writing anything
152    #[arg(long)]
153    pub dry_run: bool,
154}
155#[derive(Debug, Args)]
156pub struct PageArgs {
157    /// Maximum records to return
158    #[arg(long, default_value_t = 25, value_parser = clap::value_parser!(u16).range(1..=100))]
159    pub limit: u16,
160    /// Position from next_offset in the previous result
161    #[arg(long, default_value_t = 0, value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
162    pub offset: u32,
163    /// Comma-separated fields to include in each record
164    #[arg(long, value_delimiter = ',')]
165    pub fields: Vec<String>,
166}
167pub fn id(value: &str) -> Result<String, String> {
168    if value.trim().is_empty() || value.len() > 4096 || value.chars().any(char::is_control) {
169        Err("use a nonempty OneNote desktop ID returned by this CLI (maximum 4096 bytes)".into())
170    } else {
171        Ok(value.into())
172    }
173}
174fn query(value: &str) -> Result<String, String> {
175    if value.trim().is_empty() || value.len() > 8192 || value.contains('\0') {
176        Err("search must contain 1–8192 bytes of text".into())
177    } else {
178        Ok(value.into())
179    }
180}
181fn styles() -> clap::builder::Styles {
182    use clap::builder::styling::AnsiColor;
183    clap::builder::Styles::styled()
184        .header(AnsiColor::Magenta.on_default().bold())
185        .literal(AnsiColor::Magenta.on_default())
186        .usage(AnsiColor::Magenta.on_default().bold())
187        .placeholder(AnsiColor::Cyan.on_default())
188}
189
190#[derive(Debug, Args)]
191pub struct InitArgs {
192    /// Connect to this desktop or a Windows machine over SSH
193    #[arg(long, value_enum, default_value = "desktop")]
194    pub backend: crate::config::Backend,
195    /// SSH destination or host alias, optionally user@host
196    #[arg(long)]
197    pub host: Option<String>,
198    /// Private-key file on this machine; otherwise use the SSH agent/config
199    #[arg(long)]
200    pub identity_file: Option<std::path::PathBuf>,
201    /// SSH port; otherwise use the SSH configuration
202    #[arg(long, value_parser = clap::value_parser!(u16).range(1..))]
203    pub port: Option<u16>,
204    /// Refuse page writes through this profile
205    #[arg(long)]
206    pub read_only: bool,
207    /// Save without checking the desktop connection
208    #[arg(long)]
209    pub no_check: bool,
210    /// Replace an existing profile with these settings
211    #[arg(long)]
212    pub force: bool,
213}
214#[derive(Debug, Subcommand)]
215pub enum ProfileCommand {
216    /// List saved connections and mark the active profile
217    List,
218    /// Select the profile used by subsequent commands
219    Use {
220        #[arg(value_parser = crate::config::profile_name)]
221        name: String,
222    },
223    /// Remove saved settings; leaves SSH keys and notebooks untouched
224    Remove {
225        #[arg(value_parser = crate::config::profile_name)]
226        name: String,
227    },
228}
229#[derive(Debug, Subcommand)]
230pub enum ConfigCommand {
231    /// Show the selected connection settings, without connecting
232    Show,
233    /// Print the configuration file location
234    Path,
235}