refactor: tidy up sharing after the round of tweaks
- 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:
9 files changed
+218
-118
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user