Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 46 additions & 35 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"

"github.com/gdamore/tcell/v2"
"github.com/mattn/go-isatty"

"runtime/debug"

"github.com/spf13/cobra"
)

Expand All @@ -43,6 +42,7 @@ var (
deviceFlag string
clipboardFlag string
timeoutFlag float64
tmuxTTY string
debugLog *log.Logger
errorLog *log.Logger
)
Expand Down Expand Up @@ -95,7 +95,7 @@ func initLogging() (*os.File, error) {

if logfileFlag != "" {
if logOutput, err = os.OpenFile(logfileFlag, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644); err != nil {
return nil, fmt.Errorf("Failed to open file %v: %v", logfileFlag, err)
return nil, fmt.Errorf("failed to open file %v: %v", logfileFlag, err)
} else {
logfile = logOutput
}
Expand All @@ -122,10 +122,9 @@ func identifyTerm() error {
isTmux = true
} else if ti, err := tcell.LookupTerminfo(os.Getenv("TERM")); err != nil {
if runtime.GOOS != "windows" {
return fmt.Errorf("Failed to lookup terminfo: %w", err)
} else {
debugLog.Println("On Windows, failed to lookup terminfo:", err)
return fmt.Errorf("failed to lookup terminfo: %w", err)
}
debugLog.Println("On Windows, failed to lookup terminfo:", err)
} else {
debugLog.Printf("term name: %s, aliases: %q", ti.Name, ti.Aliases)
if strings.HasPrefix(ti.Name, "screen") {
Expand Down Expand Up @@ -186,25 +185,35 @@ func copy(fnames []string) error {
// copy
if isTmux {
if out, err := exec.Command("tmux", "show", "-gwsv", "allow-passthrough").Output(); err != nil {
return fmt.Errorf("Error running 'tmux show -gwsv allow-passthrough': %w", err)
return fmt.Errorf("error running 'tmux show -gwsv allow-passthrough': %w", err)
} else {
outStr := strings.TrimSpace(string(out))
debugLog.Println("'tmux show -gwsv allow-passthrough':", outStr)
if outStr != "on" && outStr != "all" {
return fmt.Errorf("tmux allow-passthrough must be set to 'on' or 'all'")
return errors.New("tmux allow-passthrough must be set to 'on' or 'all'")
}
}
if out, err := exec.Command("tmux", "display-message", "-p", "#{client_tty}").Output(); err != nil {
return fmt.Errorf("error running 'tmux display-message -p #{client_tty}': %w", err)
} else {
outStr := strings.TrimSpace(string(out))
debugLog.Println("'tmux display-message -p #{client_tty}':", outStr)
if !strings.HasPrefix(outStr, "/dev/") {
return errors.New("posix tty of tmux client must have prefix of '/dev/'")
}
tmuxTTY = outStr
}
}

var data []byte
if len(fnames) == 0 {
if isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) {
return fmt.Errorf("Nothing on stdin")
return errors.New("nothing on stdin")
}

var err error
if data, err = io.ReadAll(os.Stdin); err != nil {
return fmt.Errorf("Error reading stdin: %w", err)
return fmt.Errorf("error reading stdin: %w", err)
} else {
debugLog.Printf("Read %d bytes from stdin", len(data))
}
Expand All @@ -213,11 +222,11 @@ func copy(fnames []string) error {

for _, fname := range fnames {
if f, err := os.Open(fname); err != nil {
return fmt.Errorf("Error opening file %s: %w", fname, err)
return fmt.Errorf("error opening file %s: %w", fname, err)
} else if n, err := io.Copy(&dataBuff, f); err != nil {
return fmt.Errorf("Error reading file %s: %w", fname, err)
return fmt.Errorf("error reading file %s: %w", fname, err)
} else if err := f.Close(); err != nil {
return fmt.Errorf("Error closing file %s: %w", fname, err)
return fmt.Errorf("error closing file %s: %w", fname, err)
} else {
debugLog.Printf("Read %d bytes from %s", n, fname)
}
Expand All @@ -229,7 +238,7 @@ func copy(fnames []string) error {

tty, err := opentty()
if err != nil {
return fmt.Errorf("Error opening tty: %w", err)
return fmt.Errorf("error opening tty: %w", err)
}
defer closetty(tty)

Expand All @@ -246,7 +255,7 @@ func copy(fnames []string) error {

// Start OSC52
if _, err := fmt.Fprint(ttyWriter, oscOpen); err != nil {
return fmt.Errorf("Error writing osc open: %w", err)
return fmt.Errorf("error writing osc open: %w", err)
}

var b64 io.WriteCloser
Expand All @@ -257,38 +266,38 @@ func copy(fnames []string) error {
}

if _, err := b64.Write(data); err != nil {
return fmt.Errorf("Error writing data: %w", err)
return fmt.Errorf("error writing data: %w", err)
}

if err := b64.Close(); err != nil {
return fmt.Errorf("Error closing encoder: %w", err)
return fmt.Errorf("error closing encoder: %w", err)
}

// End OSC52
if _, err := fmt.Fprint(ttyWriter, oscClose); err != nil {
return fmt.Errorf("Error writing osc close: %w", err)
return fmt.Errorf("error writing osc close: %w", err)
}

if err := ttyWriter.Flush(); err != nil {
return fmt.Errorf("Error flushing bufio: %w", err)
return fmt.Errorf("error flushing bufio: %w", err)
}

return nil
}

func tmux_paste() error {
if out, err := exec.Command("tmux", "show", "-v", "set-clipboard").Output(); err != nil {
return fmt.Errorf("Error running 'tmux show -v set-clipboard': %w", err)
return fmt.Errorf("error running 'tmux show -v set-clipboard': %w", err)
} else {
outStr := strings.TrimSpace(string(out))
debugLog.Println("'tmux show -v set-clipboard':", outStr)
if outStr != "on" && outStr != "external" {
return fmt.Errorf("tmux set-clipboard must be set to 'on' or 'external'")
return errors.New("tmux set-clipboard must be set to 'on' or 'external'")
}
}
// refresh client list
if out, err := exec.Command("tmux", "refresh-client", "-l").Output(); err != nil {
return fmt.Errorf("Error running 'tmux refresh-client -l': %v", err)
return fmt.Errorf("error running 'tmux refresh-client -l': %v", err)
} else {
debugLog.Println("tmux refresh-client output:", string(out))
}
Expand Down Expand Up @@ -332,15 +341,15 @@ func paste() error {
if isTmux {
return tmux_paste()
} else if isZellij {
return fmt.Errorf("paste unsupported under zellij, unset ZELLIJ env var to force")
return errors.New("paste unsupported under zellij, unset ZELLIJ env var to force")
}
timeout := time.Duration(timeoutFlag*1_000_000_000) * time.Nanosecond
debugLog.Println("Beginning osc52 paste operation, timeout:", timeout)

if data, err := func() ([]byte, error) {
tty, err := opentty()
if err != nil {
return nil, fmt.Errorf("Error opening tty: %w", err)
return nil, fmt.Errorf("error opening tty: %w", err)
}
defer closetty(tty)

Expand All @@ -356,7 +365,7 @@ func paste() error {

// Start OSC52
if _, err := fmt.Fprint(ttyWriter, oscOpen+"?"+oscClose); err != nil {
return nil, fmt.Errorf("Error writing osc open: %w", err)
return nil, fmt.Errorf("error writing osc open: %w", err)
}

var ttyReader *bufio.Reader
Expand Down Expand Up @@ -387,30 +396,30 @@ func paste() error {
select {
case res := <-readChan:
if res.err != nil {
return nil, fmt.Errorf("Initial ReadSlice error: %w", res.err)
return nil, fmt.Errorf("initial ReadSlice error: %w", res.err)
} else if !bytes.Equal(res.data, []byte(OSC)) {
return nil, fmt.Errorf("osc header mismatch: %q", res.data)
}
case <-time.After(timeout):
return nil, fmt.Errorf("tty read timeout")
return nil, errors.New("tty read timeout")
}

// ignore clipboard info
if _, e := ttyReader.ReadSlice(';'); e != nil {
return nil, fmt.Errorf("Clipboard metadata ReadSlice error: %w", e)
return nil, fmt.Errorf("clipboard metadata ReadSlice error: %w", e)
}

pr := pasteReader{r: ttyReader}
decoder := base64.NewDecoder(base64.StdEncoding, &pr)
if data, err := io.ReadAll(decoder); err != nil {
return nil, fmt.Errorf("Error reading from decoder: %w", err)
return nil, fmt.Errorf("error reading from decoder: %w", err)
} else {
return data, nil
}
}(); err != nil {
return err
} else if _, err := os.Stdout.Write(data); err != nil {
return fmt.Errorf("Error writing to stdout: %w", err)
return fmt.Errorf("error writing to stdout: %w", err)
}

debugLog.Println("Ended osc52")
Expand All @@ -434,9 +443,9 @@ osc copy [file1 [...fileN]]
With no arguments, will read from stdin.`,
RunE: func(cmd *cobra.Command, args []string) error {
if matched, err := regexp.MatchString(CLIPBOARD_REGEX, clipboardFlag); err != nil {
return fmt.Errorf("Invalid clipboard flag: %w", err)
return fmt.Errorf("invalid clipboard flag: %w", err)
} else if !matched {
return fmt.Errorf("Invalid clipboard flag: %s", clipboardFlag)
return fmt.Errorf("invalid clipboard flag: %s", clipboardFlag)
}
rc := func() int {
if logfile, err := initLogging(); err != nil {
Expand Down Expand Up @@ -472,9 +481,9 @@ osc paste`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if matched, err := regexp.MatchString(CLIPBOARD_REGEX, clipboardFlag); err != nil {
return fmt.Errorf("Invalid clipboard flag: %w", err)
return fmt.Errorf("invalid clipboard flag: %w", err)
} else if !matched {
return fmt.Errorf("Invalid clipboard flag: %s", clipboardFlag)
return fmt.Errorf("invalid clipboard flag: %s", clipboardFlag)
}
rc := func() int {
if logfile, err := initLogging(); err != nil {
Expand Down Expand Up @@ -526,6 +535,8 @@ func ttyDevice() string {
return deviceFlag
} else if isScreen {
return "/dev/tty"
} else if isTmux {
return tmuxTTY
} else if sshtty := os.Getenv("SSH_TTY"); sshtty != "" {
return sshtty
} else {
Expand Down