onenote-cli 0.1.2

Read and capture Microsoft OneNote notes through its Windows desktop application
Documentation
use clap::{Args, Parser, Subcommand, ValueEnum};

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
    Auto,
    Text,
    Json,
}

#[derive(Debug, Parser)]
#[command(name = "onenote", version, about = "Your OneNote notebooks, from the terminal",
    styles = styles(),
    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.")]
pub struct Cli {
    /// Auto: text in a terminal, JSON when piped
    #[arg(short = 'o', long, global = true, value_enum, default_value = "auto")]
    pub output: Format,
    /// Disable color (also respects NO_COLOR)
    #[arg(long, global = true)]
    pub no_color: bool,
    #[command(subcommand)]
    pub command: Option<Command>,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// List notebooks open in the desktop application
    Notebooks {
        #[command(subcommand)]
        command: Notebooks,
    },
    /// Explore sections, including those inside section groups
    Sections {
        #[command(subcommand)]
        command: Sections,
    },
    /// Read, create, and append to pages
    Pages {
        #[command(subcommand)]
        command: Pages,
    },
    /// Search pages with OneNote's index or a literal text scan
    Search {
        #[arg(value_parser = query)]
        query: String,
        /// Limit search to a notebook, section group, or section ID
        #[arg(long, value_parser = id)]
        scope: Option<String>,
        /// Read page titles and text directly; case-insensitive literal matching
        #[arg(long)]
        scan: bool,
        /// Maximum pages to attempt per scan (default 100, maximum 500)
        #[arg(long, requires = "scan", value_parser = clap::value_parser!(u16).range(1..=500))]
        scan_limit: Option<u16>,
        /// Continue from next_scan_offset; independent of result --offset
        #[arg(long, requires = "scan", value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
        scan_offset: Option<u32>,
        #[command(flatten)]
        page: PageArgs,
    },
    /// Check Windows PowerShell and the OneNote COM connection
    Doctor {
        /// Check the local platform without launching OneNote
        #[arg(long)]
        offline: bool,
    },
    /// Describe supported capabilities and requirements
    Capabilities,
    /// Emit the offline CLI Spec v0.3 contract
    Schema {
        #[arg(long)]
        command: Option<String>,
    },
    /// Generate shell completions
    Completions { shell: clap_complete::Shell },
}
#[derive(Debug, Subcommand)]
pub enum Notebooks {
    List(PageArgs),
}
#[derive(Debug, Subcommand)]
pub enum Sections {
    List {
        /// Notebook ID returned by notebooks list
        #[arg(long, value_parser = id)]
        notebook: String,
        #[command(flatten)]
        page: PageArgs,
    },
}
#[derive(Debug, Subcommand)]
pub enum Pages {
    List {
        /// Section ID returned by sections list
        #[arg(long, value_parser = id)]
        section: String,
        #[command(flatten)]
        page: PageArgs,
    },
    /// Create a page with a title and plain-text content
    Create {
        #[arg(long, value_parser = id)]
        section: String,
        #[arg(long)]
        title: String,
        #[command(flatten)]
        content: ContentArgs,
    },
    /// Add a new text block below existing page content
    Append {
        #[arg(value_parser = id)]
        id: String,
        #[command(flatten)]
        content: ContentArgs,
    },
    Read {
        /// Page ID returned by pages list or search
        #[arg(value_parser = id)]
        id: String,
        /// Include original page XML in the result (without binary payloads)
        #[arg(long)]
        xml: bool,
    },
}
#[derive(Debug, Args)]
#[group(skip)]
pub struct ContentArgs {
    /// Plain text (not Markdown or HTML)
    #[arg(long, required_unless_present = "file", conflicts_with = "file")]
    pub text: Option<String>,
    /// Read UTF-8 text from a file; - reads piped stdin (maximum 1 MiB)
    #[arg(long)]
    pub file: Option<std::path::PathBuf>,
    /// Preview the request without connecting to OneNote or writing anything
    #[arg(long)]
    pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct PageArgs {
    /// Maximum records to return
    #[arg(long, default_value_t = 25, value_parser = clap::value_parser!(u16).range(1..=100))]
    pub limit: u16,
    /// Position from next_offset in the previous result
    #[arg(long, default_value_t = 0, value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
    pub offset: u32,
    /// Comma-separated fields to include in each record
    #[arg(long, value_delimiter = ',')]
    pub fields: Vec<String>,
}
pub fn id(value: &str) -> Result<String, String> {
    if value.trim().is_empty() || value.len() > 4096 || value.chars().any(char::is_control) {
        Err("use a nonempty OneNote desktop ID returned by this CLI (maximum 4096 bytes)".into())
    } else {
        Ok(value.into())
    }
}
fn query(value: &str) -> Result<String, String> {
    if value.trim().is_empty() || value.len() > 8192 || value.contains('\0') {
        Err("search must contain 1–8192 bytes of text".into())
    } else {
        Ok(value.into())
    }
}
fn styles() -> clap::builder::Styles {
    use clap::builder::styling::AnsiColor;
    clap::builder::Styles::styled()
        .header(AnsiColor::Magenta.on_default().bold())
        .literal(AnsiColor::Magenta.on_default())
        .usage(AnsiColor::Magenta.on_default().bold())
        .placeholder(AnsiColor::Cyan.on_default())
}