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 ".pdf" exists we // pick "-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 }