1pub mod commands;
8pub mod config;
9pub mod editor;
10pub mod output;
11
12use std::path::PathBuf;
13
14use clap::{Args, Parser, Subcommand};
15
16use crate::error::Result;
17use crate::types::{CommentPermissionType, NotePermissionRole};
18
19use self::output::OutputOpts;
20
21#[derive(Debug, Parser)]
23#[command(
24 name = "hackmd",
25 version,
26 about = "HackMD CLI — manage notes, teams, and folders from the terminal",
27 long_about = None
28)]
29pub struct Cli {
30 #[arg(long = "config-dir", env = config::ENV_CONFIG_DIR, global = true)]
32 pub config_dir: Option<PathBuf>,
33
34 #[arg(long = "endpoint", env = config::ENV_ENDPOINT_URL, global = true)]
36 pub endpoint: Option<String>,
37
38 #[arg(long = "token", env = config::ENV_ACCESS_TOKEN, global = true, hide_env_values = true)]
40 pub token: Option<String>,
41
42 #[command(subcommand)]
43 pub command: Command,
44}
45
46#[derive(Debug, Subcommand)]
48pub enum Command {
49 Login,
51 Logout,
53 Whoami(WhoamiArgs),
55 History(HistoryArgs),
57 Export(ExportArgs),
59 Teams(TeamsArgs),
61 Notes(NotesArgs),
63 #[command(name = "team-notes")]
65 TeamNotes(TeamNotesArgs),
66 Tui,
68}
69
70#[derive(Debug, Args)]
73pub struct WhoamiArgs {
74 #[command(flatten)]
75 pub output: OutputOpts,
76}
77
78#[derive(Debug, Args)]
79pub struct HistoryArgs {
80 #[command(flatten)]
81 pub output: OutputOpts,
82}
83
84#[derive(Debug, Args)]
85pub struct ExportArgs {
86 #[arg(long = "note-id")]
88 pub note_id: String,
89}
90
91#[derive(Debug, Args)]
92pub struct TeamsArgs {
93 #[command(flatten)]
94 pub output: OutputOpts,
95}
96
97#[derive(Debug, Args)]
100pub struct NotesArgs {
101 #[command(subcommand)]
102 pub action: NotesCmd,
103}
104
105#[derive(Debug, Subcommand)]
106pub enum NotesCmd {
107 List(NotesListArgs),
109 Get(NotesGetArgs),
111 Create(NotesCreateArgs),
113 Update(NotesUpdateArgs),
115 Delete(NotesDeleteArgs),
117}
118
119#[derive(Debug, Args)]
120pub struct NotesListArgs {
121 #[command(flatten)]
122 pub output: OutputOpts,
123}
124
125#[derive(Debug, Args)]
126pub struct NotesGetArgs {
127 #[arg(long = "note-id")]
128 pub note_id: String,
129 #[command(flatten)]
130 pub output: OutputOpts,
131}
132
133#[derive(Debug, Args)]
134pub struct NotesCreateArgs {
135 #[arg(long = "title")]
137 pub title: Option<String>,
138 #[arg(long = "content")]
140 pub content: Option<String>,
141 #[arg(long = "read-permission", value_enum)]
143 pub read_permission: Option<PermArg>,
144 #[arg(long = "write-permission", value_enum)]
146 pub write_permission: Option<PermArg>,
147 #[arg(long = "comment-permission", value_enum)]
149 pub comment_permission: Option<CommentArg>,
150 #[arg(short = 'e', long = "editor")]
152 pub editor: bool,
153 #[command(flatten)]
154 pub output: OutputOpts,
155}
156
157#[derive(Debug, Args)]
158pub struct NotesUpdateArgs {
159 #[arg(long = "note-id")]
160 pub note_id: String,
161 #[arg(long = "content")]
163 pub content: Option<String>,
164}
165
166#[derive(Debug, Args)]
167pub struct NotesDeleteArgs {
168 #[arg(long = "note-id")]
169 pub note_id: String,
170}
171
172#[derive(Debug, Args)]
175pub struct TeamNotesArgs {
176 #[arg(long = "team-path", global = true)]
178 pub team_path: Option<String>,
179
180 #[command(subcommand)]
181 pub action: TeamNotesCmd,
182}
183
184#[derive(Debug, Subcommand)]
185pub enum TeamNotesCmd {
186 List(TeamNotesListArgs),
188 Create(TeamNotesCreateArgs),
190 Update(TeamNotesUpdateArgs),
192 Delete(TeamNotesDeleteArgs),
194}
195
196#[derive(Debug, Args)]
197pub struct TeamNotesListArgs {
198 #[command(flatten)]
199 pub output: OutputOpts,
200}
201
202#[derive(Debug, Args)]
203pub struct TeamNotesCreateArgs {
204 #[arg(long = "title")]
205 pub title: Option<String>,
206 #[arg(long = "content")]
207 pub content: Option<String>,
208 #[arg(long = "read-permission", value_enum)]
209 pub read_permission: Option<PermArg>,
210 #[arg(long = "write-permission", value_enum)]
211 pub write_permission: Option<PermArg>,
212 #[arg(long = "comment-permission", value_enum)]
213 pub comment_permission: Option<CommentArg>,
214 #[arg(short = 'e', long = "editor")]
215 pub editor: bool,
216 #[command(flatten)]
217 pub output: OutputOpts,
218}
219
220#[derive(Debug, Args)]
221pub struct TeamNotesUpdateArgs {
222 #[arg(long = "note-id")]
223 pub note_id: String,
224 #[arg(long = "content")]
225 pub content: Option<String>,
226}
227
228#[derive(Debug, Args)]
229pub struct TeamNotesDeleteArgs {
230 #[arg(long = "note-id")]
231 pub note_id: String,
232}
233
234#[derive(Debug, Clone, Copy, clap::ValueEnum)]
240#[clap(rename_all = "snake_case")]
241pub enum PermArg {
242 Owner,
243 SignedIn,
244 Guest,
245}
246
247impl From<PermArg> for NotePermissionRole {
248 fn from(v: PermArg) -> Self {
249 match v {
250 PermArg::Owner => NotePermissionRole::Owner,
251 PermArg::SignedIn => NotePermissionRole::SignedIn,
252 PermArg::Guest => NotePermissionRole::Guest,
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, clap::ValueEnum)]
259#[clap(rename_all = "snake_case")]
260pub enum CommentArg {
261 Disabled,
262 Forbidden,
263 Owners,
264 SignedInUsers,
265 Everyone,
266}
267
268impl From<CommentArg> for CommentPermissionType {
269 fn from(v: CommentArg) -> Self {
270 match v {
271 CommentArg::Disabled => CommentPermissionType::Disabled,
272 CommentArg::Forbidden => CommentPermissionType::Forbidden,
273 CommentArg::Owners => CommentPermissionType::Owners,
274 CommentArg::SignedInUsers => CommentPermissionType::SignedInUsers,
275 CommentArg::Everyone => CommentPermissionType::Everyone,
276 }
277 }
278}
279
280pub async fn dispatch(cli: Cli) -> Result<()> {
284 let config_dir = cli.config_dir.as_deref();
285 let endpoint = cli.endpoint.as_deref();
286 let token = cli.token.as_deref();
287
288 match cli.command {
289 Command::Login => commands::auth::login(config_dir, endpoint, token).await,
290 Command::Logout => commands::auth::logout(config_dir),
291 Command::Whoami(args) => {
292 commands::auth::whoami(config_dir, endpoint, token, &args.output).await
293 }
294 Command::History(args) => {
295 commands::history::run(config_dir, endpoint, token, &args.output).await
296 }
297 Command::Export(args) => {
298 commands::export::run(config_dir, endpoint, token, &args.note_id).await
299 }
300 Command::Teams(args) => {
301 commands::teams::run(config_dir, endpoint, token, &args.output).await
302 }
303 Command::Notes(n) => match n.action {
304 NotesCmd::List(a) => {
305 commands::notes::list(config_dir, endpoint, token, &a.output).await
306 }
307 NotesCmd::Get(a) => {
308 commands::notes::get(config_dir, endpoint, token, &a.note_id, &a.output).await
309 }
310 NotesCmd::Create(a) => {
311 commands::notes::create(
312 config_dir,
313 endpoint,
314 token,
315 a.title,
316 a.content,
317 a.read_permission.map(Into::into),
318 a.write_permission.map(Into::into),
319 a.comment_permission.map(Into::into),
320 a.editor,
321 &a.output,
322 )
323 .await
324 }
325 NotesCmd::Update(a) => {
326 commands::notes::update(config_dir, endpoint, token, &a.note_id, a.content).await
327 }
328 NotesCmd::Delete(a) => {
329 commands::notes::delete(config_dir, endpoint, token, &a.note_id).await
330 }
331 },
332 Command::TeamNotes(t) => {
333 let team_path = t.team_path.clone().ok_or_else(|| {
334 crate::error::Error::Config(
335 "--team-path is required for `team-notes` subcommands".into(),
336 )
337 })?;
338 match t.action {
339 TeamNotesCmd::List(a) => {
340 commands::team_notes::list(config_dir, endpoint, token, &team_path, &a.output)
341 .await
342 }
343 TeamNotesCmd::Create(a) => {
344 commands::team_notes::create(
345 config_dir,
346 endpoint,
347 token,
348 &team_path,
349 a.title,
350 a.content,
351 a.read_permission.map(Into::into),
352 a.write_permission.map(Into::into),
353 a.comment_permission.map(Into::into),
354 a.editor,
355 &a.output,
356 )
357 .await
358 }
359 TeamNotesCmd::Update(a) => {
360 commands::team_notes::update(
361 config_dir, endpoint, token, &team_path, &a.note_id, a.content,
362 )
363 .await
364 }
365 TeamNotesCmd::Delete(a) => {
366 commands::team_notes::delete(
367 config_dir, endpoint, token, &team_path, &a.note_id,
368 )
369 .await
370 }
371 }
372 }
373 Command::Tui => commands::tui::run(config_dir, endpoint, token).await,
374 }
375}