feat: a chosen folder saves at once, and the prompt stays until it has
CI / Checks (pull_request) Successful in 40s
PR title / Conventional title (pull_request) Successful in 2s

Choosing where to save a share link's files saves them right away,
unless one would replace a file there, which still asks. The folder
prompt stays open until they're written: a folder that isn't there,
or a save that fails, is said under it in red, to fix the path. Its
footer says "enter save", and a file git doesn't ignore is named in
the status line after saving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-13 13:35:01 +02:00
1 parent 0757898d61
commit 25dbb38842
3 files changed
+98 -33

No files matched your search

+64 -27
View File
@@ -191,7 +191,7 @@ func (m *Model) handleSealed(msg sealedMsg) tea.Cmd {
switch {
case msg.err != nil && picking && errors.As(msg.err, &bad):
// Picked: say why in the picker, to pick another.
m.search.pickErr = "✗ " + strings.ToUpper(bad.Error()[:1]) + bad.Error()[1:]
m.search.pickErr = "✗ " + capitalize(bad.Error())
return nil
case msg.err != nil:
m.noteErr(msg.err)
@@ -287,12 +287,13 @@ func (m *Model) handleShareOpened(msg shareOpenedMsg) tea.Cmd {
case msg.dir == "":
return m.askShareDir(msg)
default:
m.offerWrite(msg.files, msg.dir)
m.offerWrite(msg.files, msg.dir, false)
}
return nil
}
// askShareDir asks where to save files from a repo that isn't here.
// askShareDir asks where to save files from a repo that isn't here. It
// stays until they're saved, saying why when they can't be.
func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
in := textinput.New()
in.Prompt = "folder: "
@@ -307,31 +308,51 @@ func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
if msg.sealed.Repo != "" {
lines = append([]string{m.repoLabel(msg.sealed.Repo) + m.st.Muted.Render(" isn't among your repos. Save where?")}, lines...)
}
m.prompt = &modal{
title: "Save the files", input: &in, lines: lines,
submit: func(m *Model, text string) tea.Cmd {
m.offerWrite(msg.files, expandHome(strings.TrimSpace(text)))
return nil
},
p := &modal{title: "Save the files", input: &in, lines: lines, submitLabel: "save"}
p.submit = func(m *Model, text string) tea.Cmd {
if err := m.offerWrite(msg.files, expandHome(strings.TrimSpace(text)), true); err != nil {
p.err = err.Error()
m.prompt = p // again, to fix the folder
return p.input.Focus()
}
return nil
}
return m.prompt.input.Focus()
m.prompt = p
return p.input.Focus()
}
// offerWrite shows where each file goes, then writes them as chosen:
// files there already are replaced only when asked.
func (m *Model) offerWrite(files []share.File, dir string) {
// files there already are replaced only when asked. With chosen (the
// user just picked dir), new files are written at once, and what goes
// wrong is returned rather than shown.
func (m *Model) offerWrite(files []share.File, dir string, chosen bool) error {
fail := func(err error) error {
if !chosen {
m.flash = flash{text: "save: " + err.Error(), isErr: true}
}
return err
}
if !isDir(dir) {
return fail(fmt.Errorf("no folder %s", dir))
}
targets, err := share.Plan(dir, files)
if err != nil {
m.flash = flash{text: "save: " + err.Error(), isErr: true}
return
return fail(err)
}
exist := 0
for _, t := range targets {
if t.Exists {
exist++
}
}
if chosen && exist == 0 {
return m.writeShared(targets, dir, false) // nothing to ask
}
var lines []string
exist := 0
for _, t := range targets {
file := " " + m.repoLabel(filepath.Base(dir), t.Path)
line := file + m.st.Muted.Render(" · new")
if t.Exists {
exist++
line = file + m.st.Muted.Render(" · replaces the one there")
}
if t.Exposed {
@@ -342,18 +363,9 @@ func (m *Model) offerWrite(files []share.File, dir string) {
lines = append([]string{m.st.Muted.Render("In " + dir + ":")}, lines...)
write := func(onlyNew bool) func(*Model) tea.Cmd {
return func(m *Model) tea.Cmd {
n := 0
for _, t := range targets {
if onlyNew && t.Exists {
continue
}
if err := t.Write(); err != nil {
m.flash = flash{text: "save: " + err.Error(), isErr: true}
return nil
}
n++
if err := m.writeShared(targets, dir, onlyNew); err != nil {
m.flash = flash{text: "save: " + err.Error(), isErr: true}
}
m.flash = done("saved " + plural(n, "file") + " in " + dir)
return nil
}
}
@@ -370,6 +382,31 @@ func (m *Model) offerWrite(files []share.File, dir string) {
)
}
m.showMenu("Save the shared files", lines, opts)
return nil
}
// writeShared writes the files (with onlyNew, those not there yet) and
// says where, and which ones git doesn't ignore.
func (m *Model) writeShared(targets []share.Target, dir string, onlyNew bool) error {
n := 0
var exposed []string
for _, t := range targets {
if onlyNew && t.Exists {
continue
}
if err := t.Write(); err != nil {
return fmt.Errorf("%s: %w", t.Path, err)
}
n++
if t.Exposed {
exposed = append(exposed, t.Path)
}
}
m.flash = done("saved " + plural(n, "file") + " in " + dir)
if len(exposed) > 0 {
m.flash = warning("saved " + plural(n, "file") + " in " + dir + "; git doesn't ignore " + strings.Join(exposed, ", "))
}
return nil
}
// isDir reports whether dir is a folder on disk.