refactor: tidy up sharing after the round of tweaks
CI / Checks (pull_request) Successful in 41s
PR title / Conventional title (pull_request) Successful in 2s

- Go to's picker settings are one picker struct, set while picking.
- Opening a share link plans the save, then asks or saves; no more
  offerWrite doing either by a flag.
- Closing the picker while it encrypts cancels the share, and a
  second pick meanwhile is ignored.
- Favourites and Recent leave out plain env values as keys too.
- share.key in the config is checked with share.ParseKey, so
  {{secrets/…}} is refused there already.
- A key that can't be read reads the same opening as sharing
  ("can't use …"); KeyError moved to share.go, maxFiles is
  maxUnpacked (bytes), and a stray comment is gone.

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:42:47 +02:00
1 parent 25dbb38842
commit 61de5e616d
9 files changed
+218 -118

No files matched your search

+71 -67
View File
@@ -7,6 +7,7 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strings"
"charm.land/bubbles/v2/textinput"
@@ -36,6 +37,8 @@ type shareFilesMsg struct {
type sealedMsg struct {
link ref.Ref
n int
// from is the picker the key was picked in; nil for share.key's.
from *picker
err error
}
@@ -127,32 +130,34 @@ func (m *Model) sharedFiles(s sharing) []string {
func (m *Model) confirmShare(s sharing) {
lines := append(m.sharedFiles(s), "", m.st.Muted.Render("encrypted with ")+m.refLabel(s.key))
m.showMenu(shareTitle(s), lines, []option{
{key: "c", label: "Copy the link", run: func(m *Model) tea.Cmd { return m.seal(s) }},
{key: "c", label: "Copy the link", run: func(m *Model) tea.Cmd { return m.seal(s, nil) }},
{key: "k", label: "Encrypt with another secret…", run: func(m *Model) tea.Cmd { return m.pickShareKey(s) }},
})
}
// pickShareKey asks which secret to encrypt with, in Go to, then shares
// with it.
// with it: the picker showed what's shared, so there's no asking again.
// It stays open until the link is made, to say why a secret can't be
// used.
func (m *Model) pickShareKey(s sharing) tea.Cmd {
cmd := m.openGoTo(tabSearch, true)
m.search.pick = func(m *Model, key ref.Ref) tea.Cmd {
// The picker showed what's shared: no need to ask again. It
// stays open until the link is made, to say why a key can't be
// used.
p := &picker{
title: shareTitle(s),
lines: m.sharedFiles(s),
heading: "Pick a secret to encrypt with",
help: []string{
m.st.Muted.Render("The recipient needs read access to it to decrypt the files."),
m.st.Muted.Render("Its value isn't shown or put in the link."),
},
}
p.pick = func(m *Model, key ref.Ref) tea.Cmd {
s.key = key
m.search.pickErr, m.search.pickBusy = "", "encrypting with "+key.Name+"…"
cmd := m.seal(s)
p.err, p.busy = "", "encrypting with "+key.Name+"…"
cmd := m.seal(s, p)
m.flash = flash{}
return cmd
}
m.search.pickTitle = shareTitle(s)
m.search.pickLines = m.sharedFiles(s)
m.search.pickHeading = "Pick a secret to encrypt with"
m.search.pickHelp = []string{
m.st.Muted.Render("The recipient needs read access to it to decrypt the files."),
m.st.Muted.Render("Its value isn't shown or put in the link."),
}
m.search.picker = p
m.search.input.SetWidth(searchWidth(m.w) - 10) // inside the picker's border
m.fillSearch(false)
return tea.Batch(cmd, m.searchLoads())
@@ -162,45 +167,45 @@ func (m *Model) pickShareKey(s sharing) tea.Cmd {
func (m *Model) pickPasted(text string) tea.Cmd {
key, err := share.ParseKey(strings.TrimSpace(text))
if err != nil {
m.flash = flash{text: "share: " + err.Error(), isErr: true}
m.search.picker.err = "✗ " + capitalize(err.Error())
return nil
}
return m.search.pick(m, key)
return m.search.picker.choose(m, key)
}
// seal reads the key and encrypts the files with it.
func (m *Model) seal(s sharing) tea.Cmd {
// seal reads the key and encrypts the files with it, for the picker
// from (nil: the key wasn't picked).
func (m *Model) seal(s sharing, from *picker) tea.Cmd {
read, rules := m.keyReader(), m.shareRules
m.flash = flash{text: "encrypting with " + s.key.Name + "…"}
return func() tea.Msg {
v, version, err := read(s.key)
if err != nil {
return sealedMsg{err: &share.KeyError{Name: s.key.Name, Err: err}}
return sealedMsg{from: from, err: &share.KeyError{Name: s.key.Name, Err: err}}
}
link, err := share.Seal(share.Header{Key: share.Pin(s.key, version), Repo: s.repo.Detail}, []byte(v.Reveal()), s.files, rules)
return sealedMsg{link: link, n: len(s.files), err: err}
return sealedMsg{link: link, n: len(s.files), from: from, err: err}
}
}
func (m *Model) handleSealed(msg sealedMsg) tea.Cmd {
picking := m.search != nil && m.search.pick != nil
if picking {
m.search.pickBusy = ""
if p := msg.from; p != nil {
if m.search == nil || m.search.picker != p {
return nil // the picker was closed: cancelled
}
p.busy = ""
var bad *share.KeyError
if errors.As(msg.err, &bad) {
p.err = "✗ " + capitalize(bad.Error()) // to pick another
return nil
}
m.search = nil
}
var bad *share.KeyError
switch {
case msg.err != nil && picking && errors.As(msg.err, &bad):
// Picked: say why in the picker, to pick another.
m.search.pickErr = "✗ " + capitalize(bad.Error())
return nil
case msg.err != nil:
if msg.err != nil {
m.noteErr(msg.err)
m.flash = flash{text: "share: " + msg.err.Error(), isErr: true}
return nil
}
if picking {
m.search = nil
}
m.dismissedLink = msg.link.String() // not offered back on focus
return m.copyText("encrypted link to "+plural(msg.n, "file"), msg.link.String(), false)
}
@@ -258,7 +263,7 @@ func (m *Model) openShare(s share.Sealed) tea.Cmd {
msg := shareOpenedMsg{sealed: s}
v, _, err := read(s.Key)
if err != nil {
msg.err = fmt.Errorf("reading the key %s: %w", s.Key.Name, err)
msg.err = &share.KeyError{Name: s.Key.Name, Err: err}
return msg
}
if msg.files, msg.err = s.Open([]byte(v.Reveal())); msg.err != nil {
@@ -287,7 +292,12 @@ func (m *Model) handleShareOpened(msg shareOpenedMsg) tea.Cmd {
case msg.dir == "":
return m.askShareDir(msg)
default:
m.offerWrite(msg.files, msg.dir, false)
targets, err := planSave(msg.files, msg.dir)
if err != nil {
m.flash = flash{text: "save: " + err.Error(), isErr: true}
return nil
}
m.offerSave(targets, msg.dir)
}
return nil
}
@@ -310,49 +320,44 @@ func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
}
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()
dir := expandHome(strings.TrimSpace(text))
targets, err := planSave(msg.files, dir)
switch {
case err == nil && slices.ContainsFunc(targets, func(t share.Target) bool { return t.Exists }):
m.offerSave(targets, dir) // ask before replacing
return nil
case err == nil:
// Chosen, with nothing to replace: saved at once.
if err = m.writeShared(targets, dir, false); err == nil {
return nil
}
}
return nil
p.err = err.Error()
m.prompt = p // again, to fix the folder
return p.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. 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
}
// planSave says where each file goes in dir, which must be a folder.
func planSave(files []share.File, dir string) ([]share.Target, error) {
if !isDir(dir) {
return fail(fmt.Errorf("no folder %s", dir))
}
targets, err := share.Plan(dir, files)
if err != nil {
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
return nil, fmt.Errorf("no folder %s", dir)
}
return share.Plan(dir, files)
}
// offerSave shows where each file goes, then saves them as chosen:
// files there already are replaced only when asked.
func (m *Model) offerSave(targets []share.Target, dir string) {
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 {
@@ -382,7 +387,6 @@ func (m *Model) offerWrite(files []share.File, dir string, chosen bool) error {
)
}
m.showMenu("Save the shared files", lines, opts)
return nil
}
// writeShared writes the files (with onlyNew, those not there yet) and