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 #[arg(long, global = true, env = "ONENOTE_PROFILE", value_parser = crate::config::profile_name)]
17 pub profile: Option<String>,
18 #[arg(short = 'o', long, global = true, value_enum, default_value = "auto")]
20 pub output: Format,
21 #[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 Tui,
32 Init(InitArgs),
34 Profile {
36 #[command(subcommand)]
37 command: ProfileCommand,
38 },
39 Config {
41 #[command(subcommand)]
42 command: ConfigCommand,
43 },
44 Notebooks {
46 #[command(subcommand)]
47 command: Notebooks,
48 },
49 Sections {
51 #[command(subcommand)]
52 command: Sections,
53 },
54 Pages {
56 #[command(subcommand)]
57 command: Pages,
58 },
59 Search {
61 #[arg(value_parser = query)]
62 query: String,
63 #[arg(long, value_parser = id)]
65 scope: Option<String>,
66 #[arg(long)]
68 scan: bool,
69 #[arg(long, requires = "scan", value_parser = clap::value_parser!(u16).range(1..=500))]
71 scan_limit: Option<u16>,
72 #[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 Doctor {
80 #[arg(long)]
82 offline: bool,
83 },
84 Capabilities,
86 Schema {
88 #[arg(long)]
89 command: Option<String>,
90 },
91 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 #[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 #[arg(long, value_parser = id)]
113 section: String,
114 #[command(flatten)]
115 page: PageArgs,
116 },
117 Create {
119 #[arg(long, value_parser = id)]
120 section: String,
121 #[arg(long)]
122 title: String,
123 #[command(flatten)]
124 content: ContentArgs,
125 },
126 Append {
128 #[arg(value_parser = id)]
129 id: String,
130 #[command(flatten)]
131 content: ContentArgs,
132 },
133 Read {
134 #[arg(value_parser = id)]
136 id: String,
137 #[arg(long)]
139 xml: bool,
140 },
141}
142#[derive(Debug, Args)]
143#[group(skip)]
144pub struct ContentArgs {
145 #[arg(long, required_unless_present = "file", conflicts_with = "file")]
147 pub text: Option<String>,
148 #[arg(long)]
150 pub file: Option<std::path::PathBuf>,
151 #[arg(long)]
153 pub dry_run: bool,
154}
155#[derive(Debug, Args)]
156pub struct PageArgs {
157 #[arg(long, default_value_t = 25, value_parser = clap::value_parser!(u16).range(1..=100))]
159 pub limit: u16,
160 #[arg(long, default_value_t = 0, value_parser = clap::value_parser!(u32).range(0..=1_000_000))]
162 pub offset: u32,
163 #[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 #[arg(long, value_enum, default_value = "desktop")]
194 pub backend: crate::config::Backend,
195 #[arg(long)]
197 pub host: Option<String>,
198 #[arg(long)]
200 pub identity_file: Option<std::path::PathBuf>,
201 #[arg(long, value_parser = clap::value_parser!(u16).range(1..))]
203 pub port: Option<u16>,
204 #[arg(long)]
206 pub read_only: bool,
207 #[arg(long)]
209 pub no_check: bool,
210 #[arg(long)]
212 pub force: bool,
213}
214#[derive(Debug, Subcommand)]
215pub enum ProfileCommand {
216 List,
218 Use {
220 #[arg(value_parser = crate::config::profile_name)]
221 name: String,
222 },
223 Remove {
225 #[arg(value_parser = crate::config::profile_name)]
226 name: String,
227 },
228}
229#[derive(Debug, Subcommand)]
230pub enum ConfigCommand {
231 Show,
233 Path,
235}