PrintToVeraeTimeStamp/cmd/verae-print/saveas.go

103 lines
3.1 KiB
Go

package main
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
// saveNameFromTitle turns a print-job title into a file name for the Save As
// dialog: path and extension dropped, app prefixes ("Microsoft Word - ")
// removed, characters invalid in file names replaced; empty titles fall back
// to "Printed YYYY-MM-DD HHMMSS".
func saveNameFromTitle(title string, now time.Time) string {
t := strings.TrimSpace(title)
for _, p := range []string{"Microsoft Word - ", "Microsoft Excel - ", "Microsoft PowerPoint - "} {
t = strings.TrimPrefix(t, p)
}
t = path.Base(strings.ReplaceAll(t, `\`, "/")) // slash-only: "a:b" is not a drive here
if ext := filepath.Ext(t); ext != "" && len(ext) <= 6 && !strings.ContainsAny(ext, " ") {
t = strings.TrimSuffix(t, ext)
}
t = strings.Map(func(r rune) rune {
if r < 32 || strings.ContainsRune(`<>:"/\|?*`, r) {
return '-'
}
return r
}, t)
if rs := []rune(t); len(rs) > 120 {
t = string(rs[:120])
}
t = strings.TrimRight(strings.TrimSpace(t), ".")
if t == "" || t == "." || t == "-" {
t = "Printed " + now.Format("2006-01-02 150405")
}
return t + ".pdf"
}
// saveStartDir: last folder used in the dialog, else the configured output
// folder, else Documents/Verae Timestamps (created so the dialog opens there).
func saveStartDir(outDir string) string {
for _, d := range []string{settings.LastSaveDir(), outDir} {
if d == "" {
continue
}
if st, err := os.Stat(d); err == nil && st.IsDir() {
return d
}
}
d, err := job.DefaultOutDir()
if err != nil {
h, _ := os.UserHomeDir()
return h
}
_ = os.MkdirAll(d, 0o755)
return d
}
// askSaveTarget shows the native Save As dialog for a print job. It returns
// (path, true) when the user picked a file, ("", false) when they cancelled,
// and ("", true) when no dialog could be shown (the job is then stamped into
// the default folder as before, so a print is never lost to a dialog error).
func askSaveTarget(lj vlog.Job, title, outDir string) (string, bool) {
name := saveNameFromTitle(title, time.Now())
dir := saveStartDir(outDir)
lj.Printf("saveas: prompting (default name %q, folder %q)", name, dir)
p, ok, err := nativeSaveDialog(name, dir)
if err != nil {
lj.Errorf("saveas: dialog unavailable (%v); saving to default folder", err)
return "", true
}
if !ok {
return "", false
}
p = ensurePDFExt(p)
_ = settings.SetLastSaveDir(filepath.Dir(p))
lj.Printf("saveas: user chose %q", p)
return p, true
}
// ensurePDFExt adds ".pdf" when the user typed a name without it. The dialog
// only confirmed replacing the name as typed, so if "<name>.pdf" exists we
// pick "<name>-2.pdf" etc. instead of overwriting it.
func ensurePDFExt(p string) string {
if strings.EqualFold(filepath.Ext(p), ".pdf") {
return p
}
base := p
p = base + ".pdf"
for i := 2; i < 1000; i++ {
if _, err := os.Stat(p); os.IsNotExist(err) {
break
}
p = fmt.Sprintf("%s-%d.pdf", base, i)
}
return p
}