mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-27 20:36:29 -04:00
Better modularize clipboard code (add "clip" package; break up TOTP code
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
//go:build (android && !termux) || ios
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build (!android && !ios) || termux
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package clip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
decSlice, err := crypt.DecryptFileToSlice(targetLocation)
|
||||
if err != nil {
|
||||
return errors.New("unable to decrypt entry: " + err.Error())
|
||||
}
|
||||
|
||||
// handle non-persistent TOTP copy
|
||||
var copySubject string
|
||||
var realField int
|
||||
if field == -1 {
|
||||
realField = 2
|
||||
} else {
|
||||
realField = field
|
||||
}
|
||||
|
||||
// if field exists in entry...
|
||||
if len(decSlice) > realField {
|
||||
if decSlice[realField] == "" {
|
||||
return errors.New("field is empty")
|
||||
}
|
||||
|
||||
if realField == 2 { // TOTP mode
|
||||
if field != -1 {
|
||||
fmt.Println("Clipboard refreshing with the current TOTP code until this process is closed")
|
||||
}
|
||||
errorChan := make(chan error)
|
||||
go TOTPCopier(decSlice[2], field, errorChan, nil) // "done" is not needed because the process runs until the program is killed
|
||||
err = <-errorChan
|
||||
if err != nil { // block until first successful copy
|
||||
return errors.New("error encountered in TOTP refresh process: " + err.Error())
|
||||
}
|
||||
} else { // other
|
||||
copySubject = decSlice[realField]
|
||||
}
|
||||
} 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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build (android && !termux) || ios
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//go:build android && termux
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build !windows && !darwin && !android && !ios && !termux && !wsl
|
||||
|
||||
package clip
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build windows || (linux && wsl)
|
||||
|
||||
package clip
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build (windows || darwin || android || ios || termux || wsl) && !interactive
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && !interactive
|
||||
|
||||
package clip
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build (windows || darwin || android || ios || termux || wsl) && interactive
|
||||
|
||||
package clip
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && interactive
|
||||
|
||||
package clip
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package clip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TOTPCopier is meant to be run as a goroutine to keep
|
||||
// the clipboard up-to-date with the latest TOTP token.
|
||||
// Set oneTime to -1 for a one-time (non-continuous) TOTP copy.
|
||||
func TOTPCopier(secret string, oneTime int, errorChan chan<- error, done <-chan bool) {
|
||||
var forSteam bool
|
||||
if strings.HasPrefix(secret, "steam@") {
|
||||
secret = secret[6:]
|
||||
forSteam = true
|
||||
}
|
||||
|
||||
for {
|
||||
currentTime := time.Now()
|
||||
token, err := GenTOTP(secret, currentTime, forSteam)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
err = CopyString(true, token)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
if oneTime != -1 {
|
||||
errorChan <- nil
|
||||
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// exit after sleep if indicated (will not update clipboard again)
|
||||
select {
|
||||
case <-done:
|
||||
errorChan <- nil
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user