package main import ( "bytes" "encoding/json" "errors" "fmt" "github.com/gorilla/websocket" "github.com/kbinani/screenshot" "github.com/labstack/echo/v4" "image/color" "image/jpeg" "math" "os/exec" "strconv" "strings" "time" ) type Message struct { Type string `json:type` } type SimpleMessageData struct { Type string `json:type` Value string `json:value` } type PointerMessageData struct { X string `json:x` Y string `json:y` Click string `json:click` } type MessagesData struct { Type string `json:type` Value []SimpleMessageData `json:value` } type ScreenshotMessageData struct { Quality string `json:quality` Pointer bool `json:pointer` } type MessageResponse struct { Type string `json:"type"` Value string `json:"value"` } func getSimpleMessageValue(msg []byte) string { data := SimpleMessageData{} json.Unmarshal([]byte(msg), &data) return data.Value } func sendMessageResponse(ws *websocket.Conn, r MessageResponse) { value, _ := json.Marshal(r) ws.WriteMessage(websocket.TextMessage, value) } func getPointerPosition() (float64, float64) { location := exec.Command("xdotool", "getmouselocation") output, _ := location.Output() position := string(output) currentX := 0.0 currentY := 0.0 for key, value := range strings.Split(position, " ") { if key == 0 { currentX, _ = strconv.ParseFloat(strings.Replace(value, "x:", "", 1), 32) } else if key == 1 { currentY, _ = strconv.ParseFloat(strings.Replace(value, "y:", "", 1), 32) } } return currentX, currentY } func createActions() Actions { actions := Actions{ Functions: make(map[string]func(ws *websocket.Conn, msg []byte) error), } actions.add("pointer", func(ws *websocket.Conn, msg []byte) error { data := PointerMessageData{} json.Unmarshal([]byte(msg), &data) if data.Click != "" { keys := make(map[string]string) keys["left"] = "1" keys["middle"] = "2" keys["right"] = "3" key, exists := keys[data.Click] if !exists { return errors.New("Invalid value") } cmd := exec.Command("xdotool", "click", key) return cmd.Run() } currentX, currentY := getPointerPosition() newX, _ := strconv.ParseFloat(data.X, 32) newY, _ := strconv.ParseFloat(data.Y, 32) x := currentX + newX*2.5 y := currentY + newY*2.5 cmd := exec.Command("xdotool", "mousemove", fmt.Sprintf("%.0f", x), fmt.Sprintf("%.0f", y)) return cmd.Run() }) actions.add("scroll", func(ws *websocket.Conn, msg []byte) error { value := getSimpleMessageValue(msg) key := "" if value == "down" { key = "5" } else if value == "up" { key = "4" } else { return errors.New("Invalid value") } for i := 0; i < 2; i++ { cmd := exec.Command("xdotool", "click", key) cmd.Run() } return nil }) actions.add("workspace", func(ws *websocket.Conn, msg []byte) error { value := getSimpleMessageValue(msg) if value == "" { return errors.New("Invalid value") } cmd := exec.Command("i3-msg", fmt.Sprintf("workspace \"%s\"", value)) return cmd.Run() }) actions.add("volume", func(ws *websocket.Conn, msg []byte) error { value := getSimpleMessageValue(msg) if value == "" { return errors.New("Invalid value") } if value == "up" { cmd := exec.Command("amixer", "set", "Master", "2%+") sendMessageResponse(ws, MessageResponse{ Type: "response", Value: "Volume up", }) return cmd.Run() } if value == "down" { cmd := exec.Command("amixer", "set", "Master", "2%-") sendMessageResponse(ws, MessageResponse{ Type: "response", Value: "Volume down", }) return cmd.Run() } cmd := exec.Command("amixer", "set", "Master", fmt.Sprintf("%s%%", value)) sendMessageResponse(ws, MessageResponse{ Type: "response", Value: fmt.Sprintf("Volume set to %s%%", value), }) return cmd.Run() }) actions.add("media", func(ws *websocket.Conn, msg []byte) error { value := getSimpleMessageValue(msg) if value == "" { return errors.New("Invalid value") } var arg string if value == "playpause" { arg = "play-pause" } else if value == "next" { arg = "next" } else if value == "prev" { arg = "previous" } else { return errors.New("Invalid value") } cmd := exec.Command("playerctl", "-p", "spotify", arg) err := cmd.Run() if err != nil { return err } time.Sleep(400 * time.Millisecond) cmd = exec.Command("playerctl", "-p", "spotify", "status") output, err := cmd.Output() value = strings.TrimSpace(string(output)) if err != nil { return err } if value == "Playing" { cmd = exec.Command("playerctl", "-p", "spotify", "metadata", "xesam:title") output, err := cmd.Output() value = strings.TrimSpace(string(output)) if err != nil { return err } sendMessageResponse(ws, MessageResponse{ Type: "response", Value: fmt.Sprintf("Playing: %s", value), }) } else { sendMessageResponse(ws, MessageResponse{ Type: "response", Value: "Paused", }) } return nil }) actions.add("keys", func(ws *websocket.Conn, msg []byte) error { value := strings.TrimSpace(getSimpleMessageValue(msg)) if value == "" { return errors.New("Invalid value") } keys := []string{} for _, key := range strings.Split(value, ",") { if key == "win" { key = "super" } else if key == "ctrl" { key = "Control_L" } else if key == "alt" { key = "Alt_L" } else if key == "tab" { key = "Tab" } if key != "" { keys = append(keys, key) } } if len(keys) == 0 { return errors.New("Invalid value") } cmd := exec.Command("xdotool", "key", strings.Join(keys, "+")) return cmd.Run() }) actions.add("key", func(ws *websocket.Conn, msg []byte) error { value := strings.TrimSpace(getSimpleMessageValue(msg)) keys := make(map[string]string) keys["up"] = "Up" keys["down"] = "Down" keys["left"] = "Left" keys["right"] = "Right" keys["tab"] = "Tab" keys["backspace"] = "BackSpace" keys["enter"] = "Return" keys["space"] = "space" keys["escape"] = "Escape" key, exists := keys[value] if !exists { return errors.New("Invalid value") } cmd := exec.Command("xdotool", "key", key) return cmd.Run() }) actions.add("text", func(ws *websocket.Conn, msg []byte) error { value := strings.TrimSpace(getSimpleMessageValue(msg)) if value == "" { return errors.New("Invalid value") } cmd := exec.Command("xdotool", "type", value) return cmd.Run() }) actions.add("screenshot", func(ws *websocket.Conn, msg []byte) error { data := ScreenshotMessageData{} json.Unmarshal([]byte(msg), &data) bounds := screenshot.GetDisplayBounds(0) img, err := screenshot.CaptureRect(bounds) if err != nil { return errors.New("Capture error") } var quality int if data.Quality == "lq" { quality = 10 } else { quality = 90 } if data.Pointer { currentX, currentY := getPointerPosition() pointerSize := 2 * 16.0 pixelColor := color.RGBA{ R: 255, G: 0, B: 0, A: 255, } for x := math.Max(0.0, currentX-pointerSize/2); x <= currentX+3; x++ { for y := math.Max(0.0, currentY-pointerSize/2); y < currentY+3; y++ { img.SetRGBA(int(x), int(y), pixelColor) } } } buff := new(bytes.Buffer) jpeg.Encode(buff, img, &jpeg.Options{Quality: quality}) sendMessageResponse(ws, MessageResponse{ Type: "screenshot", Value: toBase64(buff.Bytes()), }) return nil }) actions.add("messages", func(ws *websocket.Conn, msg []byte) error { data := MessagesData{} json.Unmarshal([]byte(msg), &data) for _, value := range data.Value { msg, _ := json.Marshal(value) if actions.has(value.Type) { actions.exec(value.Type, ws, msg) time.Sleep(400 * time.Millisecond) } } return nil }) return actions } var ( upgrader = websocket.Upgrader{} ) func wsController(c echo.Context) error { ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) if err != nil { return err } defer ws.Close() for { _, msg, err := ws.ReadMessage() if err != nil { ws.Close() fmt.Printf("%+v\n", "Connection closed") return err } message := Message{} json.Unmarshal([]byte(msg), &message) if message.Type != "" && actions.has(message.Type) { actions.exec(message.Type, ws, msg) } } return nil }