Better modularize clipboard code (add "clip" package; break up TOTP code

This commit is contained in:
2025-11-09 23:23:47 -05:00
parent d49632d221
commit bf1399ce8d
14 changed files with 162 additions and 140 deletions
-36
View File
@@ -1,36 +0,0 @@
//go:build (android && !termux) || ios
package core
import (
"strings"
"time"
"github.com/rwinkhart/go-boilerplate/back"
"golang.design/x/clipboard"
)
// 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) error {
clearClipboard := func() {
clipboard.Write(clipboard.FmtText, []byte(""))
back.Exit(0)
}
// if assignedContents is empty, clear the clipboard immediately and unconditionally
if assignedContents == "" {
clearClipboard()
return nil
}
// wait 30 seconds before checking clipboard contents
time.Sleep(30 * time.Second)
newContents := clipboard.Read(clipboard.FmtText)
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
clearClipboard()
}
return nil
}
-51
View File
@@ -1,51 +0,0 @@
//go:build (!android && !ios) || termux
package core
import (
"errors"
"strings"
"time"
"github.com/rwinkhart/go-boilerplate/back"
)
// 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) error {
cmdPaste, cmdClear := getClipCommands()
clearClipboard := func() error {
err := cmdClear.Run()
if err != nil {
return errors.New("unable to clear clipboard")
}
back.Exit(0)
return nil
}
// if assignedContents is empty, clear the clipboard immediately and unconditionally
if assignedContents == "" {
err := clearClipboard()
if err != nil {
return err
}
return nil
}
// wait 30 seconds before checking clipboard contents
time.Sleep(30 * time.Second)
newContents, err := cmdPaste.Output()
if err != nil {
return errors.New("unable to read clipboard contents")
}
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
err := clearClipboard()
if err != nil {
return err
}
}
return nil
}
-119
View File
@@ -1,119 +0,0 @@
package core
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/crypt"
)
// CopyArgument copies a field from an entry to the clipboard.
// If field is -1, it will one-time copy the TOTP code
// (instead of keeping the clipboard up-to-date).
func CopyArgument(targetLocation string, field int) error {
// ensure targetLocation exists and is a file
_, err := back.TargetIsFile(targetLocation, true)
if err != nil {
return err
}
decryptedEntry, err := crypt.DecryptFileToSlice(targetLocation)
if err != nil {
return errors.New("unable to decrypt entry: " + err.Error())
}
var copySubject string // will store data to be copied
// handle non-persistent TOTP copy
var realField int
if field == -1 {
realField = 2
} else {
realField = field
}
// ensure field exists in entry
if len(decryptedEntry) > realField {
// ensure field is not empty
if decryptedEntry[realField] == "" {
return errors.New("field is empty")
}
if realField != 2 {
copySubject = decryptedEntry[realField]
} else { // TOTP mode
var secret string // stores secret for TOTP generation
var forSteam bool // indicates whether to generate TOTP in Steam format
if strings.HasPrefix(decryptedEntry[2], "steam@") {
secret = decryptedEntry[2][6:]
forSteam = true
} else {
secret = decryptedEntry[2]
}
fmt.Println("Clipboard will be kept up to date with the current TOTP code until this process is closed")
for { // keep token copied to clipboard, refresh on 30-second intervals
currentTime := time.Now()
token, err := GenTOTP(secret, currentTime, forSteam)
if err != nil {
return err
}
err = copyString(true, token)
if err != nil {
return err
}
if field == -1 {
return nil // return early (for interactive clients)
}
// sleep until next 30-second interval
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
}
}
} else {
return errors.New("field does not exist in entry")
}
// copy field to clipboard, launch clipboard clearing process
err = copyString(false, copySubject)
if err != nil {
return err
}
return nil
}
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
func ClipClearArgument() error {
assignedContents := back.ReadFromStdin()
if assignedContents == "" {
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)
}
err := clipClearProcess(assignedContents)
return err
}
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP).
func GenTOTP(secret string, time time.Time, forSteam bool) (string, error) {
var totpToken string
var err error
if forSteam {
totpToken, err = totp.GenerateCodeCustom(secret, time, totp.ValidateOpts{Period: 30, Digits: 5, Encoder: otp.EncoderSteam})
} else {
totpToken, err = totp.GenerateCode(secret, time)
}
if err != nil {
return "", errors.New("unable to generate TOTP token: " + err.Error())
}
return totpToken, nil
}
-31
View File
@@ -1,31 +0,0 @@
//go:build darwin && !ios
package core
import (
"errors"
"os/exec"
"github.com/rwinkhart/go-boilerplate/back"
)
// copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) error {
cmd := exec.Command("pbcopy")
_ = back.WriteToStdin(cmd, copySubject)
err := cmd.Run()
if err != nil {
return errors.New("unable to copy to clipboard: " + err.Error())
}
if !continuous {
LaunchClipClearProcess(copySubject)
}
return nil
}
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
cmdClear := exec.Command("pbcopy")
_ = back.WriteToStdin(cmdClear, "")
return exec.Command("pbpaste"), cmdClear
}
-16
View File
@@ -1,16 +0,0 @@
//go:build (android && !termux) || ios
package core
import (
"golang.design/x/clipboard"
)
// copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) error {
clipboard.Write(clipboard.FmtText, []byte(copySubject))
if !continuous {
LaunchClipClearProcess(copySubject)
}
return nil
}
-31
View File
@@ -1,31 +0,0 @@
//go:build android && termux
package core
import (
"errors"
"os/exec"
"github.com/rwinkhart/go-boilerplate/back"
)
// copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) error {
cmd := exec.Command("termux-clipboard-set")
_ = back.WriteToStdin(cmd, copySubject)
err := cmd.Run()
if err != nil {
return errors.New("unable to copy to clipboard: " + err.Error())
}
if !continuous {
LaunchClipClearProcess(copySubject)
}
return nil
}
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
cmdClear := exec.Command("termux-clipboard-set")
_ = back.WriteToStdin(cmdClear, "")
return exec.Command("termux-clipboard-get"), cmdClear
}
-44
View File
@@ -1,44 +0,0 @@
//go:build !windows && !darwin && !android && !ios && !termux && !wsl
package core
import (
"errors"
"os"
"os/exec"
"github.com/rwinkhart/go-boilerplate/back"
)
// copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) error {
// determine whether to use wl-copy (Wayland) or xclip (X11)
var envSet, isWayland bool // track whether environment variables are set
var cmdCopy *exec.Cmd
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
cmdCopy = exec.Command("wl-copy", "-t", "text/plain")
isWayland = true
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
} else {
return errors.New("clipboard platform could not be determined")
}
_ = back.WriteToStdin(cmdCopy, copySubject)
err := cmdCopy.Run()
if err != nil {
return errors.New("unable to copy to clipboard: " + err.Error())
}
if !continuous {
LaunchClipClearProcess(copySubject, isWayland)
}
return nil
}
// 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")
}
return exec.Command("xclip", "-o", "-sel", "c"), exec.Command("xclip", "-i", "/dev/null", "-sel", "c")
}
-28
View File
@@ -1,28 +0,0 @@
//go:build windows || (linux && wsl)
package core
import (
"errors"
"fmt"
"os/exec"
"strings"
)
// copyString copies a string to the clipboard.
func copyString(continuous bool, copySubject string) error {
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
err := cmd.Run()
if err != nil {
return errors.New("unable to copy to clipboard: " + err.Error())
}
if !continuous {
LaunchClipClearProcess(copySubject)
}
return nil
}
// 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")
}
-19
View File
@@ -1,19 +0,0 @@
//go:build (windows || darwin || android || ios || termux || wsl) && !interactive
package core
import (
"os"
"os/exec"
"github.com/rwinkhart/go-boilerplate/back"
)
// LaunchClipClearProcess launches the timed clipboard clearing process.
// For non-interactive CLI implementations, an entirely separate process is created for this purpose.
func LaunchClipClearProcess(copySubject string) {
cmd := exec.Command(os.Args[0], "clipclear")
_ = back.WriteToStdin(cmd, copySubject)
_ = cmd.Start()
os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations
}
-20
View File
@@ -1,20 +0,0 @@
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && !interactive
package core
import (
"os"
"os/exec"
"strconv"
"github.com/rwinkhart/go-boilerplate/back"
)
// LaunchClipClearProcess launches the timed clipboard clearing process.
// For non-interactive CLI implementations, an entirely separate process is created for this purpose.
func LaunchClipClearProcess(copySubject string, isWayland bool) {
cmd := exec.Command(os.Args[0], "clipclear", strconv.FormatBool(isWayland))
_ = back.WriteToStdin(cmd, copySubject)
_ = cmd.Start()
os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations
}
-10
View File
@@ -1,10 +0,0 @@
//go:build (windows || darwin || android || ios || termux || wsl) && interactive
package core
// LaunchClipClearProcess launches the timed clipboard clearing process.
// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine.
// copySubject can be omitted to clear the clipboard immediately and unconditionally.
func LaunchClipClearProcess(copySubject string) {
go clipClearProcess(copySubject)
}
-16
View File
@@ -1,16 +0,0 @@
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && interactive
package core
import (
"os"
"strconv"
)
// LaunchClipClearProcess launches the timed clipboard clearing process.
// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine.
// copySubject can be omitted to clear the clipboard immediately and unconditionally.
func LaunchClipClearProcess(copySubject string, isWayland bool) {
os.Args = []string{os.Args[0], "", strconv.FormatBool(isWayland)}
go clipClearProcess(copySubject)
}