Skip to content

fix: resolve mobile typing issues (spacebar, backspace, and text ghosting)#175

Merged
imxade merged 3 commits into
AOSSIE-Org:mainfrom
Rozerxshashank:fix/typing-issues
Feb 26, 2026
Merged

fix: resolve mobile typing issues (spacebar, backspace, and text ghosting)#175
imxade merged 3 commits into
AOSSIE-Org:mainfrom
Rozerxshashank:fix/typing-issues

Conversation

@Rozerxshashank

@Rozerxshashank Rozerxshashank commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Addressed Issues:

Fixes #174

Description

This PR implements a complete rewrite of the mobile typing system to resolve critical regressions on mobile devices.

###KEY CHANGES

Synchronous Volatile Input Strategy: Replaced the previous complex diffing logic with a "Volatile" approach. The hidden input field is now forcefully reset to a single space placeholder (" ") immediately and synchronously after every keystroke. This prevents mobile keyboards from building a prediction buffer, which was the root cause of "ghost text" and character duplication.

Precise Input Mapping: Utilizes nativeEvent.inputType to explicitly distinguish between character insertion, backspace (deleteContentBackward), and line breaks.

Robust Server-Side Simulation: Updated the NutJS InputHandler to simulate the Space key as a physical hardware key press rather than a typed character, ensuring compatibility with applications that have strict keyboard hooks.

Browser Interface Optimization: Added aggressive HTML hints (inputMode="text", autoCorrect="off", etc.) to minimize mobile browser interference with the typing flow.

Functional Verification

  • Please check off the behaviors verified with this change.

Basic Gestures

  • One-finger tap: Verified as Left Click.

  • Two-finger tap: Verified as Right Click.

  • Click and drag: Verified selection behavior.

  • Pinch to zoom: Verified zoom functionality (if applicable).

Modes & Settings

  • Cursor mode: Cursor moves smoothly and accurately.

  • Scroll mode: Page scrolls as expected.

  • Sensitivity: Verified changes in cursor speed/sensitivity settings.

  • Invert Scrolling: Verified scroll direction toggles correctly.

Advanced Input

  • Key combinations: Verified "hold" behavior for modifiers (e.g., Ctrl+C) and held keys are shown in buffer.

  • Keyboard input: Verified Space, Backspace, and Enter keys work correctly.

  • Glide typing: Verified path drawing and text output.

  • Voice input: Verified speech-to-text functionality.

  • Backspace doesn't send the previous input.

Any other gesture or input behavior introduced:

  • New Gestures: Verified any other gesture or input behavior introduced in this PR.

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.

  • My code follows the project's code style and conventions

  • I have performed a self-review of my own code

  • I have commented my code, particularly in hard-to-understand areas

  • If applicable, I have made corresponding changes or additions to the documentation

  • If applicable, I have made corresponding changes or additions to tests

  • My changes generate no new warnings or errors

  • I have joined the and I will share a link to this PR with the project maintainers there

  • I have read the contributing guidlines.

  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

  • Incase of UI change I've added a demo video.

Summary by CodeRabbit

  • Bug Fixes

    • More reliable trackpad/text input: improved handling of native input events (space, backspace, enter), immediate input reset to prevent stuck characters, and clearer behavior for multi-character/special key input and modifier interactions.
  • Refactor

    • Simplified and modernized input flow and focus behavior for more maintainable, predictable input handling without changing public APIs.

@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Replaces IME composition-driven handling in the trackpad input with a unified native onChange flow (using event.inputType/data), synchronous hidden-input reset, explicit handling for Space/Backspace/Enter, refined modifier state transitions, and simplified keyboard logic; also changes key dispatch in InputHandler to press+release for mapped keys including space.

Changes

Cohort / File(s) Summary
Trackpad Input Refactor
src/routes/trackpad.tsx
Removed compositionStart/update/end and prevCompositionData. Replaced composition diffing with a single onChange handler that uses inputType/data; added hidden-input synchronous reset (value -> single space + caret placement); explicit handling for deleteContentBackward, insertLineBreak/insertParagraph, and space mapping; simplified modifier state (Active/Hold/Release) and keyboard flow; switched to defaultValue, inputMode="text", enterKeyHint="enter", and delegated focus to the hidden input.
InputHandler Key Dispatch
src/server/InputHandler.ts
Changed behavior for mapped keys to perform pressKey followed by releaseKey (press-and-release) for mapped keys, including explicit space key mapping; no public API changes.

Sequence Diagram(s)

sequenceDiagram
  participant Mobile as Mobile Keyboard
  participant Hidden as HiddenInput
  participant UI as TrackpadHandler
  participant Remote as RemoteConnection
  participant Server as Remote Server

  Mobile->>Hidden: user types (native text / composition resolved)
  Hidden->>UI: onChange (inputType, data)
  UI->>UI: interpret inputType -> (Text / Space / Backspace / Enter / Modifier)
  UI->>Hidden: synchronous reset (set value to " " and position caret)
  UI->>Remote: send key or text events
  Remote->>Server: forward input events
  Server-->>Remote: ack/response
  Remote-->>UI: delivery confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Typescript Lang

Poem

🐰
I tapped the tiny hidden key,
chased composition out of me,
a space, a backspace, enter's song,
neat events hop gently along,
clack — the remote types along.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: resolving mobile typing issues with spacebar, backspace, and text ghosting.
Description check ✅ Passed The description is comprehensive, addressing the issue number, detailed changes, complete functional verification checklist, and proper documentation of the rewrite approach.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #174: synchronous input reset prevents ghost text, precise inputType handling for spacebar/backspace/enter, space key simulated as hardware press, and browser optimization.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving the mobile typing issues outlined in #174; no unrelated modifications detected in the trackpad component or InputHandler.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/trackpad.tsx`:
- Around line 23-33: The current initializers for sensitivity and invertScroll
(the useState callbacks) read from localStorage and call parseFloat/JSON.parse
without validation, which can throw or yield NaN; update the initializers in
trackpad.tsx to safely parse and validate: wrap JSON.parse in a try/catch and
fall back to false on error for invertScroll, and for sensitivity call
parseFloat then check Number.isFinite (or isNaN) and default to 1.0 if parsing
fails or produces NaN; keep the existing typeof window guard and reference the
sensitivity and invertScroll state initializers by name when making these
changes.
- Around line 155-166: The container's onClick handler (handleContainerClick)
triggers the accessibility lint; replace it with a pointer-based handler and add
keyboard activation so screen-reader/keyboard users can perform the same action.
Specifically, rename/replace handleContainerClick to handleContainerPointer (or
similar) and attach it to the container as onPointerDown/onPointerUp (preserve
the existing guard e.target === e.currentTarget and call focusInput() with
e.preventDefault() as needed), and also add a keyboard handler (onKeyDown) on
the same container that listens for Enter and Space and calls focusInput() when
the event originates from the container; keep the same shallow target check so
behavior remains identical and both pointer and keyboard activations are
supported to satisfy the use-key-with-click-events lint.
- Line 213: Remove the autoFocus JSX attribute from the input in Trackpad
component and instead set focus imperatively after mount: create a ref (e.g.,
inputRef) attached to the input, add a useEffect in the Trackpad component that
calls inputRef.current?.focus() on mount (empty dependency array) to satisfy the
linter, and ensure the ref is typed correctly and checked for null before
calling focus.

In `@src/server/InputHandler.ts`:
- Around line 195-210: Remove the raw key logging and lint-blocking
string/template usages in the input handling block: eliminate console.log calls
that output msg.key, replace template literals like `Processing key: ${msg.key}`
and `Unmapped key: ${msg.key}` with no logging, and simplify bracket/optional
access by using direct keys (e.g., use msg.key and
KEY_MAP[msg.key.toLowerCase()] safely after validation). Keep the same control
flow around KEY_MAP lookup and the actions that call keyboard.pressKey,
keyboard.releaseKey and keyboard.type, but do not log or expose msg.key anywhere
in this function (including the alternative space handling); ensure no
eslint-disable is needed after making these changes.
- Around line 88-90: The catch block in InputHandler that calls
this.handleMessage(pending).catch(...) uses a misleading error label ("Error
processing pending move event"); update that log to accurately reflect the
scroll throttling path by changing the message to something like "Error
processing pending scroll event" (or similar) so the catch in the scroll timer
clearly references scroll-related pending events; locate this within the
InputHandler class where handleMessage is invoked for pending scroll timer
callbacks and replace the error message string in the console.error call.
- Around line 218-221: In the loop inside InputHandler that iterates "for (const
k of msg.keys)" you must validate each entry before calling k.toLowerCase():
check that k is a non-empty string (e.g., typeof k === 'string' and k.length >
0) and skip or handle non-strings/null/undefined rather than calling toLowerCase
on them; then use the validated lowerKey to lookup KEY_MAP[lowerKey]; update the
loop to either continue on invalid entries or coerce safely (String(k)) if
coercion is desired, and ensure any downstream uses expect skipped entries.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8535415 and d8f48cf.

📒 Files selected for processing (2)
  • src/routes/trackpad.tsx
  • src/server/InputHandler.ts

Comment thread src/routes/trackpad.tsx Outdated
Comment thread src/routes/trackpad.tsx Outdated
Comment thread src/routes/trackpad.tsx Outdated
Comment thread src/server/InputHandler.ts Outdated
Comment thread src/server/InputHandler.ts Outdated
Comment thread src/server/InputHandler.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/trackpad.tsx (1)

1-4: ⚠️ Potential issue | 🟠 Major

Missing "use client" directive for NextJS client component.

This component uses client-side APIs (useState, useRef, localStorage, event handlers) and must be marked as a client component in NextJS.

Proposed fix
+"use client"
+
 import { BufferBar } from "@/components/Trackpad/Buffer"
 import type { ModifierState } from "@/types"

As per coding guidelines: "Ensure that 'use client' is being used" for NextJS components using client-side features.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 1 - 4, This file uses client-side hooks
and APIs (useState, useRef, localStorage, event handlers) but is missing the
Next.js client directive; add the literal "use client" directive as the very
first line of the module (before any imports) so the component that references
BufferBar, ModifierState, createFileRoute, useRef and useState is treated as a
client component.
♻️ Duplicate comments (2)
src/routes/trackpad.tsx (2)

29-33: ⚠️ Potential issue | 🟠 Major

JSON.parse can throw on malformed localStorage data, crashing the component.

The invertScroll initializer still lacks error handling. If localStorage contains invalid JSON, the component will fail to render.

Proposed fix
     const [invertScroll] = useState(() => {
         if (typeof window === 'undefined') return false;
         const s = localStorage.getItem('rein_invert');
-        return s ? JSON.parse(s) : false;
+        if (!s) return false;
+        try {
+            return JSON.parse(s) === true;
+        } catch {
+            return false;
+        }
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 29 - 33, The useState initializer for
invertScroll (const [invertScroll] = useState(() => { ... })) can throw if
JSON.parse is fed malformed data from localStorage.getItem('rein_invert'); wrap
the parsing in a try/catch and return a safe default (false) on any error: read
the raw value with localStorage.getItem('rein_invert'), if falsy return false,
otherwise attempt JSON.parse inside try/catch and on catch return false (and
optionally clear the bad key) so the component won't crash.

155-167: 🧹 Nitpick | 🔵 Trivial

Consider using onPointerDown instead of suppressing the lint rule.

While the biome-ignore comment is acceptable for this use case, replacing onClick with onPointerDown would eliminate the need for the suppression entirely and provide slightly better mobile touch responsiveness.

Proposed fix
-    const handleContainerClick = (e: React.MouseEvent) => {
+    const handleContainerPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
         if (e.target === e.currentTarget) {
             e.preventDefault();
             focusInput();
         }
     };

     return (
-        // biome-ignore lint/a11y/useKeyWithClickEvents: Layout container delegates focus to hidden input, not an interactive element
         <div
             className="flex flex-col h-full overflow-hidden"
-            onClick={handleContainerClick}
+            onPointerDown={handleContainerPointerDown}
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 155 - 167, The container currently uses
onClick with handleContainerClick and a biome-ignore lint suppression; replace
the onClick handler with onPointerDown on the same div and update
handleContainerClick to accept React.PointerEvent (still checking e.target ===
e.currentTarget and calling focusInput()), removing the biome-ignore comment;
this preserves the current focus-delegation behavior while improving mobile
touch responsiveness and eliminating the accessibility lint suppression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/trackpad.tsx`:
- Around line 10-13: The formatting mismatch is caused by the Route export
block; run the project's configured formatter (Prettier or Biome) and reformat
the file so the export of Route = createFileRoute("/trackpad")({ component:
TrackpadPage }) matches the project's style. Locate the export using the symbols
Route, createFileRoute and TrackpadPage in the trackpad route file, run the
project's format script (npm/yarn pnpm run format or the configured
biome/prettier command), and commit the reformatted file to resolve the CI
failure.
- Around line 100-106: The Enter-key branch resets the hidden input's value but
omits the same cursor repositioning used by resetInput(), which can leave the
caret in the wrong place on some mobile keyboards; update the Enter key handling
in the key === 'enter' block to also call
hiddenInputRef.current.setSelectionRange(1, 1) (after setting
hiddenInputRef.current.value = ' ') so its behavior matches resetInput(),
ensuring consistent cursor placement; keep the existing send({ type: 'key', key:
'enter' }) call and null-check for hiddenInputRef.current.
- Around line 205-207: The onBlur handler currently calls setTimeout which may
fire after unmount and try to focus hiddenInputRef.current; change this by
storing the timeout id (e.g., timeoutIdRef) when calling setTimeout in the
onBlur handler and clear it in a useEffect cleanup (or clear it before setting a
new timeout) so the pending timer is cancelled on unmount; alternatively keep a
mountedRef checked inside the timeout callback before calling
hiddenInputRef.current?.focus(); update the onBlur, add the timeoutIdRef and a
useEffect cleanup that clears the timer to prevent focusing a null ref.

---

Outside diff comments:
In `@src/routes/trackpad.tsx`:
- Around line 1-4: This file uses client-side hooks and APIs (useState, useRef,
localStorage, event handlers) but is missing the Next.js client directive; add
the literal "use client" directive as the very first line of the module (before
any imports) so the component that references BufferBar, ModifierState,
createFileRoute, useRef and useState is treated as a client component.

---

Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 29-33: The useState initializer for invertScroll (const
[invertScroll] = useState(() => { ... })) can throw if JSON.parse is fed
malformed data from localStorage.getItem('rein_invert'); wrap the parsing in a
try/catch and return a safe default (false) on any error: read the raw value
with localStorage.getItem('rein_invert'), if falsy return false, otherwise
attempt JSON.parse inside try/catch and on catch return false (and optionally
clear the bad key) so the component won't crash.
- Around line 155-167: The container currently uses onClick with
handleContainerClick and a biome-ignore lint suppression; replace the onClick
handler with onPointerDown on the same div and update handleContainerClick to
accept React.PointerEvent (still checking e.target === e.currentTarget and
calling focusInput()), removing the biome-ignore comment; this preserves the
current focus-delegation behavior while improving mobile touch responsiveness
and eliminating the accessibility lint suppression.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8f48cf and 600eb86.

📒 Files selected for processing (1)
  • src/routes/trackpad.tsx

Comment thread src/routes/trackpad.tsx
Comment thread src/routes/trackpad.tsx Outdated
Comment thread src/routes/trackpad.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/routes/trackpad.tsx (3)

104-111: ⚠️ Potential issue | 🟡 Minor

Inconsistent input reset: missing setSelectionRange(1, 1) for Enter key.

The resetInput() helper at lines 61-66 sets both value = " " and setSelectionRange(1, 1), but the Enter fallback only sets the value. This inconsistency may cause cursor positioning issues on some mobile keyboards.

Align with resetInput behavior
 		if (key === "enter") {
 			send({ type: "key", key: "enter" })
 			if (hiddenInputRef.current) {
 				hiddenInputRef.current.value = " "
+				hiddenInputRef.current.setSelectionRange(1, 1)
 			}
 			return
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 104 - 111, The Enter-key fallback in
the key handler mirrors resetInput() by setting hiddenInputRef.current.value = "
" but omits positioning the caret; update the Enter branch (the block that calls
send({ type: "key", key: "enter" })) to, when hiddenInputRef.current exists,
also call hiddenInputRef.current.setSelectionRange(1, 1) so behavior matches
resetInput() and avoids mobile cursor issues.

29-33: ⚠️ Potential issue | 🟡 Minor

JSON.parse can throw on malformed localStorage data.

If rein_invert contains invalid JSON, the component will crash during initialization.

Wrap in try/catch
 	const [invertScroll] = useState(() => {
 		if (typeof window === "undefined") return false
 		const s = localStorage.getItem("rein_invert")
-		return s ? JSON.parse(s) : false
+		if (!s) return false
+		try {
+			return JSON.parse(s) === true
+		} catch {
+			return false
+		}
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 29 - 33, The initializer for the
invertScroll state calls JSON.parse on localStorage data which can throw on
malformed content; update the useState initializer (the function that reads
localStorage.getItem("rein_invert") used to set invertScroll) to wrap the
JSON.parse call in a try/catch, returning false as a safe fallback when parsing
fails (and optionally logging the error), so the component won't crash on
invalid stored data.

215-217: 🧹 Nitpick | 🔵 Trivial

setTimeout in onBlur can fire after component unmount.

If the component unmounts while the timeout is pending, hiddenInputRef.current?.focus() will safely no-op due to optional chaining, but it's better practice to clear the timeout on unmount.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 215 - 217, The onBlur handler currently
calls setTimeout(() => hiddenInputRef.current?.focus(), 10) which can fire after
the component unmounts; capture the timeout id (from setTimeout) into a ref
(e.g., blurTimeoutRef) when scheduling, clear any existing timeout before
setting a new one, and clearTimeout(blurTimeoutRef.current) in a useEffect
cleanup (or componentWillUnmount equivalent) so the pending focus callback is
canceled on unmount; update the onBlur handler and add the cleanup logic
referencing hiddenInputRef and the new blurTimeoutRef.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/InputHandler.ts`:
- Around line 199-204: The else-if branch checking msg.key === " " ||
msg.key?.toLowerCase() === "space" contains an unreachable clause; remove the
redundant msg.key?.toLowerCase() === "space" check so the branch only handles
the literal space character (msg.key === " "), or eliminate the branch entirely
if KEY_MAP lookup already handles "space"; update the block around KEY_MAP,
msg.key, and the keyboard.pressKey/keyboard.releaseKey calls in InputHandler
(where KEY_MAP.space and nutKey are used) so only one clear path handles the
space key.

---

Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 104-111: The Enter-key fallback in the key handler mirrors
resetInput() by setting hiddenInputRef.current.value = " " but omits positioning
the caret; update the Enter branch (the block that calls send({ type: "key",
key: "enter" })) to, when hiddenInputRef.current exists, also call
hiddenInputRef.current.setSelectionRange(1, 1) so behavior matches resetInput()
and avoids mobile cursor issues.
- Around line 29-33: The initializer for the invertScroll state calls JSON.parse
on localStorage data which can throw on malformed content; update the useState
initializer (the function that reads localStorage.getItem("rein_invert") used to
set invertScroll) to wrap the JSON.parse call in a try/catch, returning false as
a safe fallback when parsing fails (and optionally logging the error), so the
component won't crash on invalid stored data.
- Around line 215-217: The onBlur handler currently calls setTimeout(() =>
hiddenInputRef.current?.focus(), 10) which can fire after the component
unmounts; capture the timeout id (from setTimeout) into a ref (e.g.,
blurTimeoutRef) when scheduling, clear any existing timeout before setting a new
one, and clearTimeout(blurTimeoutRef.current) in a useEffect cleanup (or
componentWillUnmount equivalent) so the pending focus callback is canceled on
unmount; update the onBlur handler and add the cleanup logic referencing
hiddenInputRef and the new blurTimeoutRef.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 600eb86 and ddc8e06.

📒 Files selected for processing (2)
  • src/routes/trackpad.tsx
  • src/server/InputHandler.ts

Comment thread src/server/InputHandler.ts
@imxade imxade merged commit 762874e into AOSSIE-Org:main Feb 26, 2026
2 checks passed
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.

[Bug]: Erratic typing behavior from mobile to PC (Spacebar, Backspace, and Text Duplication)

2 participants