Compare commits

..

1 commit

Author SHA1 Message Date
Simon Vieille
60f96104a2
[wip] add audio controller 2023-08-27 21:34:02 +02:00
36 changed files with 853 additions and 1133 deletions

View file

@ -1,41 +1,11 @@
## [Unreleased]
## v3.0.0
### Fixed
* feat: center live capture in pointer
### Changed
* refactor: change organisation
* feat: use MJPEG for live capture
## v2.1.0
### Added
* add live on pointer block
* allow to show pointer
* use input type range as volume picker
## v2.0.1
### Fixed
* remove padding on pointer buttons
* fix live and screenshot render
* fix route of manifest
## v2.0.0
### Added
* add an option to start the web server with TLS (HTTPS)
### Changed
* remove jquery
* upgrade Bootstrap
## v1.0.2
### Fixed
* close connection on ws error
## v1.0.1
### Fixed
* fix process overload: replace golang.org/x/net/websocket with github.com/gorilla/websocket
- fix process overload: replace golang.org/x/net/websocket with github.com/gorilla/websocket
## v1.0.0
### Added
* rewrite of https://gitnet.fr/deblan/remote-i3wm-ws
* add configuration file
* add authentication
- rewrite of https://gitnet.fr/deblan/remote-i3wm-ws
- add configuration file
- add authentication

View file

@ -12,12 +12,11 @@ all: build
deps:
go install github.com/GeertJohan/go.rice/rice@latest
cd cmd && rice embed-go
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)" -tags=static_build ./cmd/
CGO_ENABLED=$(CGO_ENABLED) GOARCH=amd64 GOOS=linux $(CC) $(CFLAGS) -o $(BUILD_DIR)/$(LINUX_BIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)"
watch:
test -f cmd/rice-box.go && rm cmd/rice-box.go || true
gowatch -o build/app-live-linux-amd64 -args='./config.yaml' -p ./cmd/main.go
gowatch -o build/app-live-linux-amd64 -args='./config.yaml'

View file

@ -1,4 +1,4 @@
package action
package main
import (
"github.com/gorilla/websocket"
@ -8,16 +8,16 @@ type Actions struct {
Functions map[string]func(ws *websocket.Conn, msg []byte) error
}
func (actions Actions) Add(name string, callback func(ws *websocket.Conn, msg []byte) error) {
func (actions Actions) add(name string, callback func(ws *websocket.Conn, msg []byte) error) {
actions.Functions[name] = callback
}
func (actions Actions) Has(name string) bool {
func (actions Actions) has(name string) bool {
_, exists := actions.Functions[name]
return exists
}
func (actions Actions) Exec(name string, ws *websocket.Conn, msg []byte) error {
func (actions Actions) exec(name string, ws *websocket.Conn, msg []byte) error {
return actions.Functions[name](ws, msg)
}

45
audio_controller.go Normal file
View file

@ -0,0 +1,45 @@
package main
import (
"encoding/binary"
"github.com/gordonklaus/portaudio"
"github.com/labstack/echo/v4"
"net/http"
)
func chk(err error) {
if err != nil {
panic(err)
}
}
func audioController(c echo.Context) error {
if audioIsInitialized == false {
portaudio.Initialize()
defer portaudio.Terminate()
audioBuffer = make([]float32, audioSampleRate*audioSeconds)
stream, err := portaudio.OpenDefaultStream(1, 0, audioSampleRate, len(audioBuffer), func(in []float32) {
for i := range audioBuffer {
audioBuffer[i] = in[i]
}
})
chk(err)
chk(stream.Start())
defer stream.Close()
}
flusher, _ := c.Response().Writer.(http.Flusher)
c.Response().Header().Set("Connection", "Keep-Alive")
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
c.Response().Header().Set("X-Content-Type-Options", "nosniff")
c.Response().Header().Set("Transfer-Encoding", "chunked")
c.Response().Header().Set("Content-Type", "audio/wave")
for true {
binary.Write(c.Response().Writer, binary.BigEndian, &audioBuffer)
flusher.Flush() // Trigger "chunked" encoding and send a chunk...
}
return nil
}

View file

@ -1,69 +0,0 @@
package main
import (
"crypto/subtle"
"fmt"
"net/http"
"os"
rice "github.com/GeertJohan/go.rice"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"gitnet.fr/deblan/remote-i3wm-go/internal/config"
"gitnet.fr/deblan/remote-i3wm-go/internal/handler"
)
func main() {
e := echo.New()
e.HideBanner = true
if len(os.Args) != 2 {
fmt.Errorf("Configuration required!")
os.Exit(1)
}
value, err := config.CreateConfigFromFile(os.Args[1])
if err != nil {
fmt.Printf("Configuration error:")
fmt.Printf("%+v\n", err)
os.Exit(1)
}
conf := value
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
if conf.Server.Auth.Username == "" && conf.Server.Auth.Password == "" {
return true, nil
}
isValidUsername := subtle.ConstantTimeCompare([]byte(username), []byte(conf.Server.Auth.Username)) == 1
isValidPassword := subtle.ConstantTimeCompare([]byte(password), []byte(conf.Server.Auth.Password)) == 1
if isValidUsername && isValidPassword {
return true, nil
}
return false, nil
}))
assetHandler := http.FileServer(rice.MustFindBox("../static").HTTPBox())
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
e.GET("/manifest.webmanifest", handler.ManifestHandler)
e.GET("/ws", handler.WsHandler)
e.GET("/capture", handler.CaptureHandler)
e.GET("/", func(c echo.Context) error {
return handler.HomeHandler(c, conf)
})
if conf.Server.Tls.Enable == false {
e.Logger.Fatal(e.Start(conf.Server.Listen))
} else {
e.Logger.Fatal(e.StartTLS(
conf.Server.Listen,
conf.Server.Tls.CertFile,
conf.Server.Tls.CertKeyFile,
))
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,17 +1,10 @@
package config
package main
import (
"os"
"gopkg.in/yaml.v3"
"os"
)
type TlsConfig struct {
Enable bool `yaml:"enable"`
CertFile string `yaml:"certificate"`
CertKeyFile string `yaml:"certificate_key"`
}
type ServerAuthConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
@ -20,7 +13,6 @@ type ServerAuthConfig struct {
type ServerConfig struct {
Listen string `yaml:"listen"`
Auth ServerAuthConfig `yaml:"auth"`
Tls TlsConfig `yaml:tls`
}
type RemoteItemConfigItem struct {
@ -44,7 +36,7 @@ type Config struct {
Remote []RemoteItem `yaml:"remote"`
}
func CreateConfigFromFile(file string) (Config, error) {
func createConfigFromFile(file string) (Config, error) {
data, err := os.ReadFile(file)
value := Config{}

View file

@ -3,10 +3,6 @@ server:
username: admin
password: admin
listen: 0.0.0.0:4000
tls:
enable: false
certificate: /path/to/server.crt
certificate_key: /path/to/server.key
remote:
- label: Keyboard
@ -102,5 +98,7 @@ remote:
label: Volume
- label: Desktop
items:
- type: capture
label: Capture
- type: screenshot
label: Screenshot
- type: live_video
label: Live video

1
go.mod
View file

@ -7,6 +7,7 @@ require (
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/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 // indirect

2
go.sum
View file

@ -10,6 +10,8 @@ github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 h1:VLEKvjGJYAMCXw0/3
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/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5 h1:5AlozfqaVjGYGhms2OsdUyfdJME76E6rx5MdGpjzZpc=
github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5/go.mod h1:WY8R6YKlI2ZI3UyzFk7P6yGSuS+hFwNtEzrexRyD7Es=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=

16
home_controller.go Normal file
View file

@ -0,0 +1,16 @@
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
type HomeViewParams struct {
Config Config
}
func homeController(c echo.Context) error {
return c.HTML(http.StatusOK, view("views/page/home.html", HomeViewParams{
Config: config,
}))
}

View file

@ -1,68 +0,0 @@
package handler
import (
"bytes"
"image/color"
"image/jpeg"
"math"
"net/http"
"time"
"github.com/kbinani/screenshot"
"github.com/labstack/echo/v4"
"gitnet.fr/deblan/remote-i3wm-go/internal/pointer"
)
func CaptureHandler(c echo.Context) error {
bounds := screenshot.GetDisplayBounds(0)
switch c.QueryParam("type") {
case "screenshot":
if img, err := screenshot.CaptureRect(bounds); err == nil {
var buf bytes.Buffer
jpeg.Encode(&buf, img, nil)
c.Response().Header().Set("Content-Type", "image/jpeg")
return c.Blob(http.StatusOK, "image/jpeg", buf.Bytes())
}
default:
c.Response().Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
for {
if img, err := screenshot.CaptureRect(bounds); err == nil {
var buf bytes.Buffer
if c.QueryParam("pointer") == "1" {
currentX, currentY := pointer.Positions()
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)
}
}
}
jpeg.Encode(&buf, img, nil)
_, _ = c.Response().Write([]byte("--frame\r\n"))
c.Response().Write([]byte("Content-Type: image/jpeg\r\n\r\n"))
c.Response().Write(buf.Bytes())
c.Response().Write([]byte("\r\n"))
time.Sleep(33 * time.Millisecond)
}
}
}
return nil
}

View file

@ -1,22 +0,0 @@
package handler
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"gitnet.fr/deblan/remote-i3wm-go/internal/config"
"gitnet.fr/deblan/remote-i3wm-go/internal/render"
)
type HomeViewParams struct {
Config config.Config
Now time.Time
}
func HomeHandler(c echo.Context, conf config.Config) error {
return c.HTML(http.StatusOK, render.View("page/home.html", HomeViewParams{
Config: conf,
Now: time.Now(),
}))
}

View file

@ -1,47 +0,0 @@
package handler
import (
"net/http"
"github.com/labstack/echo/v4"
)
type ManifestIcon struct {
Src string `json:"src"`
Type string `json:"type"`
Sizes string `json:"sizes"`
}
type Manifest struct {
ShortName string `json:"short_name"`
Name string `json:"name"`
ThemeColor string `json:"theme_color"`
BackgroundColor string `json:"background_color"`
Display string `json:"display"`
Orientation string `json:"orientation"`
Scope string `json:"scope"`
StartUrl string `json:"start_url"`
Icons []ManifestIcon `json:"icons"`
}
func ManifestHandler(c echo.Context) error {
manifest := &Manifest{
ShortName: "RWM",
Name: "Remote i3WM",
ThemeColor: "#1e3650",
BackgroundColor: "#ffffff",
Display: "standalone",
Orientation: "portrait-primary",
Scope: "/",
StartUrl: "/",
Icons: []ManifestIcon{
ManifestIcon{
Src: "/static/img/icon.png",
Type: "image/png",
Sizes: "96x96",
},
},
}
return c.JSONPretty(http.StatusOK, manifest, " ")
}

View file

@ -1,25 +0,0 @@
package pointer
import (
"os/exec"
"strconv"
"strings"
)
func Positions() (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
}

View file

@ -1,17 +0,0 @@
package render
import (
"bytes"
"html/template"
"gitnet.fr/deblan/remote-i3wm-go/templates"
)
func View(viewName string, data any) string {
var render bytes.Buffer
view := template.Must(template.ParseFS(templates.Views, viewName, "layout/base.html"))
view.Execute(&render, data)
return render.String()
}

75
main.go Normal file
View file

@ -0,0 +1,75 @@
package main
import (
"crypto/subtle"
"embed"
"fmt"
rice "github.com/GeertJohan/go.rice"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"html/template"
"net/http"
"os"
)
const (
audioSampleRate = 44100
audioSeconds = 2
)
var (
templates map[string]*template.Template
//go:embed static
staticFiles embed.FS
//go:embed views/layout views/page
views embed.FS
config Config
actions Actions
audioBuffer []float32
audioIsInitialized bool
)
func main() {
e := echo.New()
e.HideBanner = true
if len(os.Args) != 2 {
fmt.Errorf("Configuration required!")
os.Exit(1)
}
value, err := createConfigFromFile(os.Args[1])
if err != nil {
fmt.Printf("Configuration error:")
fmt.Printf("%+v\n", err)
os.Exit(1)
}
config = value
audioIsInitialized = false
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
if config.Server.Auth.Username == "" && config.Server.Auth.Password == "" {
return true, nil
}
isValidUsername := subtle.ConstantTimeCompare([]byte(username), []byte(config.Server.Auth.Username)) == 1
isValidPassword := subtle.ConstantTimeCompare([]byte(password), []byte(config.Server.Auth.Password)) == 1
if isValidUsername && isValidPassword {
return true, nil
}
return false, nil
}))
assetHandler := http.FileServer(rice.MustFindBox("static").HTTPBox())
actions = createActions()
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
e.GET("/", homeController)
e.GET("/ws", wsController)
e.GET("/audio", audioController)
e.Logger.Fatal(e.Start(config.Server.Listen))
}

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,176 +0,0 @@
:root {
--link-color: #1e3650;
--bs-link-color: var(--link-color);
}
* {
overscroll-behavior: contain !important;
}
a {
color: var(--link-color);
}
.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 - 33px);
bottom: 33px;
margin: auto;
background: #ccc;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
position: absolute;
width: calc(100% - 50px);
-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 - 33px);
width: 50px;
background: #333;
position: fixed;
bottom: 33px;
z-index: 100;
right: 0;
}
#mouse {
position: fixed;
bottom: 83px;
z-index: 200;
width: calc(100vw - 50px);
padding: 10px;
background: #fff;
}
#pane-pointer .form-group {
padding: 0;
margin: 0;
}
#pointer-buttons {
margin-top: -42px;
width: 100%;
z-index: 110;
position: fixed;
bottom: 31px;
padding-left: 0;
padding-right: 0;
}
#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-top: 2px solid #1e3650;
position: fixed;
bottom: 0;
width: 100%;
height: 33px;
}
#shortcuts_special_keys input {
display: none;
}
#response {
position: absolute;
top: 0;
width: 100%;
color: #fff;
background: #748c26;
padding: 5px;
display: none;
}
.capture-img img {
max-width: 100%;
margin-top: 10px;
cursor: pointer;
}
#mouse-screenshot-live {
display: inline-block;
width: 80px;
padding-left: 5px;
}
#mouse-text-live {
display: inline-block;
width: calc(100% - 100px);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

4
static/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,400 +0,0 @@
let ws
let pointer, scroller, response, screenshotImg
let scrollLastTimestamp, scrollLastValue
let mousePosX, mousePosY, mouseInitPosX, mouseInitPosY
let isLive = false
let wsLock = false
let isPointerLive = false
let isScreenshotWaiting = null
let isPointerScreenshotWaiting = false
let emptyImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC"
function createWebSocketConnection() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
ws = new WebSocket(`${protocol}://${window.location.hostname}:${window.location.port}/ws`)
ws.addEventListener('open', function(event) {
document.querySelector('#disconneced').style.display = 'none'
unLock()
})
ws.addEventListener('close', function(event) {
unLock()
document.querySelector('#disconneced').style.display = 'block'
window.setTimeout(createWebSocketConnection, 5000)
})
ws.addEventListener('message', function(event) {
unLock()
let data = JSON.parse(event.data)
if (data.type === 'response') {
response.innerText = data.value
response.style.display = 'block'
window.setTimeout(function() {
response.style.display = 'none'
}, 2500)
return
}
if (data.type === 'volume') {
if (data.value.length) {
setVolume(parseInt(data.value))
}
return
}
})
}
function isLocked() {
return wsLock === true
}
function lock() {
wsLock = true
}
function unLock() {
wsLock = false
}
function send(message) {
if (isLocked()) {
return
}
lock()
ws.send(message)
}
function navigationClickHandler(e) {
if (e.target.getAttribute('href') === '#') {
return
}
Array.from(document.querySelectorAll('.pane')).forEach((item) => {
item.style.display = 'none'
})
document.querySelector(e.target.getAttribute('href')).style.display = 'block'
Array.from(document.querySelectorAll('#nav a')).forEach((item) => {
item.classList.remove('active')
})
e.target.classList.add('active')
}
function buttonClickHandler(e) {
send(e.target.getAttribute('data-msg'))
}
function shortcutClearClickHandler(e) {
document.querySelector('#shortcut-key').value = ''
Array.from(document.querySelectorAll('#shortcuts_special_keys input:checked')).forEach((item) => {
item.checked = false
item.change()
})
}
function shortcutSendClickHandler(e) {
let keys = []
let key = document.querySelector('#shortcut-key').value
Array.from(document.querySelectorAll('#shortcuts_special_keys input:checked')).forEach((item) => {
keys.push(item.value)
})
if (keys.length) {
if (key) {
keys.push(key)
}
send('{"type":"keys","value": "' + (keys.join(',').replace('"', '\\"')) + '"}')
}
}
function textClearClickHandler(e) {
document.querySelector('#text').value = ''
}
function textSendClickHandler(e) {
const keys = document.querySelector('#text').value
if (keys.length) {
send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function textKeyUpHandler(e) {
const keys = document.querySelector('#text').value
if (e.keyCode === 13) {
send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function liveTextKeyUpHandler(e) {
const value = e.target.value
const size = value.length
const messages = []
if (size > 0) {
messages.push('{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}')
if (value[size-1] === ' ') {
messages.push('{"type":"key","value":"space"}')
}
}
if (e.keyCode === 8) {
messages.push('{"type":"key","value": "backspace"}')
} else if (e.keyCode === 13 && size === 0) {
messages.push('{"type":"key","value": "enter"}')
}
send(`{"type":"messages","value":[${messages.join(',')}]}`)
e.target.value = ''
}
function shortcutsSpecialKeysOnChangeHandler(e) {
Array.from(document.querySelectorAll('#shortcuts_special_keys input:checked')).forEach((item) => {
item.parentNode.classList.add('btn-primary')
item.parentNode.classList.remove('btn-secondary')
})
Array.from(document.querySelectorAll('#shortcuts_special_keys input:not(:checked)')).forEach((item) => {
item.parentNode.classList.add('btn-secondary')
item.parentNode.classList.remove('btn-primary')
})
}
function pointerClickHandler(e) {
send('{"type":"pointer","click":"left"}')
}
function scrollerTouchStartHandler(e) {
mouseInitPosY = e.targetTouches[0].pageY
}
function scrollerTouchMoveHandler(e) {
let touch = e.changedTouches[0]
let value = ((touch.pageY - mouseInitPosY > 0) ? 'down' : 'up')
let now = new Date().getTime()
if (touch.pageY === mouseInitPosY || value === scrollLastValue && scrollLastTimestamp !== null && now - scrollLastTimestamp < 200) {
return
}
scrollLastTimestamp = now
scrollLastValue = value
mouseInitPosY = touch.pageY
send('{"type":"scroll","value": "' + value + '"}')
}
function pointerTouchStartHandler(e) {
const touch = e.targetTouches[0]
mouseInitPosX = touch.pageX
mouseInitPosY = touch.pageY
}
function pointerLiveHandler(e) {
if (!e.target.checked) {
pointer.style.backgroundImage = ""
} else {
pointer.style.backgroundImage = `url("/capture?type=live&pointer=1&${Math.random()}")`
}
}
function pointerTouchMoveHandler(e) {
if (e.changedTouches.length === 2) {
return scrollerTouchMoveHandler(e)
}
const touch = e.changedTouches[0]
mousePosX = touch.pageX
mousePosY = touch.pageY
const newX = mousePosX - mouseInitPosX
const newY = mousePosY - mouseInitPosY
mouseInitPosX = mousePosX
mouseInitPosY = mousePosY
let msg = '{"type":"pointer","x": "' + newX + '","y": "' + newY + '"}'
ws.send(msg)
}
function capture(mode) {
}
function captureScreenshotClickHandler(e) {
const img = e.target.parentNode.querySelector('.capture-img img')
img.src = "/capture?type=screenshot&" + Math.random()
}
function captureLiveClickHandler(e) {
const img = e.target.parentNode.querySelector('.capture-img img')
if (img.src.indexOf("live") > -1) {
img.src = emptyImg
} else {
img.src = "/capture?type=live&" + Math.random()
}
}
function fullscreenHandler(e) {
const targetConf = e.target.getAttribute('data-target')
const isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
const element = (targetConf === 'this')
? e.target
: document.querySelector(targetConf)
document.querySelector('body').classList.toggle('fullscreen', isFullscreen)
if (isFullscreen) {
e.target.setAttribute('data-fullscreen', '0')
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
}
} else {
e.target.setAttribute('data-fullscreen', '1')
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
}
}
}
function documentHashHandler() {
const hash = window.location.hash
if (hash) {
document.querySelector('a[href="' + hash + '"]').click()
} else {
document.querySelector('#nav > li:first-child a').click()
}
}
function addEventListenerOn(selector, eventName, listener) {
if (typeof selector === 'string') {
Array.from(document.querySelectorAll(selector)).forEach((element) => {
element.addEventListener(eventName, listener)
})
} else {
selector.addEventListener(eventName, listener)
}
}
function addListeners() {
addEventListenerOn('#nav a', 'click', navigationClickHandler)
addEventListenerOn('button[data-msg]', 'click', buttonClickHandler)
addEventListenerOn('#shortcut-clear', 'click', shortcutClearClickHandler)
addEventListenerOn('#shortcuts_special_keys input', 'change', shortcutsSpecialKeysOnChangeHandler)
addEventListenerOn('#shortcut-send', 'click', shortcutSendClickHandler)
addEventListenerOn('#text-clear', 'click', textClearClickHandler)
addEventListenerOn('#text-send', 'click', textSendClickHandler)
addEventListenerOn('#text', 'keyup', textKeyUpHandler)
Array.from(document.querySelectorAll('.live-text')).forEach((element) => {
element.setAttribute('data-composing', '0')
addEventListenerOn(element, 'compositionstart', (e) => {
element.setAttribute('data-composing', '1')
})
addEventListenerOn(element, 'compositionend', (e) => {
element.setAttribute('data-composing', '0')
})
addEventListenerOn(element, 'keyup', (e) => {
if (element.getAttribute('data-composing') === '1') {
return
}
liveTextKeyUpHandler(e)
})
})
addEventListenerOn(scroller, 'touchstart', scrollerTouchStartHandler)
addEventListenerOn(scroller, 'touchmove', scrollerTouchMoveHandler)
addEventListenerOn(pointer, 'click', pointerClickHandler)
addEventListenerOn(pointer, 'touchstart', pointerTouchStartHandler)
addEventListenerOn(pointer, 'touchmove', pointerTouchMoveHandler)
addEventListenerOn('#mouse-screenshot-live input', 'change', pointerLiveHandler)
addEventListenerOn('.capture-live', 'click', captureLiveClickHandler)
addEventListenerOn('.capture-screenshot', 'click', captureScreenshotClickHandler)
addEventListenerOn('.btn-fullscreen', 'click', fullscreenHandler)
}
function getVolume() {
if (document.querySelectorAll('.volume').length) {
try {
send('{"type":"volume","value":"value"}')
} catch (e) {
}
document.querySelectorAll('.volume input[type="range"]').forEach(function(input) {
if (input.getAttribute('data-event')) {
return
}
input.setAttribute('data-event', 'ok')
input.addEventListener('change', (e) => {
send(`{"type":"volume","value":"${e.target.value}"}`)
})
})
}
window.setTimeout(getVolume, 2000)
}
function setVolume(value) {
document.querySelectorAll('.volume').forEach(function(item) {
item.querySelector('input[type="range"]').value = value
})
}
function bootstrap() {
pointer = document.querySelector('#pointer')
scroller = document.querySelector('#scrollbar')
response = document.querySelector('#response')
screenshotImg = document.querySelector('#screenshot img')
shortcutsSpecialKeysOnChangeHandler()
createWebSocketConnection()
addListeners()
documentHashHandler()
getVolume()
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/js/service_worker.js')
}
}
addEventListenerOn(window, 'DOMContentLoaded', bootstrap)

View file

@ -1,3 +0,0 @@
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
});

137
static/main.css Normal file
View file

@ -0,0 +1,137 @@
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 - 33px - 38px);
top: calc(33px + 38px);
margin: auto;
background: #ccc;
position: absolute;
width: 100%;
-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;
}
#pane-pointer .form-group {
padding: 0;
margin: 0;
}
#pointer-buttons {
margin-top: -42px;
width: 100%;
z-index: 110;
position: fixed;
bottom: 0;
}
#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;
}

308
static/main.js Normal file
View file

@ -0,0 +1,308 @@
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) {
$('a[href="' + hash + '"]').click();
} else {
$('#nav > li:first-child a').click();
}
}
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() {
shortcutsSpecialKeysOnChangeHandler();
createWebSocketConnection();
addListeners();
documentHashHandler();
}
$(function() {
$pointer = $('#pointer');
$scroller = $('#scrollbar');
$response = $('#response');
$screenshotImg = $('#screenshot img');
bootstrap();
});

View file

@ -1,8 +0,0 @@
package static
import "embed"
var (
//go:embed css/* img/* js/*
Files embed.FS
)

View file

@ -1,25 +0,0 @@
{{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/css/bootstrap.min.css?t={{.Now.Format "2006-01-02 15:04:05"}}" type="text/css">
<link rel="stylesheet" href="/static/css/main.css?t={{.Now.Format "2006-01-02 15:04:05"}}" type="text/css">
<link rel="manifest" href="/manifest.webmanifest?t={{.Now.Format "2006-01-02 15:04:05"}}">
<link rel="icon" type="image/png" href="/static/img/icon.png?t={{.Now.Format "2006-01-02 15:04:05"}}">
<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/js/main.js?t={{.Now.Format "2006-01-02 15:04:05"}}"></script>
</body>
</html>
{{end}}

View file

@ -1,8 +0,0 @@
package templates
import "embed"
var (
//go:embed layout page
Views embed.FS
)

20
utils.go Normal file
View 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
View 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}}

View file

@ -30,8 +30,7 @@
{{if eq $value.Type "live_text"}}
<div class="form-group col-12">
<input type="text" class="form-control live-text" name="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
<pre></pre>
<input type="text" class="form-control live-text" name="text">
</div>
{{end}}
@ -63,23 +62,19 @@
{{if eq $value.Type "shortcuts"}}
<div class="col-9" id="shortcuts_special_keys">
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_ctrl">
<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 mb-1" for="shortcuts_special_key_shift">
<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 mb-1" for="shortcuts_special_key_alt">
<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 mb-1" for="shortcuts_special_key_tab">
<input type="checkbox" value="tab" id="shortcuts_special_key_tab">
tab
</label>
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_win">
<label class="btn btn-secondary" for="shortcuts_special_key_win">
<input type="checkbox" value="win" id="shortcuts_special_key_win">
win
</label>
@ -102,21 +97,23 @@
{{end}}
{{if eq $value.Type "volume"}}
<div class="col-12 volume">
<input type="range" min="0" max="100" value="50" class="form-range">
<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>
{{end}}
{{if eq $value.Type "mouse"}}
<div class="form-group col-12" id="mouse">
<div id="mouse-screenshot-live">
<label>
<input type="checkbox"> Screen
</label>
</div>
<input type="text" id="mouse-text-live" class="form-control live-text" placeholder="Live text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
<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>
@ -125,17 +122,18 @@
</div>
{{end}}
{{if eq $value.Type "capture"}}
{{if eq $value.Type "screenshot"}}
<div class="col-12">
<button type="button" class="btn btn-secondary capture-screenshot">
Screenshot
</button>
<button type="button" class="btn btn-secondary capture-live">
Live
</button>
<div class="capture-img">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC" class="btn-fullscreen" data-target="this">
</div>
<button type="button" data-msg='{"type":"screenshot","quality":"hq"}' class="btn btn-secondary">Screenshot HQ</button>
<button type="button" data-msg='{"type":"screenshot","quality":"lq"}' class="btn btn-secondary">Screenshot LQ</button>
</div>
{{end}}
{{if eq $value.Type "live_video"}}
<div class="col-12">
<button type="button" id="live-hq" class="btn btn-secondary">Live HQ</button>
<button type="button" id="live-lq" class="btn 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>
{{end}}

View file

@ -1,19 +1,18 @@
package handler
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/websocket"
"github.com/kbinani/screenshot"
"github.com/labstack/echo/v4"
"image/jpeg"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"gitnet.fr/deblan/remote-i3wm-go/internal/action"
"gitnet.fr/deblan/remote-i3wm-go/internal/pointer"
)
type Message struct {
@ -38,7 +37,6 @@ type MessagesData struct {
type ScreenshotMessageData struct {
Quality string `json:quality`
Pointer bool `json:pointer`
}
type MessageResponse struct {
@ -58,12 +56,12 @@ func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
ws.WriteMessage(websocket.TextMessage, value)
}
func createActions() action.Actions {
actions := action.Actions{
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 {
actions.add("pointer", func(ws *websocket.Conn, msg []byte) error {
data := PointerMessageData{}
json.Unmarshal([]byte(msg), &data)
@ -85,7 +83,19 @@ func createActions() action.Actions {
return cmd.Run()
}
currentX, currentY := pointer.Positions()
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)
@ -98,7 +108,7 @@ func createActions() action.Actions {
return cmd.Run()
})
actions.Add("scroll", func(ws *websocket.Conn, msg []byte) error {
actions.add("scroll", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
key := ""
@ -118,7 +128,7 @@ func createActions() action.Actions {
return nil
})
actions.Add("workspace", func(ws *websocket.Conn, msg []byte) error {
actions.add("workspace", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -130,27 +140,11 @@ func createActions() action.Actions {
return cmd.Run()
})
actions.Add("volume", func(ws *websocket.Conn, msg []byte) error {
actions.add("volume", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "value" {
cmd := exec.Command("amixer", "get", "Master")
output, _ := cmd.Output()
r := regexp.MustCompile(`\[([0-9]+%)\]`)
value := string(r.Find(output))
value = strings.Replace(value, "[", "", 1)
value = strings.Replace(value, "]", "", 1)
value = strings.Replace(value, "%", "", 1)
if value != "" {
sendMessageResponse(ws, MessageResponse{
Type: "volume",
Value: value,
})
}
return nil
if value == "" {
return errors.New("Invalid value")
}
if value == "up" {
@ -183,7 +177,7 @@ func createActions() action.Actions {
return cmd.Run()
})
actions.Add("media", func(ws *websocket.Conn, msg []byte) error {
actions.add("media", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -243,7 +237,7 @@ func createActions() action.Actions {
return nil
})
actions.Add("keys", func(ws *websocket.Conn, msg []byte) error {
actions.add("keys", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
if value == "" {
@ -259,8 +253,6 @@ func createActions() action.Actions {
key = "Control_L"
} else if key == "alt" {
key = "Alt_L"
} else if key == "tab" {
key = "Tab"
}
if key != "" {
@ -277,7 +269,7 @@ func createActions() action.Actions {
return cmd.Run()
})
actions.Add("key", func(ws *websocket.Conn, msg []byte) error {
actions.add("key", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
keys := make(map[string]string)
@ -302,7 +294,7 @@ func createActions() action.Actions {
return cmd.Run()
})
actions.Add("text", func(ws *websocket.Conn, msg []byte) error {
actions.add("text", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
if value == "" {
@ -314,15 +306,45 @@ func createActions() action.Actions {
return cmd.Run()
})
actions.Add("messages", func(ws *websocket.Conn, msg []byte) error {
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
}
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)
if actions.has(value.Type) {
actions.exec(value.Type, ws, msg)
time.Sleep(400 * time.Millisecond)
}
}
@ -335,10 +357,9 @@ func createActions() action.Actions {
var (
upgrader = websocket.Upgrader{}
actions = createActions()
)
func WsHandler(c echo.Context) error {
func wsController(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
@ -350,22 +371,15 @@ func WsHandler(c echo.Context) error {
_, 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)
if message.Type != "" && actions.has(message.Type) {
actions.exec(message.Type, ws, msg)
}
sendMessageResponse(ws, MessageResponse{
Type: "statement",
Value: "end",
})
}
return nil