spruce/internal/cmd/generate.go
Dan Anglin 90638f5569
fix: customised usage messages
Add functionality to display the default help message when running
spruce without any arguments or when the help flag is used.

Customise the help message for the subcommands.

Additional changes:

- Refactor: move the Runner interface to the internal cmd package
- Fix: Add a summary for each of the subcommands.
- Refactor: Use string builder to replace string literals.
- Perf: Use a switch statement to only create the subcommand that the
  user calls.
2023-08-13 17:45:33 +01:00

162 lines
3.9 KiB
Go

package cmd
import (
"embed"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"path/filepath"
"text/template"
"time"
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs"
)
//go:embed templates/tex/*
var templates embed.FS
type GenerateCommand struct {
*flag.FlagSet
summary string
input string
output string
employmentHistory int
verbose bool
}
func NewGenerateCommand() *GenerateCommand {
gc := GenerateCommand{
FlagSet: flag.NewFlagSet("generate", flag.ExitOnError),
summary: "Generate a PDF file from an existing CV JSON file.",
}
gc.StringVar(&gc.input, "input", "", "specify the CV JSON file that you want to input to the builder.")
gc.StringVar(&gc.output, "output", "", "specify the name of the output CV file.")
gc.IntVar(&gc.employmentHistory, "employment-history", 10, "show employment history within these number of years.")
gc.BoolVar(&gc.verbose, "verbose", false, "set to true to enable verbose logging.")
gc.Usage = usageFunc(gc.Name(), gc.summary, gc.FlagSet)
return &gc
}
func (g *GenerateCommand) Run() error {
historyLimit := time.Now().AddDate(-1*g.employmentHistory, 0, 0)
tempDir, err := os.MkdirTemp("/tmp", "cv-builder-")
if err != nil {
return fmt.Errorf("unable to create a temporary directory; %w", err)
}
defer func() {
err := os.RemoveAll(tempDir)
if err != nil {
slog.Warn(fmt.Sprintf("WARN: An error occurred when removing the temporary directory; %v", err))
}
}()
texFile, err := tex(g.input, tempDir, historyLimit)
if err != nil {
return fmt.Errorf("unable to create the tex file; %w", err)
}
if err := pdf(tempDir, texFile, g.output, g.verbose); err != nil {
return fmt.Errorf("unable to create the PDF file; %w", err)
}
return nil
}
// tex generates the CV document as a Tex file.
func tex(input, tempDir string, historyLimit time.Time) (string, error) {
slog.Info("Creating the Tex file.")
c, err := cv.NewCVFromFile(input)
if err != nil {
return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err)
}
output := filepath.Join(tempDir, "cv.tex")
file, err := os.Create(output)
if err != nil {
return "", fmt.Errorf("unable to create output file %s; %w", output, err)
}
defer file.Close()
fmap := template.FuncMap{
"notLastElement": tf.NotLastElement,
"join": tf.JoinSentences,
"durationToString": tf.FormatDuration,
"withinTimePeriod": tf.WithinTimePeriod(historyLimit),
}
t := template.Must(template.New("cv.tmpl.tex").
Funcs(fmap).
Delims("<<", ">>").
ParseFS(templates, "templates/tex/*.tmpl.tex"),
)
if err = t.Execute(file, c); err != nil {
return "", fmt.Errorf("unable to execute the CV template. %w", err)
}
slog.Info("Tex file successfully created.", "filename", output)
return output, nil
}
// pdf generates the CV document as a PDF file from the tex file.
func pdf(tempDir, texFile, output string, verbose bool) error {
slog.Info("Creating the PDF document.")
pathArg := "--path=" + tempDir
command := exec.Command("mtxrun", pathArg, "--script", "context", texFile)
if verbose {
command.Stderr = os.Stderr
command.Stdout = os.Stdout
}
if err := command.Run(); err != nil {
return err
}
if output == "" {
output = "./cv.pdf"
}
if err := copyfile(filepath.Join(tempDir, "cv.pdf"), output); err != nil {
return err
}
slog.Info("PDF document successfully created.", "filename", output)
return nil
}
func copyfile(input, output string) error {
inputFile, err := os.Open(input)
if err != nil {
return fmt.Errorf("unable to open %s; %w", input, err)
}
defer inputFile.Close()
outputFile, err := os.Create(output)
if err != nil {
return fmt.Errorf("unable to create %s; %w", output, err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return fmt.Errorf("unable to copy %s to %s; %w", input, output, err)
}
return nil
}