Browse Source

pretty good remote desktop

main
Martin Dørum 10 months ago
parent
commit
16a05a5ef8
6 changed files with 285 additions and 18 deletions
  1. 89
    2
      main.go
  2. 9
    1
      screencap/screencap.go
  3. BIN
      web/cursor.png
  4. 1
    1
      web/files.html
  5. 174
    2
      web/remote.html
  6. 12
    12
      web/util.js

+ 89
- 2
main.go View File

@@ -16,6 +16,27 @@ type Config struct {
BasePath string `toml:"base_path"`
}

type EmptyData struct {}

type KeyboardTypeData struct {
Text string `json:"text"`
}

type KeyboardKeyData struct {
Key string `json:"key"`
Modifiers []string `json:"modifiers"`
}

type ScrollData struct {
X int `json:"x"`
Y int `json:"y"`
}

type MouseClickData struct {
Button string `json:"button"`
DoubleClick bool `json:"doubleClick"`
}

type MousePosData struct {
X int `json:"x"`
Y int `json:"y"`
@@ -106,7 +127,73 @@ func main() {
}

robotgo.MoveMouse(pos.X, pos.Y)
return nil
return json.NewEncoder(w).Encode(&EmptyData{})
} else {
return errors.New("Invalid method: " + req.Method)
}
}))

http.HandleFunc("/api/remote/mouse-click", handler(func(w RW, req *Req) error {
if req.Method == "POST" {
var click MouseClickData
err := json.NewDecoder(req.Body).Decode(&click)
if err != nil {
return err
}

robotgo.MouseClick(click.Button, click.DoubleClick)
return json.NewEncoder(w).Encode(&EmptyData{})
} else {
return errors.New("Invalid method: " + req.Method)
}
}))

http.HandleFunc("/api/remote/scroll", handler(func(w RW, req *Req) error {
if req.Method == "POST" {
var scroll ScrollData
err := json.NewDecoder(req.Body).Decode(&scroll)
if err != nil {
return err
}

robotgo.Scroll(scroll.X, scroll.Y)
return json.NewEncoder(w).Encode(&EmptyData{})
} else {
return errors.New("Invalid method: "+ req.Method)
}
}))

http.HandleFunc("/api/remote/keyboard-type", handler(func(w RW, req *Req) error {
if req.Method == "POST" {
var text KeyboardTypeData
err := json.NewDecoder(req.Body).Decode(&text)
if err != nil {
return err
}

robotgo.TypeStr(text.Text)
return json.NewEncoder(w).Encode(&EmptyData{})
} else {
return errors.New("Invalid method: " + req.Method)
}
}))

http.HandleFunc("/api/remote/keyboard-keys", handler(func(w RW, req *Req) error {
if req.Method == "POST" {
var key KeyboardKeyData
err := json.NewDecoder(req.Body).Decode(&key)
if err != nil {
return err
}

var modifiers []interface{}
for _, modifier := range key.Modifiers {
modifiers = append(modifiers, modifier)
}

log.Printf("key: %s, modifiers: %#v", key.Key, modifiers)
robotgo.KeyTap(key.Key, modifiers...)
return json.NewEncoder(w).Encode(&EmptyData{})
} else {
return errors.New("Invalid method: " + req.Method)
}
@@ -182,7 +269,7 @@ func main() {
go screencap.Run()

log.Println("Listening on :3000...")
err = http.ListenAndServe("localhost:3000", nil)
err = http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatal(err)
}

+ 9
- 1
screencap/screencap.go View File

@@ -58,9 +58,12 @@ func Capture() chan *Buffer {
}

func Run() {
targetDelta := 200 * time.Millisecond

for {
<-startChan

startTime := time.Now()
img, err := screenshot.CaptureDisplay(0)
if err != nil {
log.Printf("Failed to capture screenshot: %v", err)
@@ -71,7 +74,7 @@ func Run() {
buf := &buffers[currentBuffer]
currentBuffer = (currentBuffer + 1) % len(buffers)
buf.Length = 0
err = jpeg.Encode(buf, img, &jpeg.Options{Quality: 80})
err = jpeg.Encode(buf, img, &jpeg.Options{Quality: 40})
if err != nil {
log.Printf("Failed to encode jpeg: %v", err)
time.Sleep(2 * time.Second)
@@ -84,5 +87,10 @@ func Run() {
}
chans = make([]chan *Buffer, 0)
mut.Unlock()

delta := time.Now().Sub(startTime)
if delta < targetDelta {
time.Sleep(targetDelta - delta)
}
}
}

BIN
web/cursor.png View File


+ 1
- 1
web/files.html View File

@@ -93,7 +93,7 @@ async function render() {
}

render();
window.onhashchange = render;
window.addEventListener("hashchange", render);
</script>
</body>
</html>

+ 174
- 2
web/remote.html View File

@@ -11,6 +11,12 @@ body {
margin: 0px;
}

#cursor {
position: absolute;
left: 0px;
top: 0px;
}

#screencast-container {
max-height: calc(100% - 200px);
height: 100%;
@@ -29,15 +35,181 @@ body {
</head>

<body>
<img id="cursor" src="cursor.png" width=25>
<div id="screencast-container">
<img id="screencast" src="/api/remote/screencast">
</div>

<form id="text-form">
<input name="text" type="text">
</form>

<script src="util.js"></script>
<script>
let cursorEl = document.getElementById("cursor");
let screencastEl = document.getElementById("screencast");
let screencastContainerEl = document.getElementById("screencast-container");
let textFormEl = document.getElementById("text-form");

function updateCursor(mousePos, screenSize) {
let fracX = mousePos.x / screenSize.width;
let fracY = mousePos.y / screenSize.height;
let left = fracX * screencastEl.offsetWidth + screencastEl.offsetLeft;
let top = fracY * screencastEl.offsetHeight + screencastEl.offsetTop;
cursorEl.style.left = left + "px";
cursorEl.style.top = top + "px";
}

function moveDelta(mousePos, screenSize, delta) {
mousePos.x += delta.x;
if (mousePos.x >= screenSize.width) {
mousePos.x = screenSize.width - 1;
} else if (mousePos.x < 0) {
mousePos.x = 0;
}

mousePos.y += delta.y;
if (mousePos.y >= screenSize.height) {
mousePos.y = screenSize.height - 1;
} else if (mousePos.y < 0) {
mousePos.y = 0;
}

updateCursor(mousePos, screenSize);
api("PUT", "remote/mouse-pos", mousePos);
}

function signPow(num, pow) {
if (num >= 0) {
return Math.pow(num, pow);
} else {
return -Math.pow(-num, pow);
}
}

function roundToZero(num) {
if (num >= 0) {
return Math.floor(num);
} else {
return Math.ceil(num);
}
}

async function main() {
let screenSize = api("GET", "remote/screen-size");
let mousePos = api("GET", "remote/mouse-pos");
let screenSize = await api("GET", "remote/screen-size");
let mousePos = await api("GET", "remote/mouse-pos");

updateCursor(mousePos, screenSize);
setInterval(async () => {
mousePos = await api("GET", "remote/mouse-pos");
updateCursor(mousePos, screenSize);
}, 1000);

textFormEl.addEventListener("submit", async evt => {
evt.preventDefault();
let text = evt.target.elements.text.value;
evt.target.elements.text.value = "";
await api("POST", "remote/keyboard-type", {text: evt.target.elements.text.value});
api("POST", "remote/keyboard-keys", {key: "enter", modifiers: []});
});

screencastContainerEl.addEventListener("click", evt => {
evt.preventDefault();
api("POST", "remote/mouse-click", {button: "left", doubleClick: false});
});

screencastContainerEl.addEventListener("dblclick", evt => {
evt.preventDefault();
api("POST", "remote/mouse-click", {button: "left", doubleClick: true});
});

screencastEl.addEventListener("mousemove", evt => {
if (evt.buttons != 0) {
return;
}

evt.preventDefault();
let fracX = evt.offsetX / evt.target.offsetWidth;
let fracY = evt.offsetY / evt.target.offsetHeight;
mousePos.x = Math.round(fracX * screenSize.width);
mousePos.y = Math.round(fracY * screenSize.height);
updateCursor(mousePos, screenSize);
api("PUT", "remote/mouse-pos", mousePos);
});

screencastContainerEl.addEventListener("mousemove", evt => {
if (evt.buttons != 1) {
return;
}

evt.preventDefault();
moveDelta(mousePos, screenSize, {x: evt.movementX, y: evt.movementY});
});

let numTouches = 0;
let touches = {};
let scrollDist = {x: 0, y: 0};
screencastContainerEl.addEventListener("touchstart", evt => {
evt.preventDefault();
numTouches += evt.changedTouches.length;
for (let touch of evt.changedTouches) {
touches[touch.identifier] = {x: touch.clientX, y: touch.clientY, moveDist: 0};
}
});

screencastContainerEl.addEventListener("touchmove", evt => {
evt.preventDefault();

let delta = {x: 0, y: 0}
for (let touch of evt.changedTouches) {
let oldTouch = touches[touch.identifier];
let d = {x: touch.clientX - oldTouch.x, y: touch.clientY - oldTouch.y};
oldTouch.moveDist += Math.sqrt(d.x * d.x + d.y * d.y);
oldTouch.x = touch.clientX;
oldTouch.y = touch.clientY;
delta.x += d.x;
delta.y += d.y;
}

if (numTouches == 1) {
delta.x = Math.round(signPow(delta.x, 1.5));
delta.y = Math.round(signPow(delta.y, 1.5));
moveDelta(mousePos, screenSize, delta);
} else if (numTouches == 2) {
delta.x = delta.x / 5 / numTouches;
delta.y = delta.y / 5 / numTouches;

if (Math.abs(delta.x) > Math.abs(delta.y)) {
scrollDist.x += delta.x;
let distX = roundToZero(scrollDist.x);
if (distX != 0) {
api("POST", "remote/scroll", {x: distX, y: 0});
scrollDist.x -= distX;
}
} else {
scrollDist.y += delta.y;
let distY = roundToZero(scrollDist.y);
if (distY != 0) {
api("POST", "remote/scroll", {x: 0, y: distY});
scrollDist.y -= distY;
}
}
}
});

screencastContainerEl.addEventListener("touchend", evt => {
evt.preventDefault();
numTouches -= evt.changedTouches.length;
for (let touch of evt.changedTouches) {
numTouches -= 1;
let oldTouch = touches[touch.identifier];
touches[touch.identifier] = null;
if (oldTouch.moveDist < 10) {
api("POST", "remote/mouse-click", {button: "left", doubleClick: false});
break;
}
}
});
}

main();

+ 12
- 12
web/util.js View File

@@ -1,3 +1,13 @@
window.addEventListener("error", evt => {
console.error(evt);
alert("Oh no: " + evt.message);
});

window.addEventListener("unhandledrejection", evt => {
console.error(evt);
alert("Oh no: " + evt.reason.toString());
});

async function api(method, path, body = null) {
let options = {method};
if (body != null) {
@@ -5,18 +15,8 @@ async function api(method, path, body = null) {
}

let json;
try {
let resp = await fetch("/api/" + path, options).then(r => r.text());
json = JSON.parse(resp);
} catch (err) {
alert(err.toString());
throw err;
}

if (json.error != null) {
alert(json.error);
throw new Error(json.error);
}
let resp = await fetch("/api/" + path, options).then(r => r.text());
json = JSON.parse(resp);

return json;
}

Loading…
Cancel
Save