1#![allow(clippy::new_ret_no_self)]
8#![allow(clippy::comparison_chain)]
9#![allow(clippy::uninlined_format_args)]
10
11use std::env;
12use std::ffi::OsString;
13
14use app::{App, InterruptFlag, MakeOpts, StdioOpts};
15use clap::{CommandFactory as _, Parser as _};
16use serde::Serialize;
17
18pub mod app;
19pub mod book;
20pub mod default_project;
21pub mod music;
22pub mod parser;
23pub mod prelude;
24pub mod project;
25pub mod render;
26#[cfg(feature = "tectonic")]
27pub mod tectonic_embed;
28pub mod util;
29pub mod util_cmd;
30pub mod watch;
31
32use crate::prelude::*;
33use crate::project::{Project, Settings};
34use crate::util_cmd::UtilCmd;
35use crate::watch::Watch;
36
37#[derive(Serialize, Clone, Debug)]
38pub struct ProgramMeta {
39 pub name: &'static str,
40 pub version: &'static str,
41 pub description: &'static str,
42 pub homepage: &'static str,
43 pub authors: &'static str,
44}
45
46pub const PROGRAM_META: ProgramMeta = ProgramMeta {
47 name: env!("CARGO_PKG_NAME"),
48 version: env!("CARGO_PKG_VERSION"),
49 description: env!("CARGO_PKG_DESCRIPTION"),
50 homepage: env!("CARGO_PKG_HOMEPAGE"),
51 authors: env!("CARGO_PKG_AUTHORS"),
52};
53
54#[derive(clap::Parser)]
55#[command(
56 version = env!("CARGO_PKG_VERSION"),
57 about = "bard: A Markdown-based songbook compiler",
58 help_expected = true,
59 disable_version_flag = true,
60)]
61struct Cli {
62 #[command(subcommand)]
63 cmd: Option<Command>,
64
65 #[arg(short = 'V', long, conflicts_with = "version_settings")]
67 pub version: bool,
68 #[arg(long, conflicts_with = "version_ast")]
70 pub version_settings: bool,
71 #[arg(long, conflicts_with = "version")]
73 pub version_ast: bool,
74}
75
76impl Cli {
77 fn print_version(&self) -> bool {
78 if self.version {
79 println!("{}", PROGRAM_META.version);
80 }
81 if self.version_settings {
82 println!("{}", Settings::version());
83 }
84 if self.version_ast {
85 println!("{}", book::version::current());
86 }
87
88 self.version || self.version_settings || self.version_ast
89 }
90}
91
92#[derive(clap::Parser)]
93enum Command {
94 Init {
96 #[clap(flatten)]
97 opts: StdioOpts,
98 },
99 Make {
101 #[clap(flatten)]
102 opts: MakeOpts,
103 },
104 Watch {
106 #[clap(flatten)]
107 opts: MakeOpts,
108 },
109 #[command(subcommand)]
111 Util(UtilCmd),
112
113 #[cfg(feature = "tectonic")]
114 #[command(hide = true)]
115 Tectonic(tectonic_embed::Tectonic),
116}
117
118impl Command {
119 fn run(self, app: &App) -> Result<()> {
120 use Command::*;
121
122 match self {
123 Init { .. } => bard_init(app),
124 Make { .. } => bard_make(app),
125 Watch { .. } => bard_watch(app),
126 Util(cmd) => cmd.run(app),
127
128 #[cfg(feature = "tectonic")]
129 Tectonic(tectonic) => tectonic.run(app),
130 }
131 }
132}
133
134fn get_cwd() -> Result<PathBuf> {
135 env::current_dir().context("Could not read current directory")
136}
137
138pub fn bard_init_at<P: AsRef<Path>>(app: &App, path: P) -> Result<()> {
139 let path = path.as_ref();
140
141 app.status("Initialize", format!("new project at {:?}", path));
142 Project::init(path).context("Could not initialize a new project")?;
143 app.success("Done!");
144 Ok(())
145}
146
147pub fn bard_init(app: &App) -> Result<()> {
148 let cwd = get_cwd()?;
149 bard_init_at(app, cwd)
150}
151
152pub fn bard_make_at<P: AsRef<Path>>(app: &App, path: P) -> Result<Project> {
153 Project::new(app, path.as_ref())
154 .and_then(|project| {
155 project.render(app)?;
156 Ok(project)
157 })
158 .context("Could not make project")
159}
160
161pub fn bard_make(app: &App) -> Result<()> {
162 let cwd = get_cwd()?;
163
164 bard_make_at(app, cwd)?;
165 app.success("Done!");
166 Ok(())
167}
168
169pub fn bard_watch_at<P: AsRef<Path>>(app: &App, path: P, mut watch: Watch) -> Result<()> {
170 loop {
171 let project = bard_make_at(app, &path)?;
172
173 eprintln!();
174 app.status("Watching", "for changes in the project ...");
175 match watch.watch(&project, app.interrupt_flag())? {
176 Some(paths) if paths.len() == 1 => {
177 app.indent(format!("Change detected at {:?} ...", paths[0]))
178 }
179 Some(..) => app.indent("Change detected ..."),
180 None => break,
181 }
182 }
183
184 Ok(())
185}
186
187pub fn bard_watch(app: &App) -> Result<()> {
188 let cwd = get_cwd()?;
189 let watch = Watch::new()?;
190 bard_watch_at(app, cwd, watch)
191}
192
193pub fn bard(args: &[OsString], interrupt: InterruptFlag) -> i32 {
194 let cli = Cli::parse_from(args);
195 if cli.print_version() {
196 return 0;
197 }
198
199 let cmd = if let Some(cmd) = cli.cmd {
200 cmd
201 } else {
202 let _ = Cli::command().print_help();
203 return 0;
204 };
205
206 let app = match &cmd {
207 Command::Init { opts } => App::new(&opts.clone().into(), interrupt),
208 Command::Make { opts } => App::new(opts, interrupt),
209 Command::Watch { opts } => App::new(opts, interrupt),
210 Command::Util(_) => App::new(&Default::default(), interrupt),
211
212 #[cfg(feature = "tectonic")]
213 Command::Tectonic(_) => App::new_as_tectonic(interrupt),
214 };
215
216 if let Err(err) = cmd.run(&app) {
217 app.error(err);
218 1
219 } else {
220 0
221 }
222}