PrintToVeraeTimeStamp/cmd/verae-print/saveas.go
Grok Bot (for George) 13ebc351f5 0.3.2: Save As filename prompt per print (Windows + macOS)
- promptForFilename setting (default true; env VERAE_PROMPT_FOR_FILENAME),
  'verae-print config --prompt-for-filename', last Save As folder in
  ~/.verae/state.json
- Windows helper: common file dialog (PDF filter, overwrite prompt, topmost
  owner) before stamping; macOS printer path: choose file name (NSSavePanel)
  via 'stamp -prompt-name -title'; cancel discards the job (no stamp/credit)
- job.Options.OutPath: original saved at the chosen path, certificate and
  .verae.json next to it with the chosen base name (never overwritten)
- held jobs keep the chosen path (pendingMeta.OutPath)
- installer: 'Ask for a file name for each print' checkbox; removed the
  'Open the user guide' finish-page checkbox; mac-key-setup asks too
- auth test no longer touches the real macOS keychain (VERAE_SKIP_KEYCHAIN)
- version 0.3.2
2026-10-03 20:00:56 -04:00

102 lines
3 KiB
Go

package main
import (
"fmt"
"os"
"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 = filepath.Base(strings.ReplaceAll(t, `\`, "/"))
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
}