122 lines
3.2 KiB
Go
122 lines
3.2 KiB
Go
// tether-client: connects to a tether-server, writes incoming messages
|
|
// to the OS clipboard and prints them to stdout.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atotto/clipboard"
|
|
)
|
|
|
|
var (
|
|
noClipboard bool
|
|
myLabel string
|
|
)
|
|
|
|
type Message struct {
|
|
Text string `json:"text"`
|
|
Source string `json:"source,omitempty"`
|
|
TS int64 `json:"ts"`
|
|
}
|
|
|
|
func main() {
|
|
server := flag.String("server", "https://tether.pecord.io", "tether-server base URL")
|
|
label := flag.String("label", "linux-client", "X-Tether-Client label")
|
|
sendText := flag.String("send", "", "send this text and exit (otherwise listen)")
|
|
flag.BoolVar(&noClipboard, "no-clipboard", false, "don't write incoming messages to the OS clipboard")
|
|
flag.Parse()
|
|
myLabel = *label
|
|
|
|
if *sendText != "" {
|
|
send(*server, *label, *sendText)
|
|
return
|
|
}
|
|
|
|
if !noClipboard && clipboard.Unsupported {
|
|
fmt.Fprintln(os.Stderr, "tether-client: OS clipboard unsupported on this platform; falling back to stdout-only")
|
|
noClipboard = true
|
|
}
|
|
|
|
for {
|
|
if err := listen(*server, *label); err != nil {
|
|
log.Printf("stream error: %v — reconnecting in 3s", err)
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func send(server, label, text string) {
|
|
body, _ := json.Marshal(Message{Text: text, Source: label})
|
|
req, _ := http.NewRequest("POST", server+"/api/send", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Tether-Source", label)
|
|
r, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Fatalf("send: %v", err)
|
|
}
|
|
defer r.Body.Close()
|
|
if r.StatusCode >= 300 {
|
|
log.Fatalf("send: HTTP %d", r.StatusCode)
|
|
}
|
|
fmt.Println("sent.")
|
|
}
|
|
|
|
func listen(server, label string) error {
|
|
req, _ := http.NewRequest("GET", server+"/api/stream", nil)
|
|
req.Header.Set("X-Tether-Client", label)
|
|
r, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Body.Close()
|
|
if r.StatusCode != 200 {
|
|
return fmt.Errorf("HTTP %d", r.StatusCode)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "tether-client: connected to %s as %q\n", server, label)
|
|
sc := bufio.NewScanner(r.Body)
|
|
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
|
|
var ev, data string
|
|
for sc.Scan() {
|
|
line := sc.Text()
|
|
switch {
|
|
case strings.HasPrefix(line, "event: "):
|
|
ev = strings.TrimPrefix(line, "event: ")
|
|
case strings.HasPrefix(line, "data: "):
|
|
data = strings.TrimPrefix(line, "data: ")
|
|
case line == "":
|
|
if ev == "clipboard" && data != "" {
|
|
var m Message
|
|
if err := json.Unmarshal([]byte(data), &m); err == nil {
|
|
ts := time.UnixMilli(m.TS).Format("15:04:05")
|
|
fmt.Printf("\n────── %s from %s ──────\n%s\n", ts, m.Source, m.Text)
|
|
// Don't echo our own message back into our own clipboard
|
|
if !noClipboard && m.Source != myLabel {
|
|
if err := clipboard.WriteAll(m.Text); err != nil {
|
|
fmt.Fprintf(os.Stderr, " ! clipboard write error: %v\n", err)
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, " → clipboard updated")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ev, data = "", ""
|
|
case strings.HasPrefix(line, ": "):
|
|
// keepalive — ignore
|
|
}
|
|
}
|
|
if err := sc.Err(); err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|