bard/
lib.rs

1//! `bard`, the Markdown-based songbook compiler.
2//!
3//! > ### <span style="font-variant: small-caps">**This is not a public API.** </span>
4//! This library is an implementation detail of the `bard` CLI tool.
5//! These APIs are internal and may break without notice.
6
7#![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    /// Print program version in semver format
66    #[arg(short = 'V', long, conflicts_with = "version_settings")]
67    pub version: bool,
68    /// Print project settings file version in semver format
69    #[arg(long, conflicts_with = "version_ast")]
70    pub version_settings: bool,
71    /// Print project template AST version in semver format
72    #[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    /// Initialize a new bard project skeleton in this directory
95    Init {
96        #[clap(flatten)]
97        opts: StdioOpts,
98    },
99    /// Build the current project"
100    Make {
101        #[clap(flatten)]
102        opts: MakeOpts,
103    },
104    /// Like make, but keep running and rebuild each time there's a change in project files
105    Watch {
106        #[clap(flatten)]
107        opts: MakeOpts,
108    },
109    /// CLI utilities for postprocessing
110    #[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}