Compare commits

...

25 commits
v1.0.0 ... main

Author SHA1 Message Date
Simon Vieille 7cf77935b3
update go version for build 2024-04-05 10:09:30 +02:00
Simon Vieille 451717ddac
update changelog 2023-12-10 16:27:40 +01:00
Simon Vieille ae53fe1ab7
add live on pointer block
allow to show pointer
2023-12-10 16:26:53 +01:00
Simon Vieille 5aab502a76
update changelog 2023-12-06 17:56:53 +01:00
Simon Vieille e3cbf3eda2
change manifest url 2023-12-06 17:54:32 +01:00
Simon Vieille ff5a66ad07
fix screenshots 2023-12-06 17:53:48 +01:00
Simon Vieille a07f14765a
remove padding en pointer buttons 2023-12-06 17:53:16 +01:00
Simon Vieille 233d1a4d96
move manifest in controller 2023-11-20 14:40:10 +01:00
Simon Vieille a9601c479d
update changelog 2023-11-17 20:20:32 +01:00
Simon Vieille e65fccee03 Merge branch 'feature/refactoring' into develop 2023-11-17 20:17:30 +01:00
Simon Vieille 73b9f98297
fix nav link colors 2023-11-17 20:17:26 +01:00
Simon Vieille fc8e74c772
upgrade boostrap from v4 to v5 2023-11-17 20:13:32 +01:00
Simon Vieille 253073f15f
fix js 2023-11-17 20:10:38 +01:00
Simon Vieille c865080df1
move js and css to specific directories 2023-11-17 20:05:50 +01:00
Simon Vieille 5f18c04d4d
remove jquery 2023-11-17 20:03:42 +01:00
Simon Vieille e6934dd9aa Merge branch 'feature/pwa' into develop 2023-11-17 19:02:58 +01:00
Simon Vieille 71ce3bb90d
add tls option 2023-11-17 18:50:03 +01:00
Simon Vieille 3533ea4dfe
add minimal info for PWA 2023-11-17 18:25:26 +01:00
Simon Vieille dd24fda35b
update changelog 2023-09-10 19:37:02 +02:00
Simon Vieille 5e76b76c31
close connection on ws error 2023-09-10 19:35:43 +02:00
Simon Vieille d80395fd6e
update changelog 2023-08-27 17:55:58 +02:00
Simon Vieille 676e11db1c
replace golang.org/x/net/websocket with github.com/gorilla/websocket 2023-08-27 17:54:31 +02:00
Simon Vieille 6d6bfee742
add specific function to create actions 2023-08-27 16:56:43 +02:00
Simon Vieille f335eaf792
update readme 2023-08-26 13:20:51 +02:00
Simon Vieille 6a2670b39c
update readme 2023-08-26 13:20:25 +02:00
23 changed files with 664 additions and 442 deletions

View file

@ -1,7 +1,32 @@
## [Unreleased]
## v1.0.0
### Added
* add live on pointer block
* allow to show pointer
- rewrite of https://gitnet.fr/deblan/remote-i3wm-ws
- add configuration file
- add authentication
## 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
## v1.0.0
### Added
* rewrite of https://gitnet.fr/deblan/remote-i3wm-ws
* add configuration file
* add authentication

View file

@ -25,6 +25,6 @@ Download the latest binary from [releases](https://gitnet.fr/deblan/remote-i3wm-
Create a YAML file from [the model](https://gitnet.fr/deblan/remote-i3wm-go/src/branch/main/config.yaml) and then customize it!
Run the server: `/path/to/app-latest-linux-amd64 /path/to/config.yaml`.
Run the server: `./app-linux-amd64 ./config.yaml`.
Now you can access the web interface on http://127.0.0.1:4000.

View file

@ -1,14 +1,14 @@
package main
import (
"golang.org/x/net/websocket"
"github.com/gorilla/websocket"
)
type Actions struct {
Functions map[string]func(ws *websocket.Conn, msg string) error
Functions map[string]func(ws *websocket.Conn, msg []byte) error
}
func (actions Actions) add(name string, callback func(ws *websocket.Conn, msg string) error) {
func (actions Actions) add(name string, callback func(ws *websocket.Conn, msg []byte) error) {
actions.Functions[name] = callback
}
@ -18,6 +18,6 @@ func (actions Actions) has(name string) bool {
return exists
}
func (actions Actions) exec(name string, ws *websocket.Conn, msg string) error {
func (actions Actions) exec(name string, ws *websocket.Conn, msg []byte) error {
return actions.Functions[name](ws, msg)
}

View file

@ -1,10 +1,17 @@
package main
import (
"gopkg.in/yaml.v3"
"os"
"gopkg.in/yaml.v3"
)
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"`
@ -13,6 +20,7 @@ type ServerAuthConfig struct {
type ServerConfig struct {
Listen string `yaml:"listen"`
Auth ServerAuthConfig `yaml:"auth"`
Tls TlsConfig `yaml:tls`
}
type RemoteItemConfigItem struct {

View file

@ -3,6 +3,10 @@ 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

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/gorilla/websocket v1.5.0 // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 // indirect
github.com/labstack/echo/v4 v4.11.1 // 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/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=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=

17
main.go
View file

@ -17,8 +17,9 @@ var (
//go:embed static
staticFiles embed.FS
//go:embed views/layout views/page
views embed.FS
config Config
views embed.FS
config Config
actions Actions
)
func main() {
@ -55,10 +56,20 @@ func main() {
}))
assetHandler := http.FileServer(rice.MustFindBox("static").HTTPBox())
actions = createActions()
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
e.GET("/manifest.webmanifest", manifestController)
e.GET("/", homeController)
e.GET("/ws", wsController)
e.Logger.Fatal(e.Start(config.Server.Listen))
if config.Server.Tls.Enable == false {
e.Logger.Fatal(e.Start(config.Server.Listen))
} else {
e.Logger.Fatal(e.StartTLS(
config.Server.Listen,
config.Server.Tls.CertFile,
config.Server.Tls.CertKeyFile,
))
}
}

46
manifest_controller.go Normal file
View file

@ -0,0 +1,46 @@
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
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 manifestController(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, " ")
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,10 @@
:root {
--link-color: #1e3650;
--bs-link-color: var(--link-color);
}
a {
color: #1e3650;
color: var(--link-color);
}
.btn-primary {
@ -62,8 +67,10 @@ a {
top: calc(33px + 38px);
margin: auto;
background: #ccc;
background-size: contain;
background-repeat: no-repeat;
position: absolute;
width: 100%;
width: calc(100% - 50px);
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@ -92,6 +99,8 @@ a {
z-index: 110;
position: fixed;
bottom: 0;
padding-left: 0;
padding-right: 0;
}
#pointer-buttons .btn {
@ -135,3 +144,14 @@ a {
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);
}

BIN
static/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

353
static/js/main.js Normal file
View file

@ -0,0 +1,353 @@
let ws
let pointer, scroller, response, screenshotImg
let scrollLastTimestamp, scrollLastValue
let mousePosX, mousePosY, mouseInitPosX, mouseInitPosY
let isLive = false
let isPointerLive = false
let isScreenshotWaiting = false
let isPointerScreenshotWaiting = false
function createWebSocketConnection() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
ws = new WebSocket(`${protocol}://${window.location.hostname}:${window.location.port}/ws`)
ws.onopen = function(event) {
document.querySelector('#disconneced').style.display = 'none'
}
ws.onclose = function(event) {
document.querySelector('#disconneced').style.display = 'block'
window.setTimeout(createWebSocketConnection, 5000)
}
ws.onmessage = function(event) {
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)
} else if (data.type === 'screenshot') {
if (isScreenshotWaiting) {
isScreenshotWaiting = false
screenshotImg.setAttribute('src', 'data:image/png;base64, ' + data.value)
}
let pointer = document.querySelector('#pointer')
if (isPointerScreenshotWaiting) {
pointer.style.backgroundImage = `url('data:image/png;base64, ${data.value}')`
isPointerScreenshotWaiting = false
} else {
pointer.style.backgroundImage = 'none'
}
}
}
}
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) {
ws.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)
}
ws.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) {
ws.send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function textKeyUpHandler(e) {
const keys = document.querySelector('#text').value
if (e.keyCode === 13) {
ws.send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function liveTextKeyUpHandler(e) {
const value = e.target.value
if (e.keyCode === 8) {
ws.send('{"type":"key","value": "backspace"}')
} else if (e.keyCode === 13) {
ws.send('{"type":"key","value": "enter"}')
} else if (value.length) {
if (value === ' ') {
ws.send('{"type":"key","value": "space"}')
} else {
ws.send('{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}')
}
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) {
ws.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
ws.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) {
isPointerLive = false
isPointerScreenshotWaiting = false
return
}
isPointerLive = true
let doScreenshot = function() {
if (isPointerLive) {
if (!isPointerScreenshotWaiting) {
isPointerScreenshotWaiting = true
ws.send(`{"type":"screenshot","quality":"lq","pointer":true}`)
}
window.setTimeout(doScreenshot, 300)
}
}
doScreenshot()
}
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 liveHqClickHandler(e) {
return liveClickHandler(e, 'hq')
}
function liveLqClickHandler(e) {
return liveClickHandler(e, 'lq')
}
function liveClickHandler(e, quality) {
if (isLive) {
isLive = false
isScreenshotWaiting = false
document.querySelector('#live-hq').innerText = 'Live HQ'
document.querySelector('#live-lq').innerText = 'Live LQ'
return
}
isLive = true
e.target.innerText = 'Stop live'
let doScreenshot = function() {
if (isLive) {
if (!isScreenshotWaiting) {
isScreenshotWaiting = true
ws.send(`{"type":"screenshot","quality":"${quality}"}`)
}
window.setTimeout(doScreenshot, 100)
}
}
doScreenshot()
}
function fullscreenHandler(e) {
let element = document.querySelector(e.target.getAttribute('data-target'))
let isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
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)
addEventListenerOn('.live-text', 'keyup', liveTextKeyUpHandler)
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('#live-hq', 'click', liveHqClickHandler)
addEventListenerOn('#live-lq', 'click', liveLqClickHandler)
addEventListenerOn('.btn-fullscreen', 'click', fullscreenHandler)
}
function bootstrap() {
pointer = document.querySelector('#pointer')
scroller = document.querySelector('#scrollbar')
response = document.querySelector('#response')
screenshotImg = document.querySelector('#screenshot img')
shortcutsSpecialKeysOnChangeHandler()
createWebSocketConnection()
addListeners()
documentHashHandler()
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/js/service_worker.js')
}
}
addEventListenerOn(window, 'DOMContentLoaded', bootstrap)

View file

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

View file

@ -1,308 +0,0 @@
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

@ -4,8 +4,10 @@
<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">
<link rel="stylesheet" href="/static/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="/static/css/main.css" type="text/css">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" type="image/png" href="/static/img/icon.png">
<title>Remote i3-wm</title>
</head>
<body>
@ -17,10 +19,7 @@
<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>
<script src="/static/js/main.js"></script>
</body>
</html>
{{end}}

View file

@ -62,19 +62,23 @@
{{if eq $value.Type "shortcuts"}}
<div class="col-9" id="shortcuts_special_keys">
<label class="btn btn-secondary" for="shortcuts_special_key_ctrl">
<label class="btn btn-secondary mb-1" 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">
<label class="btn btn-secondary mb-1" 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">
<label class="btn btn-secondary mb-1" 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">
<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">
<input type="checkbox" value="win" id="shortcuts_special_key_win">
win
</label>
@ -112,8 +116,13 @@
{{end}}
{{if eq $value.Type "mouse"}}
<div class="form-group col-12">
<input type="text" class="form-control live-text" placeholder="Live text" name="text">
<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">
</div/>
<div id="scrollbar"></div>
<div id="pointer"></div>

View file

@ -5,10 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/websocket"
"github.com/kbinani/screenshot"
"github.com/labstack/echo/v4"
"golang.org/x/net/websocket"
"image/color"
"image/jpeg"
"math"
"os/exec"
"strconv"
"strings"
@ -37,6 +39,7 @@ type MessagesData struct {
type ScreenshotMessageData struct {
Quality string `json:quality`
Pointer bool `json:pointer`
}
type MessageResponse struct {
@ -44,7 +47,7 @@ type MessageResponse struct {
Value string `json:"value"`
}
func getSimpleMessageValue(msg string) string {
func getSimpleMessageValue(msg []byte) string {
data := SimpleMessageData{}
json.Unmarshal([]byte(msg), &data)
@ -53,15 +56,33 @@ func getSimpleMessageValue(msg string) string {
func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
value, _ := json.Marshal(r)
websocket.Message.Send(ws, string(value))
ws.WriteMessage(websocket.TextMessage, value)
}
func wsController(c echo.Context) error {
var actions = Actions{
Functions: make(map[string]func(ws *websocket.Conn, msg string) error),
func getPointerPosition() (float64, float64) {
location := exec.Command("xdotool", "getmouselocation")
output, _ := location.Output()
position := string(output)
currentX := 0.0
currentY := 0.0
for key, value := range strings.Split(position, " ") {
if key == 0 {
currentX, _ = strconv.ParseFloat(strings.Replace(value, "x:", "", 1), 32)
} else if key == 1 {
currentY, _ = strconv.ParseFloat(strings.Replace(value, "y:", "", 1), 32)
}
}
actions.add("pointer", func(ws *websocket.Conn, msg string) error {
return currentX, currentY
}
func createActions() Actions {
actions := Actions{
Functions: make(map[string]func(ws *websocket.Conn, msg []byte) error),
}
actions.add("pointer", func(ws *websocket.Conn, msg []byte) error {
data := PointerMessageData{}
json.Unmarshal([]byte(msg), &data)
@ -83,19 +104,7 @@ func wsController(c echo.Context) error {
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)
}
}
currentX, currentY := getPointerPosition()
newX, _ := strconv.ParseFloat(data.X, 32)
newY, _ := strconv.ParseFloat(data.Y, 32)
@ -108,7 +117,7 @@ func wsController(c echo.Context) error {
return cmd.Run()
})
actions.add("scroll", func(ws *websocket.Conn, msg string) error {
actions.add("scroll", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
key := ""
@ -128,7 +137,7 @@ func wsController(c echo.Context) error {
return nil
})
actions.add("workspace", func(ws *websocket.Conn, msg string) error {
actions.add("workspace", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -140,7 +149,7 @@ func wsController(c echo.Context) error {
return cmd.Run()
})
actions.add("volume", func(ws *websocket.Conn, msg string) error {
actions.add("volume", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -177,7 +186,7 @@ func wsController(c echo.Context) error {
return cmd.Run()
})
actions.add("media", func(ws *websocket.Conn, msg string) error {
actions.add("media", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -237,7 +246,7 @@ func wsController(c echo.Context) error {
return nil
})
actions.add("keys", func(ws *websocket.Conn, msg string) error {
actions.add("keys", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
if value == "" {
@ -253,6 +262,8 @@ func wsController(c echo.Context) error {
key = "Control_L"
} else if key == "alt" {
key = "Alt_L"
} else if key == "tab" {
key = "Tab"
}
if key != "" {
@ -269,7 +280,7 @@ func wsController(c echo.Context) error {
return cmd.Run()
})
actions.add("key", func(ws *websocket.Conn, msg string) error {
actions.add("key", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
keys := make(map[string]string)
@ -294,7 +305,7 @@ func wsController(c echo.Context) error {
return cmd.Run()
})
actions.add("text", func(ws *websocket.Conn, msg string) error {
actions.add("text", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
if value == "" {
@ -306,7 +317,7 @@ func wsController(c echo.Context) error {
return cmd.Run()
})
actions.add("screenshot", func(ws *websocket.Conn, msg string) error {
actions.add("screenshot", func(ws *websocket.Conn, msg []byte) error {
data := ScreenshotMessageData{}
json.Unmarshal([]byte(msg), &data)
@ -325,6 +336,24 @@ func wsController(c echo.Context) error {
quality = 90
}
if data.Pointer {
currentX, currentY := getPointerPosition()
pointerSize := 2 * 16.0
pixelColor := color.RGBA{
R: 255,
G: 0,
B: 0,
A: 255,
}
for x := math.Max(0.0, currentX-pointerSize/2); x <= currentX+3; x++ {
for y := math.Max(0.0, currentY-pointerSize/2); y < currentY+3; y++ {
img.SetRGBA(int(x), int(y), pixelColor)
}
}
}
buff := new(bytes.Buffer)
jpeg.Encode(buff, img, &jpeg.Options{Quality: quality})
@ -336,7 +365,7 @@ func wsController(c echo.Context) error {
return nil
})
actions.add("messages", func(ws *websocket.Conn, msg string) error {
actions.add("messages", func(ws *websocket.Conn, msg []byte) error {
data := MessagesData{}
json.Unmarshal([]byte(msg), &data)
@ -344,7 +373,7 @@ func wsController(c echo.Context) error {
msg, _ := json.Marshal(value)
if actions.has(value.Type) {
actions.exec(value.Type, ws, string(msg))
actions.exec(value.Type, ws, msg)
time.Sleep(400 * time.Millisecond)
}
}
@ -352,20 +381,37 @@ func wsController(c echo.Context) error {
return nil
})
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
for {
msg := ""
websocket.Message.Receive(ws, &msg)
return actions
}
message := Message{}
json.Unmarshal([]byte(msg), &message)
var (
upgrader = websocket.Upgrader{}
)
if message.Type != "" && actions.has(message.Type) {
actions.exec(message.Type, ws, msg)
}
func wsController(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
for {
_, msg, err := ws.ReadMessage()
if err != nil {
ws.Close()
fmt.Printf("%+v\n", "Connection closed")
return err
}
}).ServeHTTP(c.Response(), c.Request())
message := Message{}
json.Unmarshal([]byte(msg), &message)
if message.Type != "" && actions.has(message.Type) {
actions.exec(message.Type, ws, msg)
}
}
return nil
}