Skip to content

Commit 18771b2

Browse files
authored
feat/toggle cursor visibility (#87)
# Checklist - [X] A link to a related issue in our repository: https://linear.app/onkernel/issue/KERNEL-443/api-expose-option-to-hide-remote-browser-cursor-in-api - [x] A description of the changes proposed in the pull request. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces POST /computer/cursor to hide/show the cursor via unclutter, updates OpenAPI and generated client/server code, adds runtime dependency and docs for local testing. > > - **API/Backend**: > - **New endpoint** `POST /computer/cursor` with `SetCursorRequest { hidden: boolean }` to hide/show the cursor. > - Implements `SetCursor` in `server/cmd/api/api/computer.go` (kills existing `unclutter`, optionally starts new with `DISPLAY`, idle=0, large jitter). > - Updates generated OAPI code: models, client methods, server handlers, routing, and response parsers; embeds updated swagger. > - **Images**: > - Adds `unclutter` to runtime packages in `images/chromium-headful/Dockerfile`. > - Minor build script: logs image index/name in `build-unikernel.sh`. > - **Docs**: > - Adds `images/README.md` with local testing steps and example curl for the new endpoint. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 51ef3a0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent bb066c5 commit 18771b2

File tree

6 files changed

+491
-118
lines changed

6 files changed

+491
-118
lines changed

images/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## How to test kernel-images changes locally with docker
2+
3+
- Make relevant changes to kernel-images example adding a new endpoint at `kernel-images/server/cmd/api/api/computer.go`, example I added `SetCursor()` endpoint.
4+
- Run openApi to generate the boilerplate for the new endpoints with make oapi-generate
5+
- Check changes at `kernel-images/server/lib/oapi/oapi.go`
6+
- `cd kernel-images/images/chromium-headful`
7+
- Build and run the docker image with `./build-docker.sh && ENABLE_WEBRTC=true ./run-docker.sh`
8+
- Open http://localhost:8080/ in your browser
9+
- Now new endpoint should be available for tests example curl command:
10+
```sh
11+
curl -X POST localhost:444/computer/cursor \
12+
-H "Content-Type: application/json" \
13+
-d '{"hidden": true}'
14+
```
15+

images/chromium-headful/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap
223223
set -eux; \
224224
apt-get update; \
225225
apt-get --no-install-recommends -y install \
226-
wget ca-certificates python2 supervisor xclip xdotool \
226+
wget ca-certificates python2 supervisor xclip xdotool unclutter \
227227
pulseaudio dbus-x11 xserver-xorg-video-dummy \
228228
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 \
229229
x11-xserver-utils \

images/chromium-headful/build-unikernel.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ docker cp cnt-"$app_name":/ ./.rootfs
2525
rm -f initrd || true
2626
sudo mkfs.erofs --all-root -d2 -E noinline_data -b 4096 initrd ./.rootfs
2727

28+
echo "Image index/name: $UKC_INDEX/$IMAGE"
29+
2830
# Package the unikernel (and the new initrd) to KraftCloud
2931
kraft pkg \
3032
--name $UKC_INDEX/$IMAGE \

server/cmd/api/api/computer.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"os/exec"
1111
"strconv"
12+
"syscall"
1213
"time"
1314

1415
"github.com/onkernel/kernel-images/server/lib/logger"
@@ -362,6 +363,71 @@ func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestO
362363
return oapi.TypeText200Response{}, nil
363364
}
364365

366+
const (
367+
368+
// Unclutter configuration for cursor hiding
369+
// Setting idle to 0 hides the cursor immediately
370+
unclutterIdleSeconds = "0"
371+
372+
// A very large jitter value (9 million pixels) ensures that all mouse
373+
// movements are treated as "noise", keeping the cursor permanently hidden
374+
// when combined with idle=0
375+
unclutterJitterPixels = "9000000"
376+
)
377+
378+
func (s *ApiService) SetCursor(ctx context.Context, request oapi.SetCursorRequestObject) (oapi.SetCursorResponseObject, error) {
379+
log := logger.FromContext(ctx)
380+
381+
// serialize input operations to avoid overlapping commands
382+
s.inputMu.Lock()
383+
defer s.inputMu.Unlock()
384+
385+
// Validate request body
386+
if request.Body == nil {
387+
return oapi.SetCursor400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{
388+
Message: "request body is required"},
389+
}, nil
390+
}
391+
body := *request.Body
392+
393+
// Kill any existing unclutter processes first
394+
pkillCmd := exec.CommandContext(ctx, "pkill", "unclutter")
395+
pkillCmd.SysProcAttr = &syscall.SysProcAttr{
396+
Credential: &syscall.Credential{Uid: 0, Gid: 0},
397+
}
398+
399+
if err := pkillCmd.Run(); err != nil {
400+
if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 1 {
401+
log.Error("failed to kill existing unclutter processes", "err", err)
402+
return oapi.SetCursor500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
403+
Message: "failed to kill existing unclutter processes"},
404+
}, nil
405+
}
406+
}
407+
408+
if body.Hidden {
409+
display := s.resolveDisplayFromEnv()
410+
unclutterCmd := exec.CommandContext(context.Background(),
411+
"unclutter",
412+
"-idle", unclutterIdleSeconds,
413+
"-jitter", unclutterJitterPixels,
414+
)
415+
unclutterCmd.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=%s", display))
416+
unclutterCmd.SysProcAttr = &syscall.SysProcAttr{
417+
Credential: &syscall.Credential{Uid: 0, Gid: 0},
418+
}
419+
420+
if err := unclutterCmd.Start(); err != nil {
421+
log.Error("failed to start unclutter", "err", err)
422+
return oapi.SetCursor500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
423+
Message: "failed to start unclutter"},
424+
}, nil
425+
}
426+
}
427+
428+
return oapi.SetCursor200JSONResponse{Ok: true}, nil
429+
}
430+
365431
func (s *ApiService) PressKey(ctx context.Context, request oapi.PressKeyRequestObject) (oapi.PressKeyResponseObject, error) {
366432
log := logger.FromContext(ctx)
367433

0 commit comments

Comments
 (0)