From 3b57d59ddcf0694b2bbae8c86ec06faff299e950 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Sat, 21 Dec 2024 15:31:14 -0500 Subject: [PATCH] Allow clearing the clipboard instantly; improve code sharing between platforms; facilitate GUI/TUI client development regarding the clipboard --- core/copy.go | 39 ++++++++++++- core/copyDARWIN.go | 30 ++-------- core/copyTERMUX.go | 32 +++-------- core/copyUNIXGeneric.go | 55 +++++-------------- core/copyWIN.go | 26 ++------- ...go => launchClipClearProcessCLIGeneric.go} | 6 +- core/launchClipClearProcessCLIUNIX.go | 24 ++++++++ core/launchClipClearProcessGUI.go | 9 --- core/launchClipClearProcessGUIGeneric.go | 9 +++ core/launchClipClearProcessGUIUNIX.go | 15 +++++ 10 files changed, 117 insertions(+), 128 deletions(-) rename core/{launchClipClearProcessCLI.go => launchClipClearProcessCLIGeneric.go} (75%) create mode 100644 core/launchClipClearProcessCLIUNIX.go delete mode 100644 core/launchClipClearProcessGUI.go create mode 100644 core/launchClipClearProcessGUIGeneric.go create mode 100644 core/launchClipClearProcessGUIUNIX.go diff --git a/core/copy.go b/core/copy.go index 869f9b1..0ae95fb 100644 --- a/core/copy.go +++ b/core/copy.go @@ -59,18 +59,51 @@ func CopyArgument(targetLocation string, field int) { } } -// ClipClearArgument is called to clear the clipboard after 30 seconds if the contents have not been modified. +// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess. func ClipClearArgument() { // read previous clipboard contents from stdin clipScanner := bufio.NewScanner(os.Stdin) if clipScanner.Scan() { - oldContents := clipScanner.Text() - clipClear(oldContents) + assignedContents := clipScanner.Text() + clipClearProcess(assignedContents) } else { os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment) } } +// clipClearProcess clears the clipboard after 30 seconds if the clipboard contents have not changed. +// assignedContents can be omitted to clear the clipboard immediately and unconditionally. +func clipClearProcess(assignedContents string) { + cmdPaste, cmdClear := getClipCommands() + + clearClipboard := func() { + err := cmdClear.Run() + if err != nil { + fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset) + os.Exit(ErrorClipboard) + } + Exit(0) + } + + // if assignedContents is empty, clear the clipboard immediately and unconditionally + if assignedContents == "" { + clearClipboard() + } + + // wait 30 seconds before checking clipboard contents + time.Sleep(30 * time.Second) + + newContents, err := cmdPaste.Output() + if err != nil { + fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset) + os.Exit(ErrorClipboard) + } + + if assignedContents == strings.TrimRight(string(newContents), "\r\n") { + clearClipboard() + } +} + // GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP). func GenTOTP(secret string, time time.Time, forSteam bool) string { var totpToken string diff --git a/core/copyDARWIN.go b/core/copyDARWIN.go index 300d27d..fd56239 100644 --- a/core/copyDARWIN.go +++ b/core/copyDARWIN.go @@ -6,8 +6,6 @@ import ( "fmt" "os" "os/exec" - "strings" - "time" ) // copyString copies a string to the clipboard. @@ -21,29 +19,13 @@ func copyString(continuous bool, copySubject string) { } if !continuous { - launchClipClear(copySubject) + launchClipClearProcess(copySubject) } } -// clipClear is called in a separate process to clear the clipboard after 30 seconds. -func clipClear(oldContents string) { - time.Sleep(30 * time.Second) - - cmd := exec.Command("pbpaste") - newContents, err := cmd.Output() - if err != nil { - fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - - if oldContents == strings.TrimRight(string(newContents), "\r\n") { - cmd = exec.Command("pbcopy") - writeToStdin(cmd, "") - err = cmd.Run() - if err != nil { - fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - } - Exit(0) +// getClipCommands returns the commands for pasting and clearing the clipboard contents. +func getClipCommands() (*exec.Cmd, *exec.Cmd) { + cmdClear := exec.Command("pbcopy") + writeToStdin(cmdClear, "") + return exec.Command("pbpaste"), cmdClear } diff --git a/core/copyTERMUX.go b/core/copyTERMUX.go index ade3c35..6928ed4 100644 --- a/core/copyTERMUX.go +++ b/core/copyTERMUX.go @@ -1,4 +1,4 @@ -//go:build linux && termux +//go:build android && termux package core @@ -6,8 +6,6 @@ import ( "fmt" "os" "os/exec" - "strings" - "time" ) // copyString copies a string to the clipboard. @@ -21,29 +19,13 @@ func copyString(continuous bool, copySubject string) { } if !continuous { - launchClipClear(copySubject) + launchClipClearProcess(copySubject) } } -// clipClear is called in a separate process to clear the clipboard after 30 seconds. -func clipClear(oldContents string) { - time.Sleep(30 * time.Second) - - cmd := exec.Command("termux-clipboard-get") - newContents, err := cmd.Output() - if err != nil { - fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - - if oldContents == strings.TrimRight(string(newContents), "\r\n") { - cmd = exec.Command("termux-clipboard-set") - writeToStdin(cmd, "") - err = cmd.Run() - if err != nil { - fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - } - Exit(0) +// getClipCommands returns the commands for pasting and clearing the clipboard contents. +func getClipCommands() (*exec.Cmd, *exec.Cmd) { + cmdClear := exec.Command("termux-clipboard-set") + writeToStdin(cmdClear, "") + return exec.Command("termux-clipboard-get"), cmdClear } diff --git a/core/copyUNIXGeneric.go b/core/copyUNIXGeneric.go index 28dac11..60385b2 100644 --- a/core/copyUNIXGeneric.go +++ b/core/copyUNIXGeneric.go @@ -6,68 +6,39 @@ import ( "fmt" "os" "os/exec" - "strings" - "time" ) // copyString copies a string to the clipboard. func copyString(continuous bool, copySubject string) { - var envSet bool // track whether environment variables are set - var cmd *exec.Cmd + var envSet, isWayland bool // track whether environment variables are set + var cmdCopy *exec.Cmd // determine whether to use wl-copy (Wayland) or xclip (X11) if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet { - cmd = exec.Command("wl-copy", "-t", "text/plain") + cmdCopy = exec.Command("wl-copy", "-t", "text/plain") + isWayland = true } else if _, envSet = os.LookupEnv("DISPLAY"); envSet { - cmd = exec.Command("xclip", "-sel", "c", "-t", "text/plain") + cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain") } else { fmt.Println(AnsiError + "Clipboard platform could not be determined - Note that the clipboard does not function in a raw TTY" + AnsiReset) os.Exit(ErrorClipboard) } - writeToStdin(cmd, copySubject) - err := cmd.Run() + writeToStdin(cmdCopy, copySubject) + err := cmdCopy.Run() if err != nil { fmt.Println(AnsiError+"Failed to copy to clipboard:", err.Error()+AnsiReset) os.Exit(ErrorClipboard) } if !continuous { - launchClipClear(copySubject) + launchClipClearProcess(copySubject, isWayland) } } -// clipClear is called in a separate process to clear the clipboard after 30 seconds. -func clipClear(oldContents string) { - time.Sleep(30 * time.Second) - - // determine clipboard tool to use (wl-clipboard VS xclip) - var envSet bool - var cmdClear, cmdPaste *exec.Cmd - if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet { - cmdClear = exec.Command("wl-copy", "-c") - cmdPaste = exec.Command("wl-paste") - } else if _, envSet = os.LookupEnv("DISPLAY"); envSet { - cmdClear = exec.Command("xclip", "-i", "/dev/null", "-sel", "c") - cmdPaste = exec.Command("xclip", "-o", "-sel", "c") - } else { - fmt.Println(AnsiError + "Clipboard platform could not be determined - Neither $WAYLAND_DISPLAY nor $DISPLAY are set" + AnsiReset) - os.Exit(ErrorClipboard) +// getClipCommands returns the commands for pasting and clearing the clipboard contents. +func getClipCommands() (*exec.Cmd, *exec.Cmd) { + if os.Args[2] == "true" { // wayland + return exec.Command("wl-paste"), exec.Command("wl-copy", "-c") } - - // read current clipboard contents - newContents, err := cmdPaste.Output() - if err != nil { - fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - - // clear clipboard if contents have not been modified - if oldContents == strings.TrimRight(string(newContents), "\r\n") { - err = cmdClear.Run() - if err != nil { - fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - } - Exit(0) + return exec.Command("xclip", "-o", "-sel", "c"), exec.Command("xclip", "-i", "/dev/null", "-sel", "c") } diff --git a/core/copyWIN.go b/core/copyWIN.go index 67fa84d..6fe3ed0 100644 --- a/core/copyWIN.go +++ b/core/copyWIN.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "strings" - "time" ) // copyString copies a string to the clipboard. @@ -20,28 +19,11 @@ func copyString(continuous bool, copySubject string) { } if !continuous { - launchClipClear(copySubject) + launchClipClearProcess(copySubject) } } -// clipClear is called in a separate process to clear the clipboard after 30 seconds. -func clipClear(oldContents string) { - time.Sleep(30 * time.Second) - - cmd := exec.Command("powershell.exe", "-c", "Get-Clipboard") - newContents, err := cmd.Output() - if err != nil { - fmt.Println(AnsiError+"Failed to read clipboard contents:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - - if oldContents == strings.TrimRight(string(newContents), "\r\n") { - cmd = exec.Command("powershell.exe", "-c", "Set-Clipboard") - err = cmd.Run() - if err != nil { - fmt.Println(AnsiError+"Failed to clear clipboard:", err.Error()+AnsiReset) - os.Exit(ErrorClipboard) - } - } - Exit(0) +// getClipCommands returns the commands for pasting and clearing the clipboard contents. +func getClipCommands() (*exec.Cmd, *exec.Cmd) { + return exec.Command("powershell.exe", "-c", "Get-Clipboard"), exec.Command("powershell.exe", "-c", "Set-Clipboard") } diff --git a/core/launchClipClearProcessCLI.go b/core/launchClipClearProcessCLIGeneric.go similarity index 75% rename from core/launchClipClearProcessCLI.go rename to core/launchClipClearProcessCLIGeneric.go index eb397fe..c7b1bff 100644 --- a/core/launchClipClearProcessCLI.go +++ b/core/launchClipClearProcessCLIGeneric.go @@ -1,4 +1,4 @@ -//go:build !returnOnExit +//go:build (windows || darwin || android || termux || wsl) && !returnOnExit package core @@ -8,9 +8,9 @@ import ( "os/exec" ) -// launchClipClear launches the automated clipboard clearing process. +// launchClipClearProcess launches the automated clipboard clearing process. // For non-interactive CLI implementations, an entirely separate process is created for this purpose. -func launchClipClear(copySubject string) { +func launchClipClearProcess(copySubject string) { executableName := os.Args[0] cmd := exec.Command(executableName, "clipclear") writeToStdin(cmd, copySubject) diff --git a/core/launchClipClearProcessCLIUNIX.go b/core/launchClipClearProcessCLIUNIX.go new file mode 100644 index 0000000..2843da3 --- /dev/null +++ b/core/launchClipClearProcessCLIUNIX.go @@ -0,0 +1,24 @@ +//go:build !windows && !darwin && !android && !termux && !wsl && !returnOnExit + +package core + +import ( + "fmt" + "os" + "os/exec" + "strconv" +) + +// launchClipClearProcess launches the automated clipboard clearing process. +// For non-interactive CLI implementations, an entirely separate process is created for this purpose. +func launchClipClearProcess(copySubject string, isWayland bool) { + executableName := os.Args[0] + cmd := exec.Command(executableName, "clipclear", strconv.FormatBool(isWayland)) + writeToStdin(cmd, copySubject) + err := cmd.Start() + if err != nil { + fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) + os.Exit(ErrorClipboard) + } + os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations +} diff --git a/core/launchClipClearProcessGUI.go b/core/launchClipClearProcessGUI.go deleted file mode 100644 index 4d0a029..0000000 --- a/core/launchClipClearProcessGUI.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build returnOnExit - -package core - -// launchClipClear launches the automated clipboard clearing process. -// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine. -func launchClipClear(copySubject string) { - go clipClear(copySubject) -} diff --git a/core/launchClipClearProcessGUIGeneric.go b/core/launchClipClearProcessGUIGeneric.go new file mode 100644 index 0000000..c47fa92 --- /dev/null +++ b/core/launchClipClearProcessGUIGeneric.go @@ -0,0 +1,9 @@ +//go:build (windows || darwin || android || termux || wsl) && returnOnExit + +package core + +// launchClipClearProcess launches the automated clipboard clearing process. +// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine. +func launchClipClearProcess(copySubject string) { + go clipClearProcess(copySubject) +} diff --git a/core/launchClipClearProcessGUIUNIX.go b/core/launchClipClearProcessGUIUNIX.go new file mode 100644 index 0000000..b3ea810 --- /dev/null +++ b/core/launchClipClearProcessGUIUNIX.go @@ -0,0 +1,15 @@ +//go:build !windows && !darwin && !android && !termux && !wsl && returnOnExit + +package core + +import ( + "os" + "strconv" +) + +// launchClipClearProcess launches the automated clipboard clearing process. +// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine. +func launchClipClearProcess(copySubject string, isWayland bool) { + os.Args[2] = strconv.FormatBool(isWayland) + go clipClearProcess(copySubject) +}