lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 7a7be64c4577f260441f4ade1f1b86ffd3b31b5b
parent 4f250f04cf94416445369fb917ae3b6d38ad2c78
Author: Sean Enck <sean@ttypty.com>
Date:   Sun, 29 Jun 2025 23:21:58 -0400

add clipmgr help, add an ability to stop the daemon

Diffstat:
Mcmd/lb/main.go | 2+-
Minternal/app/commands/core.go | 2++
Minternal/app/help/core.go | 7+++++++
Minternal/app/help/core_test.go | 4++--
Ainternal/app/help/doc/clipmanager.txt | 5+++++
Minternal/platform/clipmanager.go | 23++++++++++++++++++++++-
Minternal/platform/clipmanager_test.go | 60++++++++++++++++++++++++++++++++++++++++++++++++++++--------
7 files changed, 91 insertions(+), 12 deletions(-)

diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -44,7 +44,7 @@ func handleEarly(command string, args []string) (bool, error) { fmt.Printf("version: %s\n", vers) return true, nil case commands.ClipManager, commands.ClipManagerDaemon: - return true, platform.ClipboardManager(command == commands.ClipManagerDaemon, platform.DefaultClipboardDaemon{}) + return true, platform.ClipboardManager(args, command == commands.ClipManagerDaemon, platform.DefaultClipboardDaemon{}) } return false, nil } diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go @@ -68,6 +68,8 @@ const ( TOTPURL = "url" // TOTPSeed will display the seed for the TOTP tokens TOTPSeed = "seed" + // ClipManagerStop will stop the clipboard manager + ClipManagerStop = "-kill" ) var ( diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -38,6 +38,8 @@ type ( HelpConfigCommand string NoColor string ReadOnlyCommands string + ClipManager string + ClipManagerStop string Config struct { Env string Home string @@ -105,6 +107,9 @@ func Usage(verbose bool, exe string) ([]string, error) { results = append(results, subCommand(commands.TOTP, commands.TOTPSeed, isEntry, "show the TOTP seed (only)")) results = append(results, subCommand(commands.TOTP, commands.TOTPShow, isEntry, "show the totp entry")) results = append(results, command(commands.Version, "", "display version information")) + results = append(results, command(commands.ClipManager, "", "run the clipboard manager daemon")) + results = append(results, command(commands.ClipManagerDaemon, "", "clipboard manager daemonized function")) + results = append(results, subCommand(commands.ClipManager, commands.ClipManagerStop, "", "stop a running clipboard manager daemon")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", exe)} if verbose { @@ -117,6 +122,8 @@ func Usage(verbose bool, exe string) ([]string, error) { CompletionsCommand: commands.Completions, HelpCommand: commands.Help, HelpConfigCommand: commands.HelpConfig, + ClipManager: commands.ClipManager, + ClipManagerStop: commands.ClipManagerStop, NoColor: config.NoColorFlag, ReadOnlyCommands: strings.Join(commands.ReadOnly, ", "), } diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go @@ -9,11 +9,11 @@ import ( func TestUsage(t *testing.T) { u, _ := help.Usage(false, "lb") - if len(u) != 27 { + if len(u) != 30 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 128 { + if len(u) != 137 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/app/help/doc/clipmanager.txt b/internal/app/help/doc/clipmanager.txt @@ -0,0 +1,5 @@ +The `{{ $.ClipManager }}` functionality allows running a local clipboard +manager for the system that will monitor the clipboard and clear it when +it has been idle for a (configured) period of time. This is specifically +invoked via `{{ $.Executable }} {{ $.ClipManager }}` (and can be killed +by adding `{{ $.ClipManagerStop }}` as an argument). diff --git a/internal/platform/clipmanager.go b/internal/platform/clipmanager.go @@ -81,7 +81,7 @@ func (d DefaultClipboardDaemon) Checkpid(pid int) error { } // ClipboardManager handles the daemon runner -func ClipboardManager(daemon bool, manager ClipboardDaemon) error { +func ClipboardManager(args []string, daemon bool, manager ClipboardDaemon) error { if manager == nil { return errors.New("manager is nil") } @@ -101,6 +101,24 @@ func ClipboardManager(daemon bool, manager ClipboardDaemon) error { return val, nil } if !daemon { + invalid := false + switch len(args) { + case 0: + break + case 1: + invalid = args[0] != commands.ClipManagerStop + if !invalid { + if PathExists(clipboard.PIDFile) { + return os.WriteFile(clipboard.PIDFile, []byte("0"), 0o644) + } + return nil + } + default: + invalid = true + } + if invalid { + return fmt.Errorf("invalid manager arguments: %v", args) + } if PathExists(clipboard.PIDFile) { p, err := getProcess() if err != nil { @@ -116,6 +134,9 @@ func ClipboardManager(daemon bool, manager ClipboardDaemon) error { } return manager.Start(commands.Executable, commands.ClipManagerDaemon) } + if len(args) > 0 { + return fmt.Errorf("invalid daemon arguments: %v", args) + } paste, pasteArgs, err := clipboard.Args(false) if err != nil { return err diff --git a/internal/platform/clipmanager_test.go b/internal/platform/clipmanager_test.go @@ -72,18 +72,62 @@ func TestErrors(t *testing.T) { store.Clear() defer store.Clear() t.Setenv("WAYLAND_DISPLAY", "1") - if err := platform.ClipboardManager(false, nil); err == nil || err.Error() != "manager is nil" { + if err := platform.ClipboardManager(nil, false, nil); err == nil || err.Error() != "manager is nil" { t.Errorf("invalid error: %v", err) } - if err := platform.ClipboardManager(false, &mock{}); err == nil || err.Error() != "pidfile is unset" { + if err := platform.ClipboardManager(nil, false, &mock{}); err == nil || err.Error() != "pidfile is unset" { t.Errorf("invalid error: %v", err) } store.SetString("LOCKBOX_CLIP_PIDFILE", "a") + if err := platform.ClipboardManager([]string{"x", "y"}, false, &mock{}); err == nil || err.Error() != "invalid manager arguments: [x y]" { + t.Errorf("invalid error: %v", err) + } + if err := platform.ClipboardManager([]string{"x"}, false, &mock{}); err == nil || err.Error() != "invalid manager arguments: [x]" { + t.Errorf("invalid error: %v", err) + } m := &mock{} m.err = errors.New("xyz") - if err := platform.ClipboardManager(true, m); err == nil || strings.Count(err.Error(), "xyz") != 6 { + if err := platform.ClipboardManager(nil, true, m); err == nil || strings.Count(err.Error(), "xyz") != 6 { + t.Errorf("invalid error: %v", err) + } + if err := platform.ClipboardManager([]string{"x"}, true, m); err == nil || err.Error() != "invalid daemon arguments: [x]" { + t.Errorf("invalid error: %v", err) + } +} + +func TestStartKill(t *testing.T) { + store.Clear() + defer store.Clear() + store.SetString("LOCKBOX_CLIP_PIDFILE", "a") + t.Setenv("WAYLAND_DISPLAY", "1") + m := &mock{} + if err := platform.ClipboardManager([]string{"-kill"}, false, m); err != nil { + t.Errorf("invalid error: %v", err) + } + if m.cmd != "" || fmt.Sprintf("%v", m.args) != "[]" { + t.Errorf("invalid calls: %s %v", m.cmd, m.args) + } + pidFile := "testdata" + os.MkdirAll(pidFile, 0o755) + pidFile = filepath.Join("testdata", "pidfile") + store.SetString("LOCKBOX_CLIP_PIDFILE", pidFile) + os.WriteFile(pidFile, []byte("123"), 0o644) + defer os.Remove(pidFile) + m.cmd = "" + m.args = []string{} + if err := platform.ClipboardManager([]string{"-kill"}, false, m); err != nil { + t.Errorf("invalid error: %v", err) + } + if m.cmd != "" || fmt.Sprintf("%v", m.args) != "[]" { + t.Errorf("invalid calls: %s %v", m.cmd, m.args) + } + b, err := os.ReadFile(pidFile) + if err != nil { t.Errorf("invalid error: %v", err) } + if strings.TrimSpace(string(b)) != "0" { + t.Errorf("invalid pid kill: %s", string(b)) + } } func TestStart(t *testing.T) { @@ -92,7 +136,7 @@ func TestStart(t *testing.T) { store.SetString("LOCKBOX_CLIP_PIDFILE", "a") t.Setenv("WAYLAND_DISPLAY", "1") m := &mock{} - if err := platform.ClipboardManager(false, m); err != nil { + if err := platform.ClipboardManager(nil, false, m); err != nil { t.Errorf("invalid error: %v", err) } if m.cmd != "lb" || fmt.Sprintf("%v", m.args) != "[clipmgrd]" { @@ -106,13 +150,13 @@ func TestStart(t *testing.T) { defer os.Remove(pidFile) m.cmd = "" m.args = []string{} - if err := platform.ClipboardManager(false, m); err == nil || !strings.Contains(err.Error(), "Atoi") { + if err := platform.ClipboardManager(nil, false, m); err == nil || !strings.Contains(err.Error(), "Atoi") { t.Errorf("invalid error: %v", err) } m.data = "1234" m.cmd = "" m.args = []string{} - if err := platform.ClipboardManager(false, m); err != nil { + if err := platform.ClipboardManager(nil, false, m); err != nil { t.Errorf("invalid error: %v", err) } if m.cmd != "" || fmt.Sprintf("%v", m.args) != "[]" { @@ -126,7 +170,7 @@ func TestPIDMismatch(t *testing.T) { store.SetString("LOCKBOX_CLIP_PIDFILE", "falsepid") t.Setenv("WAYLAND_DISPLAY", "1") m := &mock{} - if err := platform.ClipboardManager(true, m); err != nil { + if err := platform.ClipboardManager(nil, true, m); err != nil { t.Errorf("invalid error: %v", err) } } @@ -138,7 +182,7 @@ func TestChange(t *testing.T) { t.Setenv("WAYLAND_DISPLAY", "1") m := &mock{} // NOTE: 100 (count before static) + 120 (default timeout) + 1 (caused break of loop) - if err := platform.ClipboardManager(true, m); err == nil || strings.Count(err.Error(), "copied: 221") != 6 { + if err := platform.ClipboardManager(nil, true, m); err == nil || strings.Count(err.Error(), "copied: 221") != 6 { t.Errorf("invalid error: %v", err) } }