Skip to content

Instantly share code, notes, and snippets.

@zqidev
Last active February 2, 2026 17:36
Show Gist options
  • Select an option

  • Save zqidev/2e94c3847a1bd9bc4f5dfa4cfd1e787c to your computer and use it in GitHub Desktop.

Select an option

Save zqidev/2e94c3847a1bd9bc4f5dfa4cfd1e787c to your computer and use it in GitHub Desktop.
NumWorks Enhancements — Keyboard Shortcut Support, Visual Keypress Animations, and WebKit Arrow Key Fix

NumWorks Enhancements — Keyboard Shortcut Support, Visual Keypress Animations, and WebKit Arrow Key Fix

Overview

The NumWorks online emulator behaves best in Chromium-based browsers. On WebKit (Safari and WebKit-based wrappers such as Pake/Tauri apps on macOS), it has a notable limitation:

  • The arrow keys do not work reliably, even though letters, digits, Enter, and most other keys function as expected.

To address this and improve the overall desktop experience, I created a JS script that:

  1. Restores reliable arrow key behavior on WebKit.
  2. Adds desktop-style keyboard shortcuts that drive the calculator using familiar key combinations.
  3. Provides visual keypress animations on the NumWorks UI so that every physical keypress is mirrored on the on-screen keypad.

Example Usage:

CleanShot 2026-01-28 at 01 38 48


Feature additions

Keyboard shortcut support

The script sits on top of the existing NumWorks web UI and listens for keyboard events. When certain key combinations are pressed, it “presses” the corresponding on‑screen keys for you by sending pointer events to the correct span[data-key="…"] elements.

Examples of supported shortcuts:

  • Copy and paste

    • Cmd + C / Ctrl + C
      -> Internally presses Shift + Var, which is the NumWorks “Copy” operation.

    • Cmd + V / Ctrl + V
      -> Internally presses Shift + Toolbox, which is the NumWorks “Paste” operation.

  • Clear

    • Cmd + Backspace / Ctrl + Backspace
      -> Internally presses Shift + Backspace, which clears the current field.
  • Navigation and control

    • Esc
      -> Presses the NumWorks Back key.

    • Ctrl + T
      -> Presses Toolbox (Ctrl only, so it does not conflict with Cmd+T on macOS).

    • Ctrl + H
      -> Presses Home (again, Ctrl only to avoid Cmd+H / “Hide” on macOS).

In addition to these higher-level shortcuts, the script also maps normal keyboard input to the appropriate calculator keys:

  • Digits 0–9 to the corresponding NumWorks number keys.
  • Parentheses, decimal point, and arithmetic operators (+, -, *, /, ^) to their NumWorks keys.
  • Alphabetical characters (a–z), space, ?, and ! to the correct “alpha” keys and symbols on the device.
  • Brackets and comparison symbols:
    • [ , ], {, }, <, > mapped to the appropriate dedicated keys and their shifted variants.

For each of these, the script determines which data-key should be pressed and sends synthetic pointer events (pointerdown + pointerup) at the correct coordinates, so from the emulator’s perspective it looks identical to a real click on the on-screen keypad.

Visual keypress animations

To make the interaction feel closer to using a physical calculator, the script adds a visual feedback system on top of the NumWorks keypad.

  • When you press a key on your keyboard (letters, numbers, arrows, operations, etc.), the corresponding on-screen key is highlighted.
  • When you hold a key down, the highlight remains active for as long as the key is held, and it clears when you release it.
  • For combined actions (for example, a Shift-based operation or an Alpha-based operation), the script:
    • briefly highlights the modifier key (Shift or Alpha) in one color, and
    • highlights the target key in a different color, to distinguish the mode.

Color scheme:

  • Normal keys: neutral highlight color.
  • Shift-based actions (e.g. Copy, Paste, Clear, shifted brackets and comparison operators):
    • Shift key uses a yellow tone that matches the NumWorks Shift accent.
    • The associated key is highlighted in a distinct “shift-mode” color.
  • Alpha-based actions (letters, space, ?, !):
    • Alpha key is highlighted in a dedicated “alpha-mode” color.
    • The corresponding letter/symbol key is highlighted in a separate alpha color.

This makes it clear, at a glance, whether the script is applying a normal press, a Shift-modified operation, or an Alpha-modified input.


Why the WebKit arrow key bug occurs (and how this script handles it)

While the bulk of this file is about enhancing the user experience, it also contains a fix for the underlying WebKit arrow key issue.

  • In Chromium, arrow key events reach the NumWorks canvas in a “clean” state, so the emulator’s input handler processes them and calls preventDefault() itself.
  • In WebKit (Safari and WebKit-based wrappers), arrow key events are marked as already handled (event.defaultPrevented === true) by something earlier in the event chain (I'm not sure what yet. Maybe something accessibility related?), before the emulator’s handler runs.
  • The emulator appears to ignore key events when defaultPrevented is already true, so arrow keys effectively do nothing on Safari, even though other keys still work.

The script corrects this by:

  1. Listening for arrow key keydown / keyup at the capture phase on window, so it sees them before the code that calls preventDefault().
  2. Stopping the original event from propagating further.
  3. Creating a new synthetic keyboard event with the same key information, but allowing it to reach the NumWorks canvas without being marked as handled in advance.
  4. Dispatching that synthetic event directly to the simulator canvas, without letting it bubble back up and cause recursion.

As a result, the emulator receives arrow key events in the form it expects, and arrow navigation works in WebKit in the same way it does in Chromium.


Usage

JS Injection

For testing directly in a browser:

  1. Navigate to
    https://www.numworks.com/simulator/
  2. Open the JavaScript console. Safari: (Develop -> Show JavaScript Console); Chrome: (View -> Developer -> Developer Tools)
  3. Paste the contents of numworks-enhancements.js into the console.
  4. Press Enter.

After the script initializes:

  • Arrow keys should work on WebKit (specific to Safari users).
  • Keyboard shortcuts such as Cmd/Ctrl+C, Cmd/Ctrl+V, Cmd/Ctrl+Backspace, Ctrl+T, Ctrl+H, and Esc click the correct NumWorks keys.
  • The on-screen calculator shows clear visual highlights for each keypress and modifier combination.

Integrating as an injected script or userscript

  • In a Pake/Tauri-based desktop wrapper:
    Pass the file via --inject so that it is loaded automatically when the NumWorks simulator is shown.

  • As a userscript or extension:
    Wrap numworks-enhancements.js in a userscript or a minimal browser extension that injects it on page matching to https://www.numworks.com/simulator/*.

The script automatically checks that it is running on the simulator URL before initiating, so it is inert on other sites.


Enjoy! Please let me know if you found this useful, or have any suggested improvements.

// Numworks Simulator Enhancements
// Arrow key fix, keyboard shortcuts, and keypress indicator system
(function() {
// console.log('[Numworks] Installing complete enhancement suite...');
function initialize() {
// Check if we're on the simulator page
if (!window.location.href.includes('numworks.com/simulator')) {
// console.log('[Numworks] Not on simulator page, skipping...');
return;
}
const canvas = document.querySelector('canvas[tabindex]');
if (!canvas) {
// console.warn('[Numworks] Canvas not found, retrying in 500ms...');
setTimeout(initialize, 500);
return;
}
// console.log('[Numworks] Canvas found, installing enhancements...');
canvas.focus();
// ============================================
// Visual Feedback System with Color Coding
// ============================================
// Color schemes for different button modes (using Numworks yellow for shift!)
const COLORS = {
normal: 'rgba(0, 0, 0, .2)', // Default active color (gray)
shift: 'rgba(184, 143, 46, .4)', // Numworks shift yellow
alpha: 'rgba(100, 150, 255, .35)' // Light blue for alpha mode
};
// Track currently active keys to maintain held state
const activeKeys = new Map(); // dataKey -> { element, previous, color }
// Activate visual feedback on a button (for held keys)
function activateKey(dataKey, color = COLORS.normal) {
const el = document.querySelector(`.calculator span[data-key="${dataKey}"][data-v-09bb4f2f]`);
if (!el) {
// console.warn(`[Numworks] Visual feedback: element not found for data-key="${dataKey}"`);
return;
}
// If already active with same color, don't re-trigger
if (activeKeys.has(dataKey)) {
const existing = activeKeys.get(dataKey);
if (existing.color === color) return;
// Different color - update it
el.style.backgroundColor = color;
existing.color = color;
return;
}
// Save previous inline background (if any) so we can restore it
const previous = el.style.backgroundColor;
activeKeys.set(dataKey, { element: el, previous: previous, color: color });
// Apply the color
el.style.backgroundColor = color;
}
// Deactivate visual feedback (on keyup)
function deactivateKey(dataKey) {
const stored = activeKeys.get(dataKey);
if (!stored) return;
const { element, previous } = stored;
element.style.backgroundColor = previous;
activeKeys.delete(dataKey);
}
// Force clear all active states (safety mechanism)
function clearAllActiveKeys() {
activeKeys.forEach((stored, dataKey) => {
const { element, previous } = stored;
element.style.backgroundColor = previous;
});
activeKeys.clear();
}
// Flash visual feedback for simulated clicks (non-held)
function flashActiveKey(dataKey, color = COLORS.normal, durationMs = 120) {
const el = document.querySelector(`.calculator span[data-key="${dataKey}"][data-v-09bb4f2f]`);
if (!el) {
// console.warn(`[Numworks] Visual feedback: element not found for data-key="${dataKey}"`);
return;
}
// Save previous inline background
const previous = el.style.backgroundColor;
// Apply the color
el.style.backgroundColor = color;
setTimeout(() => {
el.style.backgroundColor = previous;
}, durationMs);
}
// Flash a combo (modifier + key with different colors)
function flashCombo(modifierKey, targetKey, modifierColor, targetColor, delay = 5) {
flashActiveKey(modifierKey, modifierColor);
setTimeout(() => {
flashActiveKey(targetKey, targetColor);
}, delay);
}
// ============================================
// Key Mappings
// ============================================
// Arrow keys to data-key mapping
const arrowKeyMap = {
'ArrowUp': 1,
'ArrowDown': 2,
'ArrowLeft': 0,
'ArrowRight': 3
};
// Number keys to data-key mapping
const numberKeyMap = {
'0': 48, '1': 42, '2': 43, '3': 44, '4': 36,
'5': 37, '6': 38, '7': 30, '8': 31, '9': 32
};
// Special keys to data-key mapping
const specialKeyMap = {
'Enter': 52,
'(': 33,
')': 34,
'.': 49,
'Backspace': 17,
'Delete': 17, // Same as backspace
'i': 21, // Imaginary number
',': 22
};
// Operation keys to data-key mapping
const operationKeyMap = {
'^': 23, // Exponent (Shift+6)
'*': 39, // Multiplication
'/': 40, // Divide
'+': 45, // Plus
'-': 46 // Minus
};
// Bracket keys (shift combos)
const bracketKeyMap = {
'[': 18, // Left bracket (shift combo)
']': 19, // Right bracket (shift combo)
'{': 20, // Left brace (shift combo)
'}': 21, // Right brace (shift combo)
'<': 28, // Less than (shift combo)
'>': 29 // Greater than (shift combo)
};
// Alphabetical keys (a-z) - CORRECTED with gaps!
// a-q: 18-34 (continuous)
// r-v: 36-40 (skip 35, skip 41)
// w-z: 42-45 (skip 47)
const alphaKeyMap = {
'a': 18, 'b': 19, 'c': 20, 'd': 21, 'e': 22, 'f': 23,
'g': 24, 'h': 25, 'i': 26, 'j': 27, 'k': 28, 'l': 29,
'm': 30, 'n': 31, 'o': 32, 'p': 33, 'q': 34,
// Gap at 35
'r': 36, 's': 37, 't': 38, 'u': 39, 'v': 40,
// Gap at 41
'w': 42, 'x': 43, 'y': 44, 'z': 45,
' ': 46, // Space
// Gap at 47
'?': 48, // Question mark
'!': 49 // Exclamation mark
};
// Shift combo keys that trigger special functions
const shiftComboKeys = {
15: 'var', // Copy
16: 'toolbox', // Paste
17: 'backspace', // Clear
18: 'leftBracket',
19: 'rightBracket',
20: 'leftBrace',
21: 'rightBrace',
28: 'lessThan',
29: 'greaterThan'
};
// Constants for modifier keys
const SHIFT_KEY = 12;
const ALPHA_KEY = 13;
// Track modifier key states to handle caps lock / spam
let shiftActive = false;
let alphaActive = false;
// ============================================
// Part 1: Arrow Key Fix (with visual feedback)
// ============================================
const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
let isRedispatching = false;
['keydown', 'keyup'].forEach(eventType => {
window.addEventListener(eventType, function(e) {
if (isRedispatching) return;
if (!arrowKeys.includes(e.key)) return;
// console.log(`[Numworks] Intercepted arrow ${eventType}: ${e.key}`);
const dataKey = arrowKeyMap[e.key];
// Handle visual feedback - activate on keydown, deactivate on keyup
if (eventType === 'keydown') {
activateKey(dataKey);
} else if (eventType === 'keyup') {
deactivateKey(dataKey);
}
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
isRedispatching = true;
const cleanEvent = new KeyboardEvent(eventType, {
key: e.key,
code: e.code,
keyCode: e.keyCode,
which: e.keyCode,
charCode: 0,
bubbles: false,
cancelable: true,
composed: false,
view: window,
detail: 0
});
canvas.dispatchEvent(cleanEvent);
isRedispatching = false;
}, true);
});
// ============================================
// Part 2: Intelligent Visual Feedback System
// ============================================
// Safety: Clear all states on window blur or focus loss
window.addEventListener('blur', clearAllActiveKeys);
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
clearAllActiveKeys();
}
});
// Determine if a key is an alpha key
function isAlphaKey(key) {
return alphaKeyMap.hasOwnProperty(key);
}
// Add visual feedback for all keys with intelligent color coding
window.addEventListener('keydown', function(e) {
// Ignore CapsLock to prevent stuck states
if (e.key === 'CapsLock') {
return;
}
let dataKey = null;
let color = COLORS.normal;
// Check for alpha keys (letters, space, ?, !)
// Only trigger if NOT using Ctrl/Cmd (to avoid conflicts with shortcuts)
if (isAlphaKey(e.key) && !e.ctrlKey && !e.metaKey) {
dataKey = alphaKeyMap[e.key];
color = COLORS.alpha;
// Activate alpha key first (only if not already active)
if (!alphaActive) {
activateKey(ALPHA_KEY, COLORS.normal);
alphaActive = true;
}
activateKey(dataKey, color);
return;
}
// Check for shift combo keys (brackets, braces, etc.)
if (bracketKeyMap.hasOwnProperty(e.key)) {
dataKey = bracketKeyMap[e.key];
color = COLORS.shift;
// Activate shift key first (only if not already active)
if (!shiftActive) {
activateKey(SHIFT_KEY, COLORS.normal);
shiftActive = true;
}
activateKey(dataKey, color);
return;
}
// Regular key detection
if (numberKeyMap[e.key]) {
dataKey = numberKeyMap[e.key];
} else if (specialKeyMap[e.key]) {
dataKey = specialKeyMap[e.key];
} else if (operationKeyMap[e.key]) {
dataKey = operationKeyMap[e.key];
}
// Activate visual feedback if we found a matching data-key
if (dataKey !== null) {
activateKey(dataKey, color);
}
}, false);
// Clear visual feedback on keyup
window.addEventListener('keyup', function(e) {
// Ignore CapsLock
if (e.key === 'CapsLock') {
return;
}
let dataKey = null;
// Check all key maps
if (isAlphaKey(e.key)) {
dataKey = alphaKeyMap[e.key];
// Deactivate the specific key
if (dataKey !== null) {
deactivateKey(dataKey);
}
// Clear alpha modifier state
deactivateKey(ALPHA_KEY);
alphaActive = false;
return;
}
if (bracketKeyMap.hasOwnProperty(e.key)) {
dataKey = bracketKeyMap[e.key];
// Deactivate the specific key
if (dataKey !== null) {
deactivateKey(dataKey);
}
// Clear shift modifier state
deactivateKey(SHIFT_KEY);
shiftActive = false;
return;
}
if (numberKeyMap[e.key]) {
dataKey = numberKeyMap[e.key];
} else if (specialKeyMap[e.key]) {
dataKey = specialKeyMap[e.key];
} else if (operationKeyMap[e.key]) {
dataKey = operationKeyMap[e.key];
}
if (dataKey !== null) {
deactivateKey(dataKey);
}
}, false);
// Extra safety: detect when modifiers are released directly
window.addEventListener('keyup', function(e) {
if (e.key === 'Shift') {
deactivateKey(SHIFT_KEY);
shiftActive = false;
} else if (e.key === 'Alt' || e.key === 'Control' || e.key === 'Meta') {
// Clear alpha on any modifier release as safety
deactivateKey(ALPHA_KEY);
alphaActive = false;
}
}, true);
// ============================================
// Part 3: Keyboard Shortcuts for Calculator Buttons
// ============================================
// Map of data-key values to button names (for reference)
const buttonMap = {
5: 'Back',
6: 'Home',
12: 'Shift',
13: 'Alpha',
15: 'Var',
16: 'Toolbox',
17: 'Backspace'
};
// Helper function to click a calculator button using pointer events
function clickButton(dataKey, showVisualFeedback = true, color = COLORS.normal) {
const button = document.querySelector(`[data-key="${dataKey}"]`);
if (!button) {
// console.warn(`[Numworks] Button not found: data-key="${dataKey}"`);
return false;
}
// Show visual feedback (flash for simulated clicks)
if (showVisualFeedback) {
flashActiveKey(dataKey, color);
}
// Get the center position of the button
const rect = button.getBoundingClientRect();
const clientX = rect.left + rect.width / 2;
const clientY = rect.top + rect.height / 2;
// Create pointer down event
const pointerDown = new PointerEvent("pointerdown", {
bubbles: true,
cancelable: true,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
clientX,
clientY
});
// Create pointer up event
const pointerUp = new PointerEvent("pointerup", {
bubbles: true,
cancelable: true,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
clientX,
clientY
});
// Dispatch the events
button.dispatchEvent(pointerDown);
button.dispatchEvent(pointerUp);
// console.log(`[Numworks] Clicked button: ${buttonMap[dataKey] || dataKey}`);
return true;
}
// Helper function to click two buttons in sequence with color coding
function clickSequence(modifierKey, targetKey, modifierColor = COLORS.normal, targetColor = COLORS.shift, delay = 5) {
clickButton(modifierKey, true, modifierColor);
setTimeout(() => clickButton(targetKey, true, targetColor), delay);
}
// Track if we're currently processing a shortcut to prevent duplicates
let processingShortcut = false;
// Keyboard shortcut handler
window.addEventListener('keydown', function(e) {
// Prevent duplicate processing
if (processingShortcut) {
// console.log('[Numworks] Already processing shortcut, skipping...');
return;
}
let handled = false;
// Escape: Back button
if (e.key === 'Escape') {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
processingShortcut = true;
// console.log('[Numworks] Shortcut: Back');
clickButton(5); // Back (5)
setTimeout(() => { processingShortcut = false; }, 100);
handled = true;
}
// Ctrl+Backspace or Cmd+Backspace: Shift + Backspace (Clear field)
else if ((e.ctrlKey || e.metaKey) && e.key === 'Backspace') {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
processingShortcut = true;
// console.log('[Numworks] Shortcut: Clear field (Shift+Backspace)');
clickSequence(SHIFT_KEY, 17, COLORS.normal, COLORS.shift, 5);
setTimeout(() => { processingShortcut = false; }, 100);
handled = true;
}
// Ctrl+C or Cmd+C: Shift + Var (Copy)
else if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
processingShortcut = true;
// console.log('[Numworks] Shortcut: Copy (Shift+Var)');
clickSequence(SHIFT_KEY, 15, COLORS.normal, COLORS.shift, 5);
setTimeout(() => { processingShortcut = false; }, 100);
handled = true;
}
// Ctrl+V or Cmd+V: Shift + Toolbox (Paste)
else if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
processingShortcut = true;
// console.log('[Numworks] Shortcut: Paste (Shift+Toolbox)');
clickSequence(SHIFT_KEY, 16, COLORS.normal, COLORS.shift, 5);
setTimeout(() => { processingShortcut = false; }, 100);
handled = true;
}
// Ctrl+T (NOT Cmd+T to avoid new tab on macOS): Toolbox
else if (e.ctrlKey && !e.metaKey && e.key === 't') {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
processingShortcut = true;
// console.log('[Numworks] Shortcut: Toolbox');
clickButton(16); // Toolbox (16)
setTimeout(() => { processingShortcut = false; }, 100);
handled = true;
}
// Ctrl+H (NOT Cmd+H to avoid hide on macOS): Home
else if (e.ctrlKey && !e.metaKey && e.key === 'h') {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
processingShortcut = true;
// console.log('[Numworks] Shortcut: Home');
clickButton(6); // Home (6)
setTimeout(() => { processingShortcut = false; }, 100);
handled = true;
}
if (handled) {
// console.log('[Numworks] Shortcut handled successfully');
}
}, true); // Capture phase to catch it early
}
// Initialize when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
setTimeout(initialize, 1000);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment