add pages
refactoring
This commit is contained in:
parent
6962664fbb
commit
e2413a19be
22
Makefile
Normal file
22
Makefile
Normal file
|
@ -0,0 +1,22 @@
|
|||
CGO_ENABLED = 0
|
||||
CC = go build
|
||||
CFLAGS = -trimpath
|
||||
LDFLAGS = all=-w -s
|
||||
GCFLAGS = all=
|
||||
ASMFLAGS = all=
|
||||
|
||||
BUILD_DIR = build
|
||||
LINUX_BIN = app-latest-linux-amd64
|
||||
|
||||
all: build
|
||||
|
||||
deps:
|
||||
go install github.com/GeertJohan/go.rice/rice@latest
|
||||
rice embed-go
|
||||
|
||||
.PHONY:
|
||||
build: deps
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOARCH=amd64 GOOS=linux $(CC) $(CFLAGS) -o $(BUILD_DIR)/$(LINUX_BIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)"
|
||||
|
||||
watch:
|
||||
gowatch -o build/app-live-linux-amd64
|
BIN
build/app-latest-linux-amd64
Executable file
BIN
build/app-latest-linux-amd64
Executable file
Binary file not shown.
BIN
build/app-live-linux-amd64
Executable file
BIN
build/app-live-linux-amd64
Executable file
Binary file not shown.
2
go.mod
2
go.mod
|
@ -3,6 +3,8 @@ module gitnet.fr/deblan/remote-i3wm-go
|
|||
go 1.21.0
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.3 // indirect
|
||||
github.com/daaku/go.zipexe v1.0.2 // indirect
|
||||
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/jezek/xgb v1.1.0 // indirect
|
||||
|
|
9
go.sum
9
go.sum
|
@ -1,9 +1,16 @@
|
|||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
|
||||
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
|
||||
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 h1:VLEKvjGJYAMCXw0/32r9io61tEXnMWDRxMk+peyRVFc=
|
||||
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
|
||||
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 h1:YOp8St+CM/AQ9Vp4XYm4272E77MptJDHkwypQHIRl9Q=
|
||||
|
@ -21,11 +28,13 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
|
|
10
home_controller.go
Normal file
10
home_controller.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func homeController(c echo.Context) error {
|
||||
return c.HTML(http.StatusOK, view("views/page/home.html", nil))
|
||||
}
|
412
main.go
412
main.go
|
@ -1,400 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/kbinani/screenshot"
|
||||
"embed"
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"golang.org/x/net/websocket"
|
||||
"image/jpeg"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"os"
|
||||
)
|
||||
|
||||
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`
|
||||
}
|
||||
|
||||
type MessageResponse struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func getSimpleMessageValue(msg string) string {
|
||||
data := SimpleMessageData{}
|
||||
json.Unmarshal([]byte(msg), &data)
|
||||
|
||||
return data.Value
|
||||
}
|
||||
|
||||
func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
|
||||
value, _ := json.Marshal(r)
|
||||
websocket.Message.Send(ws, string(value))
|
||||
}
|
||||
|
||||
func toBase64(b []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func ws(c echo.Context) error {
|
||||
var actions = Actions{
|
||||
Functions: make(map[string]func(ws *websocket.Conn, msg string) error),
|
||||
}
|
||||
|
||||
actions.add("pointer", func(ws *websocket.Conn, msg string) 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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 string) 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 string) 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 string) 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 string) 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 string) 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"
|
||||
}
|
||||
|
||||
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 string) 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 string) 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 string) 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
|
||||
}
|
||||
|
||||
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 string) 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, string(msg))
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
websocket.Handler(func(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
for {
|
||||
msg := ""
|
||||
websocket.Message.Receive(ws, &msg)
|
||||
|
||||
message := Message{}
|
||||
json.Unmarshal([]byte(msg), &message)
|
||||
|
||||
if message.Type != "" && actions.has(message.Type) {
|
||||
actions.exec(message.Type, ws, msg)
|
||||
}
|
||||
}
|
||||
}).ServeHTTP(c.Response(), c.Request())
|
||||
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
templates map[string]*template.Template
|
||||
//go:embed static
|
||||
staticFiles embed.FS
|
||||
//go:embed views/layout views/page
|
||||
views embed.FS
|
||||
)
|
||||
|
||||
func main() {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
|
||||
RI3_USERNAME := os.Getenv("RI3_USERNAME")
|
||||
RI3_PASSWORD := os.Getenv("RI3_PASSWORD")
|
||||
RI3_BIND := os.Getenv("RI3_BIND")
|
||||
|
||||
if RI3_BIND == "" {
|
||||
RI3_BIND = "0.0.0.0:4000"
|
||||
}
|
||||
|
||||
assetHandler := http.FileServer(rice.MustFindBox("static").HTTPBox())
|
||||
|
||||
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
||||
if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 &&
|
||||
subtle.ConstantTimeCompare([]byte(password), []byte("admin")) == 1 {
|
||||
if RI3_USERNAME == "" && RI3_PASSWORD == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
isValidUsername := subtle.ConstantTimeCompare([]byte(username), []byte(RI3_USERNAME)) == 1
|
||||
isValidPassword := subtle.ConstantTimeCompare([]byte(password), []byte(RI3_PASSWORD)) == 1
|
||||
|
||||
if isValidUsername && isValidPassword {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}))
|
||||
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "Hello, World!")
|
||||
})
|
||||
e.GET("/", echo.WrapHandler(assetHandler))
|
||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
|
||||
e.GET("/", homeController)
|
||||
e.GET("/ws", wsController)
|
||||
|
||||
e.GET("/ws", ws)
|
||||
|
||||
e.Logger.Fatal(e.Start(":4000"))
|
||||
e.Logger.Fatal(e.Start(RI3_BIND))
|
||||
}
|
||||
|
|
84
rice-box.go
Normal file
84
rice-box.go
Normal file
File diff suppressed because one or more lines are too long
7
static/bootstrap.bundle.min.js
vendored
Normal file
7
static/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/bootstrap.min.css
vendored
Normal file
7
static/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/bootstrap.min.js
vendored
Normal file
7
static/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
static/jquery.min.js
vendored
Normal file
4
static/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
141
static/main.css
Normal file
141
static/main.css
Normal file
|
@ -0,0 +1,141 @@
|
|||
a {
|
||||
color: #1e3650;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1e3650;
|
||||
border-color: #0e2640;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active {
|
||||
background: #1e3650;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link {
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
color: #777;
|
||||
margin: 3px 0;
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.select2 {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-radius {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
#pointer {
|
||||
height: calc(100vh - 80px);
|
||||
margin: auto;
|
||||
background: #ccc;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#scrollbar {
|
||||
height: calc(100vh - 80px);
|
||||
width: 50px;
|
||||
background: #333;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fullscreen #scrollbar {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.fullscreen #pointer {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
#pane-pointer .form-group {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#pointer-buttons {
|
||||
position: absolute;
|
||||
margin-top: -42px;
|
||||
width: 100%;
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
#pointer-buttons .btn {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#disconneced {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: #ff6161;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#disconneced a {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#nav {
|
||||
border-bottom: 2px solid #1e3650;
|
||||
}
|
||||
|
||||
#shortcuts_special_keys input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#response {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
background: #748c26;
|
||||
padding: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#screenshot img {
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
310
static/main.js
Normal file
310
static/main.js
Normal file
|
@ -0,0 +1,310 @@
|
|||
var ws;
|
||||
var $pointer, $scroller, $response, $screenshotImg;
|
||||
var scrollLastTimestamp, scrollLastValue;
|
||||
var mousePosX, mousePosY, mouseInitPosX, mouseInitPosY;
|
||||
var isLive = false;
|
||||
var isScreenshotWaiting = false;
|
||||
|
||||
var createWebSocketConnection = function() {
|
||||
ws = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
|
||||
|
||||
ws.onopen = function(event) {
|
||||
$('#disconneced').fadeOut();
|
||||
}
|
||||
|
||||
ws.onclose = function(event) {
|
||||
$('#disconneced').fadeIn();
|
||||
|
||||
window.setTimeout(createWebSocketConnection, 5000);
|
||||
}
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'response') {
|
||||
$response.text(data.value);
|
||||
$response.fadeIn();
|
||||
|
||||
window.setTimeout(function() {
|
||||
$response.fadeOut();
|
||||
}, 2500);
|
||||
} else if (data.type === 'screenshot') {
|
||||
isScreenshotWaiting = false
|
||||
$screenshotImg.attr('src', 'data:image/png;base64, ' + data.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var navigationClickHandler = function(e) {
|
||||
if ($(this).attr('href') === '#') {
|
||||
return
|
||||
}
|
||||
|
||||
$('.pane').hide();
|
||||
|
||||
var target = $(this).attr('href');
|
||||
$(target).show();
|
||||
|
||||
$('#nav a').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
}
|
||||
|
||||
var buttonClickHandler = function(e) {
|
||||
var msg = $(this).attr('data-msg');
|
||||
ws.send(msg);
|
||||
}
|
||||
|
||||
var shortcutClearClickHandler = function(e) {
|
||||
$('#shortcut-key').val('');
|
||||
$('#shortcuts_special_keys input:checked').each(function() {
|
||||
$(this).prop('checked', false).trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
var shortcutSendClickHandler = function(e) {
|
||||
var keys = [];
|
||||
|
||||
$('#shortcuts_special_keys input:checked').each(function() {
|
||||
keys.push($(this).val());
|
||||
});
|
||||
|
||||
var key = $('#shortcut-key').val();
|
||||
|
||||
if (keys.length) {
|
||||
if (key) {
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
var msg = '{"type":"keys","value": "' + (keys.join(',').replace('"', '\\"')) + '"}';
|
||||
ws.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var textClearClickHandler = function(e) {
|
||||
$('#text').val('');
|
||||
}
|
||||
|
||||
var textSendClickHandler = function(e) {
|
||||
var keys = $('#text').val();
|
||||
|
||||
if (keys.length) {
|
||||
var msg = '{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}';
|
||||
ws.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var textKeyUpHandler = function(e) {
|
||||
var keys = $('#text').val();
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
var msg = '{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}';
|
||||
ws.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var liveTextKeyUpHandler = function(e) {
|
||||
var value = $(this).val();
|
||||
var live = false;
|
||||
|
||||
if (e.keyCode === 8) {
|
||||
var msg = '{"type":"key","value": "backspace"}';
|
||||
ws.send(msg);
|
||||
} else if (e.keyCode === 13) {
|
||||
var msg = '{"type":"key","value": "enter"}';
|
||||
ws.send(msg);
|
||||
} else if (value.length) {
|
||||
if (value === ' ') {
|
||||
var msg = '{"type":"key","value": "space"}';
|
||||
ws.send(msg);
|
||||
} else {
|
||||
var msg = '{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}';
|
||||
ws.send(msg);
|
||||
}
|
||||
|
||||
$(this).val('');
|
||||
}
|
||||
}
|
||||
|
||||
var shortcutsSpecialKeysOnChangeHandler = function(e) {
|
||||
$('#shortcuts_special_keys input:checked').each(function() {
|
||||
$(this).parent().addClass('btn-primary').removeClass('btn-secondary');
|
||||
})
|
||||
|
||||
$('#shortcuts_special_keys input:not(:checked)').each(function() {
|
||||
$(this).parent().addClass('btn-secondary').removeClass('btn-primary');
|
||||
})
|
||||
}
|
||||
|
||||
var pointerClickHandler = function(e) {
|
||||
var msg = '{"type":"pointer","click":"left"}';
|
||||
ws.send(msg);
|
||||
}
|
||||
|
||||
var scrollerTouchStartHandler = function(e) {
|
||||
var touch = e.targetTouches[0];
|
||||
mouseInitPosY = touch.pageY;
|
||||
}
|
||||
|
||||
var scrollerTouchMoveHandler = function(e) {
|
||||
var touch = e.changedTouches[0];
|
||||
var value = ((touch.pageY - mouseInitPosY > 0) ? 'down' : 'up');
|
||||
var now = new Date().getTime();
|
||||
|
||||
if (touch.pageY === mouseInitPosY || value === scrollLastValue && scrollLastTimestamp !== null && now - scrollLastTimestamp < 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollLastTimestamp = now;
|
||||
scrollLastValue = value;
|
||||
|
||||
var msg = '{"type":"scroll","value": "' + value + '"}';
|
||||
|
||||
mouseInitPosY = touch.pageY;
|
||||
ws.send(msg);
|
||||
}
|
||||
|
||||
var pointerTouchStartHandler = function(e) {
|
||||
var touch = e.targetTouches[0];
|
||||
mouseInitPosX = touch.pageX;
|
||||
mouseInitPosY = touch.pageY;
|
||||
}
|
||||
|
||||
var pointerTouchMoveHandler = function(e) {
|
||||
if (e.changedTouches.length === 2) {
|
||||
return scrollerTouchMoveHandler(e);
|
||||
}
|
||||
|
||||
var touch = e.changedTouches[0];
|
||||
mousePosX = touch.pageX;
|
||||
mousePosY = touch.pageY;
|
||||
|
||||
var newX = mousePosX - mouseInitPosX;
|
||||
var newY = mousePosY - mouseInitPosY;
|
||||
|
||||
mouseInitPosX = mousePosX;
|
||||
mouseInitPosY = mousePosY;
|
||||
|
||||
var msg = '{"type":"pointer","x": "' + newX + '","y": "' + newY + '"}';
|
||||
|
||||
ws.send(msg);
|
||||
}
|
||||
|
||||
var liveHqClickHandler = function(e) {
|
||||
return liveClickHandler(e, 'hq')
|
||||
}
|
||||
|
||||
var liveLqClickHandler = function(e) {
|
||||
return liveClickHandler(e, 'lq')
|
||||
}
|
||||
|
||||
var liveClickHandler = function(e, quality) {
|
||||
if (isLive) {
|
||||
isLive = false;
|
||||
isScreenshotWaiting = false;
|
||||
$('#live-hq').text(`Live HQ`);
|
||||
$('#live-lq').text(`Live LQ`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isLive = true;
|
||||
$(e.target).text('Stop live');
|
||||
|
||||
var doScreenshot = function() {
|
||||
if (isLive) {
|
||||
if (!isScreenshotWaiting) {
|
||||
isScreenshotWaiting = true
|
||||
ws.send(`{"type":"screenshot","quality":"${quality}"}`);
|
||||
}
|
||||
|
||||
window.setTimeout(doScreenshot, 100);
|
||||
}
|
||||
}
|
||||
|
||||
doScreenshot();
|
||||
}
|
||||
|
||||
var fullscreenHandler = function(e) {
|
||||
var element = $(e.target.getAttribute('data-target'));
|
||||
var isFullscreen = parseInt($(e.target).attr('data-fullscreen'));
|
||||
|
||||
$('body').toggleClass('fullscreen', isFullscreen)
|
||||
|
||||
if (isFullscreen) {
|
||||
element.attr('data-fullscreen', '0');
|
||||
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
}
|
||||
} else {
|
||||
$(e.target).attr('data-fullscreen', '1');
|
||||
|
||||
if (element.get(0).requestFullscreen) {
|
||||
element.get(0).requestFullscreen();
|
||||
} else if (element.get(0).webkitRequestFullscreen) {
|
||||
element.get(0).webkitRequestFullscreen();
|
||||
} else if (element.get(0).mozRequestFullScreen) {
|
||||
element.get(0).mozRequestFullScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var documentHashHandler = function() {
|
||||
var hash = window.location.hash;
|
||||
|
||||
if (hash) {
|
||||
$(hash).show();
|
||||
$('a[href="' + hash + '"]').addClass('active');
|
||||
} else {
|
||||
$('#pane-keyboard').show();
|
||||
$('#nav a').first().addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
var addListeners = function() {
|
||||
$('#nav a').click(navigationClickHandler);
|
||||
$('button[data-msg]').click(buttonClickHandler);
|
||||
|
||||
$('#shortcut-clear').click(shortcutClearClickHandler);
|
||||
$('#shortcuts_special_keys input').change(shortcutsSpecialKeysOnChangeHandler);
|
||||
$('#shortcut-send').click(shortcutSendClickHandler);
|
||||
|
||||
$('#text-clear').click(textClearClickHandler);
|
||||
$('#text-send').click(textSendClickHandler);
|
||||
$('#text').on('keyup', textKeyUpHandler);
|
||||
$('.live-text').on('keyup', liveTextKeyUpHandler);
|
||||
|
||||
$scroller
|
||||
.on('touchstart', scrollerTouchStartHandler)
|
||||
.on('touchmove', scrollerTouchMoveHandler);
|
||||
|
||||
$pointer
|
||||
.on('click', pointerClickHandler)
|
||||
.on('touchstart', pointerTouchStartHandler)
|
||||
.on('touchmove', pointerTouchMoveHandler);
|
||||
|
||||
$('#live-hq').click(liveHqClickHandler);
|
||||
$('#live-lq').click(liveLqClickHandler);
|
||||
|
||||
$('.btn-fullscreen').click(fullscreenHandler)
|
||||
}
|
||||
|
||||
var bootstrap = function() {
|
||||
documentHashHandler();
|
||||
shortcutsSpecialKeysOnChangeHandler();
|
||||
createWebSocketConnection();
|
||||
addListeners();
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$pointer = $('#pointer');
|
||||
$scroller = $('#scrollbar');
|
||||
$response = $('#response');
|
||||
$screenshotImg = $('#screenshot img');
|
||||
|
||||
bootstrap();
|
||||
});
|
20
utils.go
Normal file
20
utils.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
func view(viewName string, data any) string {
|
||||
var render bytes.Buffer
|
||||
|
||||
view := template.Must(template.ParseFS(views, viewName, "views/layout/base.html"))
|
||||
view.Execute(&render, data)
|
||||
|
||||
return render.String()
|
||||
}
|
||||
|
||||
func toBase64(b []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
26
views/layout/base.html
Normal file
26
views/layout/base.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{{define "main"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<link rel="stylesheet" href="/static/bootstrap.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="/static/main.css" type="text/css">
|
||||
<title>Remote i3-wm</title>
|
||||
</head>
|
||||
<body>
|
||||
{{template "content" .}}
|
||||
|
||||
<div id="disconneced">
|
||||
You are disconnected [<a href="#" onclick="location.reload(); return false;">Refresh</a>]
|
||||
</div>
|
||||
|
||||
<div id="response"></div>
|
||||
|
||||
<script src="/static/jquery.min.js"></script>
|
||||
<script src="/static/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/bootstrap.min.js"></script>
|
||||
<script src="/static/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
217
views/page/home.html
Normal file
217
views/page/home.html
Normal file
|
@ -0,0 +1,217 @@
|
|||
{{template "main" .}}
|
||||
{{define "content"}}
|
||||
<div class="container-fluid no-padding">
|
||||
<div class="row no-margin">
|
||||
<div class="col-12 no-padding">
|
||||
<ul class="nav nav-pills nav-fill" id="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link no-radius" href="#pane-keyboard">KEYBOARD</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link no-radius" href="#pane-i3">I3</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link no-radius" href="#pane-pointer">MOUSE</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link no-radius" href="#pane-media">MEDIA</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link no-radius" href="#pane-desktop">DESKTOP</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link no-radius btn-fullscreen" data-target="html" href="#">💻</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div id="pane-keyboard" class="pane">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="legend">Live text</p>
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
<input type="text" class="form-control live-text" name="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="legend">TEXT</p>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<input type="text" class="form-control" id="text">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<button type="button" id="text-send" class="btn btn-primary">Send</button>
|
||||
<button type="button" id="text-clear" class="btn btn-secondary">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="legend">Keys</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"key","value":"left"}' class="btn btn-secondary">←</button>
|
||||
<button type="button" data-msg='{"type":"key","value":"up"}' class="btn btn-secondary">↑</button>
|
||||
<button type="button" data-msg='{"type":"key","value":"down"}' class="btn btn-secondary">↓</button>
|
||||
<button type="button" data-msg='{"type":"key","value":"right"}' class="btn btn-secondary">→</button>
|
||||
</div>
|
||||
<div class="line col-12"></div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"key","value":"escape"}' class="btn btn-secondary">Escape</button>
|
||||
<button type="button" data-msg='{"type":"key","value":"tab"}' class="btn btn-secondary">TAB</button>
|
||||
<button type="button" data-msg='{"type":"key","value":"backspace"}' class="btn btn-secondary">Backspace</button>
|
||||
<button type="button" data-msg='{"type":"key","value":"enter"}' class="btn btn-secondary">Enter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="legend">Shortcuts</p>
|
||||
</div>
|
||||
<div class="col-9" id="shortcuts_special_keys">
|
||||
<label class="btn btn-secondary" for="shortcuts_special_key_ctrl">
|
||||
<input type="checkbox" value="ctrl" id="shortcuts_special_key_ctrl">
|
||||
ctrl
|
||||
</label>
|
||||
<label class="btn btn-secondary" for="shortcuts_special_key_shift">
|
||||
<input type="checkbox" value="shift" id="shortcuts_special_key_shift">
|
||||
shift
|
||||
</label>
|
||||
<label class="btn btn-secondary" for="shortcuts_special_key_alt">
|
||||
<input type="checkbox" value="alt" id="shortcuts_special_key_alt">
|
||||
alt
|
||||
</label>
|
||||
<label class="btn btn-secondary" for="shortcuts_special_key_win">
|
||||
<input type="checkbox" value="win" id="shortcuts_special_key_win">
|
||||
win
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group col-3">
|
||||
<input type="text" id="shortcut-key" class="form-control" name="shortcuts_char">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" id="shortcut-send" class="btn btn-primary">Send</button>
|
||||
<button type="button" id="shortcut-clear" class="btn btn-secondary">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pane-i3" class="pane">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="legend">Workspaces</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"workspace","value":"1. IRC"}' class="btn btn-secondary btn-sm">1. IRC</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"2. WWW"}' class="btn btn-secondary btn-sm">2. WWW</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"3. MAIL"}' class="btn btn-secondary btn-sm">3. MAIL</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"6. MEDIA"}' class="btn btn-secondary btn-sm">6. MEDIA</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"7. WORK"}' class="btn btn-secondary btn-sm">7. WORK</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"8. VM"}' class="btn btn-secondary btn-sm">8. VM</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"workspace","value":"4"}' class="btn btn-secondary btn-sm">4</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"5"}' class="btn btn-secondary btn-sm">5</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"9"}' class="btn btn-secondary btn-sm">9</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"10"}' class="btn btn-secondary btn-sm">10</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"11"}' class="btn btn-secondary btn-sm">11</button>
|
||||
<button type="button" data-msg='{"type":"workspace","value":"12"}' class="btn btn-secondary btn-sm">12</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="legend">Software</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"keys","value":"win,d"},{"type":"text","value":"urxvt"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">urxvt</button>
|
||||
<button type="button" data-msg='{"type":"keys","value":"win,d"}' class="btn btn-secondary">dmenu</button>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<p class="legend">UI</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"keys","value":"win,z"}' class="btn btn-secondary">win+z</button>
|
||||
<button type="button" data-msg='{"type":"keys","value":"win,x"}' class="btn btn-secondary">win+x</button>
|
||||
<button type="button" data-msg='{"type":"keys","value":"win,c"}' class="btn btn-secondary">win+c</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"zp"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">zp</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"zm"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">zm</button>
|
||||
</div>
|
||||
<div class="line col-12"></div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"keys","value":"win,d"},{"type":"text","value":"no-screensaver"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">no-screensaver[on]</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"keys","value":"win,d"},{"type":"text","value":"pkill no-screensaver"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">no-screensaver[off]</button>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<p class="legend">Movie</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"v;mll"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">v;mll</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"rt -l"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">rt -l</button>
|
||||
</div>
|
||||
<div class="line col-12"></div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mug"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mug</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mup"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mup</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mug 1"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mug 1</button>
|
||||
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mup 1"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mup 1</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row pane" id="pane-pointer">
|
||||
<div class="form-group col-12">
|
||||
<input type="text" class="form-control live-text" placeholder="Live text" name="text">
|
||||
</div/>
|
||||
<div id="scrollbar"></div>
|
||||
<div id="pointer"></div>
|
||||
<div id="pointer-buttons">
|
||||
<button type="button" data-msg='{"type":"pointer","click":"left"}' class="btn btn-primary no-radius col-5"> </button><button type="button no-margin" data-msg='{"type":"pointer","click":"middle"}' class="btn btn-secondary no-radius col-2"> </button><button type="button no-margin" data-msg='{"type":"pointer","click":"right"}' class="btn btn-primary no-radius col-5"> </button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row pane" id="pane-media">
|
||||
<div class="col-12">
|
||||
<p class="legend">Spotify</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"media","value":"playpause"}' class="btn btn-secondary">Play/Pause</button>
|
||||
<button type="button" data-msg='{"type":"media","value":"next"}' class="btn btn-secondary">Next</button>
|
||||
<button type="button" data-msg='{"type":"media","value":"prev"}' class="btn btn-secondary">Previous</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="legend">Volume</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"volume","value":"0"}' class="btn btn-secondary">0%</button>
|
||||
<button type="button" data-msg='{"type":"volume","value":"25"}' class="btn btn-secondary">25%</button>
|
||||
<button type="button" data-msg='{"type":"volume","value":"50"}' class="btn btn-secondary">50%</button>
|
||||
<button type="button" data-msg='{"type":"volume","value":"75"}' class="btn btn-secondary">75%</button>
|
||||
<button type="button" data-msg='{"type":"volume","value":"100"}' class="btn btn-secondary">100%</button>
|
||||
</div>
|
||||
<div class="line col-12"></div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"volume","value":"down"}' class="btn btn-secondary">-</button>
|
||||
<button type="button" data-msg='{"type":"volume","value":"up"}' class="btn btn-secondary">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pane" id="pane-desktop">
|
||||
<div class="col-12">
|
||||
<p class="legend">Desktop</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"screenshot","quality":"hq"}' class="btn btn-sm btn-secondary">Screenshot HQ</button>
|
||||
<button type="button" data-msg='{"type":"screenshot","quality":"lq"}' class="btn btn-sm btn-secondary">Screenshot LQ</button>
|
||||
<button type="button" id="live-hq" class="btn btn-sm btn-secondary">Live HQ</button>
|
||||
<button type="button" id="live-lq" class="btn btn-sm btn-secondary">Live LQ</button>
|
||||
<div id="screenshot"><img class="btn-fullscreen" data-target="#screenshot img" src="data:image/png; base64, iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC"></div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
371
ws_controller.go
Normal file
371
ws_controller.go
Normal file
|
@ -0,0 +1,371 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/kbinani/screenshot"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/net/websocket"
|
||||
"image/jpeg"
|
||||
"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`
|
||||
}
|
||||
|
||||
type MessageResponse struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func getSimpleMessageValue(msg string) string {
|
||||
data := SimpleMessageData{}
|
||||
json.Unmarshal([]byte(msg), &data)
|
||||
|
||||
return data.Value
|
||||
}
|
||||
|
||||
func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
|
||||
value, _ := json.Marshal(r)
|
||||
websocket.Message.Send(ws, string(value))
|
||||
}
|
||||
|
||||
func wsController(c echo.Context) error {
|
||||
var actions = Actions{
|
||||
Functions: make(map[string]func(ws *websocket.Conn, msg string) error),
|
||||
}
|
||||
|
||||
actions.add("pointer", func(ws *websocket.Conn, msg string) 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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 string) 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 string) 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 string) 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 string) 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 string) 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"
|
||||
}
|
||||
|
||||
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 string) 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 string) 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 string) 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
|
||||
}
|
||||
|
||||
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 string) 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, string(msg))
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
websocket.Handler(func(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
for {
|
||||
msg := ""
|
||||
websocket.Message.Receive(ws, &msg)
|
||||
|
||||
message := Message{}
|
||||
json.Unmarshal([]byte(msg), &message)
|
||||
|
||||
if message.Type != "" && actions.has(message.Type) {
|
||||
actions.exec(message.Type, ws, msg)
|
||||
}
|
||||
}
|
||||
}).ServeHTTP(c.Response(), c.Request())
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue