gazetta_cli/
lib.rs

1//  Copyright (C) 2015 Steven Allen
2//
3//  This file is part of gazetta.
4//
5//  This program is free software: you can redistribute it and/or modify it under the terms of the
6//  GNU General Public License as published by the Free Software Foundation version 3 of the
7//  License.
8//
9//  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10//  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
11//  the GNU General Public License for more details.
12//
13//  You should have received a copy of the GNU General Public License along with this program.  If
14//  not, see <http://www.gnu.org/licenses/>.
15//
16
17use std::env;
18use std::error::Error;
19use std::fs::File;
20use std::io::Write;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use std::{fs, process};
24
25use clap::{Parser, Subcommand};
26use gazetta_core::model::Source;
27use gazetta_core::render::Gazetta;
28use slug::slugify;
29
30use chrono::offset::Local as Date;
31
32// Internal trait to use dynamic dispatch instead of monomorphizing run.
33trait RenderPaths {
34    fn render_paths(&self, source_path: &Path, dest_path: &Path) -> Result<(), Box<dyn Error>>;
35}
36
37impl<G: Gazetta> RenderPaths for G {
38    fn render_paths(&self, source_path: &Path, dest_path: &Path) -> Result<(), Box<dyn Error>> {
39        let source = Source::new(source_path)?;
40        self.render(&source, dest_path)?;
41        Ok(())
42    }
43}
44
45/// Run the CLI.
46pub fn run<G: Gazetta>(gazetta: G) -> ! {
47    process::exit(_run(&gazetta).unwrap_or_else(|e| {
48        eprintln!("{}", e);
49        1
50    }))
51}
52
53#[derive(Parser)]
54#[command(version, about, long_about = None)]
55pub struct Cli {
56    /// Specify the source directory (defaults to the current directory)
57    #[arg(short, long, value_name = "DIRECTORY")]
58    source: Option<PathBuf>,
59    #[command(subcommand)]
60    commands: Commands,
61}
62
63#[derive(Subcommand)]
64enum Commands {
65    Render {
66        /// Overwrite any existing
67        #[arg(short, long)]
68        force: bool,
69        /// The output directory
70        destination: PathBuf,
71    },
72    New {
73        /// Edit the new page in your $EDITOR
74        #[arg(short, long)]
75        edit: bool,
76        /// Directory in which to create the page
77        directory: PathBuf,
78        /// The page title.
79        title: String,
80    },
81}
82
83fn _run(render_paths: &dyn RenderPaths) -> Result<i32, Box<dyn Error>> {
84    let cli = Cli::parse();
85    let source_path = cli
86        .source
87        .or_else(|| {
88            let mut path = PathBuf::new();
89            path.push(".");
90            while path.exists() {
91                path.push("gazetta.yaml");
92                let is_root = path.exists();
93                path.pop();
94                if is_root {
95                    return Some(path);
96                }
97                path.push("..");
98            }
99            None
100        })
101        .ok_or("Could not find a gazetta config in this directory or any parent directories.")?;
102
103    match cli.commands {
104        Commands::Render { force, destination } => {
105            if fs::metadata(&destination).is_ok() {
106                if force {
107                    fs::remove_dir_all(&destination).map_err(|e| {
108                        format!("Failed to remove '{}': {}", destination.display(), e)
109                    })?;
110                } else {
111                    return Err(format!("Target '{}' exists.", destination.display()).into());
112                }
113            }
114            render_paths.render_paths(&source_path, &destination)?;
115            Ok(0)
116        }
117        Commands::New {
118            edit,
119            directory,
120            title,
121        } => {
122            let mut path = directory;
123            path.push(slugify(&title));
124            if path.exists() {
125                return Err(format!("Directory '{}' exists.", path.display()).into());
126            }
127            fs::create_dir(&path)
128                .map_err(|e| format!("Failed to create directory '{}': {}", path.display(), e))?;
129
130            path.push("index.md");
131            let mut file = File::create(&path)?;
132            writeln!(file, "---")?;
133            writeln!(file, "title: {}", &title)?;
134            writeln!(file, "date: {}", Date::now().format("%Y-%m-%d"))?;
135            writeln!(file, "---")?;
136            println!("Created page: {}", path.display());
137            if edit {
138                path.pop();
139                match Command::new(
140                    env::var_os("EDITOR")
141                        .as_deref()
142                        .unwrap_or_else(|| "vim".as_ref()),
143                )
144                .arg("index.md")
145                .current_dir(path)
146                .status()
147                {
148                    Ok(status) => match status.code() {
149                        Some(code) => Ok(code),
150                        None => Err("Editor was killed.".into()),
151                    },
152                    Err(e) => Err(format!("Failed to spawn editor: {}", e).into()),
153                }
154            } else {
155                Ok(0)
156            }
157        }
158    }
159}