feat: e on the status pane edits the config (#55)
`e` on the status pane (`0`) opens your config file, as in lazygit's status panel. The status footer shows it as `e edit config`. There's no global key. It opens the config the same way `e` opens a local file: - inside nvim, in that nvim (lazysecrets quits); - otherwise in `$VISUAL` or `$EDITOR`, coming back after; - otherwise in the system's default app. It opens the first config file: your own, also when `--config` layers several. A config that doesn't exist yet is created with a comment linking to its docs. Closing the editor says "restart lazysecrets to use it", since the config is read at startup. Docs: keys.md (status pane) and config.md. Tested: the default app with a new config, nvim with an existing one left unchanged, the restart message, and the footer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #55 Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
This commit was merged in pull request #55.
This commit is contained in:
1 parent
1a5e40a012
commit
2044341b20
6 files changed
+98
-8
No files matched your search
+40
-4
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -13,18 +14,20 @@ import (
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/browser"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/update"
|
||||
)
|
||||
|
||||
// Opening a repo's file (e): SOPS files with sops edit, which decrypts
|
||||
// Opening a repo's file (e), or on the status pane the config: SOPS files with sops edit, which decrypts
|
||||
// them for the editor and encrypts them again after. Other files, inside
|
||||
// nvim: in that nvim, once lazysecrets has quit; else in $VISUAL or
|
||||
// $EDITOR, coming back after; else in the system's default app. On a
|
||||
// secret, editors that can open at a line start on its key.
|
||||
|
||||
type editDoneMsg struct {
|
||||
c provider.Container
|
||||
name string
|
||||
err error
|
||||
c provider.Container
|
||||
name string
|
||||
config bool // the config file: read at startup
|
||||
err error
|
||||
}
|
||||
|
||||
// sopsUnchanged is sops edit's exit code when the file wasn't changed.
|
||||
@@ -44,6 +47,8 @@ func (m *Model) edit() tea.Cmd {
|
||||
var c provider.Container
|
||||
var o *provider.Object
|
||||
switch m.focus {
|
||||
case paneStatus: // like lazygit's status panel
|
||||
return m.editConfig()
|
||||
case paneSources:
|
||||
m.flash = warning("select a file to open it")
|
||||
return nil
|
||||
@@ -93,6 +98,33 @@ func (m *Model) edit() tea.Cmd {
|
||||
return m.openInEditor(f, finished)
|
||||
}
|
||||
|
||||
// configHeader starts a config file made to be edited.
|
||||
const configHeader = "# lazysecrets config: " + update.DefaultRepo + "/src/branch/main/docs/config.md\n"
|
||||
|
||||
// editConfig opens the user's config file (the first of them), as e
|
||||
// opens a file, making it if it isn't there yet: e on the status pane.
|
||||
func (m *Model) editConfig() tea.Cmd {
|
||||
if len(m.configFiles) == 0 {
|
||||
m.flash = warning("no config file")
|
||||
return nil
|
||||
}
|
||||
path := m.configFiles[0]
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
err := os.MkdirAll(filepath.Dir(path), 0o755)
|
||||
if err == nil {
|
||||
err = os.WriteFile(path, []byte(configHeader), 0o644)
|
||||
}
|
||||
if err != nil {
|
||||
m.flash = flash{text: "edit config: " + err.Error(), isErr: true}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
return m.openInEditor(provider.File{Path: path}, func(err error) tea.Msg {
|
||||
return editDoneMsg{name: name, config: true, err: err}
|
||||
})
|
||||
}
|
||||
|
||||
// plusLine are editors that open at a line with +N before the file.
|
||||
var plusLine = map[string]bool{
|
||||
"vi": true, "vim": true, "nvim": true, "nano": true, "emacs": true, "emacsclient": true,
|
||||
@@ -142,6 +174,10 @@ func (m *Model) handleEditDone(msg editDoneMsg) tea.Cmd {
|
||||
m.flash = flash{text: "open " + msg.name + ": " + msg.err.Error(), isErr: true}
|
||||
return nil
|
||||
}
|
||||
if msg.config {
|
||||
m.flash = done("closed " + msg.name + ": restart lazysecrets to use it")
|
||||
return nil
|
||||
}
|
||||
m.flash = done("closed " + msg.name)
|
||||
if c, ok := m.containers.Selected(); ok && containerKey(c) == containerKey(msg.c) {
|
||||
m.hideValue()
|
||||
|
||||
@@ -54,7 +54,8 @@ func (c contextHelp) ShortHelp() []key.Binding {
|
||||
switch {
|
||||
case c.focus == paneStatus:
|
||||
details := key.NewBinding(key.WithKeys(k.Enter.Keys()...), key.WithHelp(k.Enter.Help().Key, "details"))
|
||||
return append(b, details, k.Update, k.Help, k.Quit)
|
||||
config := key.NewBinding(key.WithKeys(k.Edit.Keys()...), key.WithHelp(k.Edit.Help().Key, "edit config"))
|
||||
return append(b, details, config, k.Update, k.Help, k.Quit)
|
||||
case c.focus == paneDetail && c.account && c.of == paneStatus:
|
||||
open := key.NewBinding(key.WithKeys(k.Open.Keys()...), key.WithHelp(k.Open.Help().Key, "open link"))
|
||||
return append(b, k.Up, k.Down, copyField, open, k.Edit, k.Filter, k.Help, k.Quit)
|
||||
|
||||
@@ -342,3 +342,53 @@ func TestEditorAtLine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "lazysecrets", "config.yaml")
|
||||
var opened []string
|
||||
d := newDriverOpts(t, Options{ConfigFiles: []string{path}, OpenURL: func(u string) error { opened = append(opened, u); return nil }})
|
||||
d.m.getenv = func(string) string { return "" }
|
||||
|
||||
// e on the status pane. No editor: the system's default app, on a
|
||||
// config made for it.
|
||||
d.keys("0", "e")
|
||||
if !slices.Equal(opened, []string{path}) {
|
||||
t.Errorf("opened %q", opened)
|
||||
}
|
||||
if b, err := os.ReadFile(path); err != nil || !strings.HasPrefix(string(b), "# lazysecrets config") {
|
||||
t.Errorf("config made: %q, %v", b, err)
|
||||
}
|
||||
|
||||
// Inside nvim: in that nvim; an existing config is left as it is.
|
||||
os.WriteFile(path, []byte("icons: false\n"), 0o644)
|
||||
d.m.getenv = func(k string) string { return map[string]string{"NVIM": "/tmp/nvim.sock"}[k] }
|
||||
var nvim []string
|
||||
d.m.nvimOpen = func(server, path string, line int) error {
|
||||
nvim = append(nvim, server+" "+path)
|
||||
return nil
|
||||
}
|
||||
if msg := d.m.editConfig()(); msg != (tea.QuitMsg{}) {
|
||||
t.Errorf("didn't quit for nvim: %v", msg)
|
||||
}
|
||||
if want := []string{"/tmp/nvim.sock " + path}; !slices.Equal(nvim, want) {
|
||||
t.Errorf("nvim got %q", nvim)
|
||||
}
|
||||
if b, _ := os.ReadFile(path); string(b) != "icons: false\n" {
|
||||
t.Errorf("config changed: %q", b)
|
||||
}
|
||||
|
||||
d.send(editDoneMsg{name: "config.yaml", config: true})
|
||||
if !strings.Contains(d.m.flash.text, "restart") {
|
||||
t.Errorf("flash %q", d.m.flash.text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusFooterEditsConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriver(t)
|
||||
d.keys("0")
|
||||
if !strings.Contains(d.screen(), "e edit config") {
|
||||
t.Errorf("no edit config in the footer:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user