Skip to content

feat: report where the user is typing, so a client can keep it on screen - #5513

Open
harrison001 wants to merge 1 commit into
LizardByte:masterfrom
harrison001:macos-caret
Open

feat: report where the user is typing, so a client can keep it on screen#5513
harrison001 wants to merge 1 commit into
LizardByte:masterfrom
harrison001:macos-caret

Conversation

@harrison001

Copy link
Copy Markdown

Description

On a phone the on-screen keyboard covers half the picture, and the field you're typing into is usually in the covered half. The client has no way to know which half matters. The host does.

GET /caret answers in fractions of the streamed display, so the client doesn't need the host's resolution:

{"x":0.42,"y":0.71,"w":0.01,"h":0.02,"source":"caret"}
{"x":0.42,"y":0.71,"w":0,"h":0,"source":"pointer"}
{}

source says which it is. caret is the focused element's insertion point via Accessibility, macOS only. pointer is the cursor position, used when the focused application reports no caret, which most don't; that half works on Windows, Linux/X11 and macOS. {} means neither was available and the client should leave the picture where it is.

HTTPS only, so it sits behind the same client-certificate check as applist and launch.

platf::pointer_location() is new in platform/common.h. I didn't reuse get_mouse_loc(): it's documented as existing only for tests, and it takes the input backend of a running stream, which an HTTP handler doesn't have.

Three things on the macOS side that aren't obvious from the docs:

  • AXUIElementCreateSystemWide() with kAXFocusedUIElementAttribute returns nothing for applications that answer when asked directly, iTerm2 among them. It has to ask the application.
  • Accessibility uses whole-desktop coordinates, so a display above the main one gives negative values. A caret on a display that isn't being streamed is dropped.
  • An element with no insertion point returns an empty rect at the origin rather than an error, and taking that at face value puts the caret in the top-left corner.

Nothing upstream calls this yet. The consumer is TracePort, an open-source Moonlight fork of mine that this was built for. The endpoint is additive and does nothing if no client asks.

Tested on macOS 26.6.1, M1 Max, streaming to that client:

  • iTerm2 and Xcode report a caret. Brave, VS Code, Sublime Text and Telegram don't, and fall back to the pointer. 528 requests over one three-minute session, all past client-certificate verification.
  • Pointer on a second display returns {}. On the centre of the streamed display it returns exactly {"x":0.500,"y":0.500}.

The Windows and Linux implementations of pointer_location() are compile-tested only. CI builds both, but I have neither platform to run them on.

Screenshot

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

See our AI usage policy.

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

A phone's on-screen keyboard covers half the picture, and the client cannot tell
which half matters — the field being typed into is usually the hidden one. The
host can tell, so GET /caret reports it.

The answer is fractions of the streamed display, so the client needs to know
nothing about resolutions, and "source" says which of two things it is:

- "caret", the focused application's insertion point, read through Accessibility.
  macOS only, because no other platform exposes one.
- "pointer", where the cursor is. The fallback for the applications that report
  no caret, which is most of them, and a good stand-in: you click into a field to
  type in it. It is also the only thing that helps in trackpad mode, where the
  client sends relative motion and never learns where the pointer ended up.

An empty object means neither was available and the client should leave the
picture where it is.

Served over HTTPS only, so it reaches paired and enabled clients alone. Where
someone is typing, and the pointer position it falls back to, describe what the
user is doing closely enough to belong behind the same verification as the rest
of the session.

platf::pointer_location() is declared in platform/common.h and implemented on
each platform. get_mouse_loc() was the obvious thing to reuse and is not usable
here: it is documented as existing only for tests, and it takes the input backend
of a running stream, which an HTTP handler does not have.

Windows reads GetCursorPos and normalises against the primary monitor. Linux
queries X11 and normalises against the X screen, which is what x11grab captures;
Wayland offers no way to ask, so it reports nothing.

Three details on the macOS side, each found by probing a machine rather than
from documentation:

- Ask the application, not the system. AXUIElementCreateSystemWide() with
  kAXFocusedUIElementAttribute returns nothing for applications that answer when
  asked directly, iTerm2 among them.
- Filter by display. Accessibility works in whole-desktop coordinates, so a
  display above the main one gives negative values, and a caret on a display that
  is not being streamed means nothing to the client.
- An empty rect at the origin means no caret. Elements without an insertion point
  return that rather than an error, and taking it at face value puts the caret in
  the top-left corner.

Verified on macOS 26.6.1: iTerm2 and Xcode report a caret; Brave, VS Code,
Sublime Text and Telegram do not and fall back to the pointer.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant