authorgravatar for chris@xenforo.comChris Deeming <chris@xenforo.com> 2026-08-21 06:08:50+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-20 22:08:50-07:00
log18f6d034897d9cc5429dcb88c6a9795d183f1687
tree6dd9b275708c619ad2700d91aa59ef677865c18f
parent6d5c6c4a671c7b6fc9723c63c1003916fb3b8ab0
signature Signed by PGP key B5690EEEBB952194

fix(web): encode shifted characters correctly in the terminal (#7485)


3 files changed, 28 insertions(+), 2 deletions(-)

apps/web/src/terminal/ghostty/core.ts+6-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 type GhosttyKeyboardLayoutMap,2 type GhosttyKeyboardLayoutMap,
3 ghosttyConsumedMods,
3 ghosttyKeyForCode,4 ghosttyKeyForCode,
4 ghosttyUnshiftedCodepoint,5 ghosttyUnshiftedCodepoint,
5 loadGhosttyKeyboardLayoutMap,6 loadGhosttyKeyboardLayoutMap,
...@@ -464,7 +465,11 @@ export class GhosttyTerminalCore {...@@ -464,7 +465,11 @@ export class GhosttyTerminalCore {
464 (event.getModifierState("CapsLock") ? 1 << 4 : 0) |465 (event.getModifierState("CapsLock") ? 1 << 4 : 0) |
465 (event.getModifierState("NumLock") ? 1 << 5 : 0);466 (event.getModifierState("NumLock") ? 1 << 5 : 0);
466 this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods);467 this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods);
467 this.runtime.call("ghostty_key_event_set_consumed_mods", this.keyEvent, 0);468 this.runtime.call(
469 "ghostty_key_event_set_consumed_mods",
470 this.keyEvent,
471 ghosttyConsumedMods(event),
472 );
468 this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0);473 this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0);
469 this.runtime.call(474 this.runtime.call(
470 "ghostty_key_event_set_unshifted_codepoint",475 "ghostty_key_event_set_unshifted_codepoint",
apps/web/src/terminal/ghostty/keyCodes.test.ts+13-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
22
3import { ghosttyKeyForCode, ghosttyUnshiftedCodepoint } from "./keyCodes";3import { ghosttyConsumedMods, ghosttyKeyForCode, ghosttyUnshiftedCodepoint } from "./keyCodes";
44
5describe("ghosttyKeyForCode", () => {5describe("ghosttyKeyForCode", () => {
6 it("keeps the tail of the pinned Ghostty key enum in order", () => {6 it("keeps the tail of the pinned Ghostty key enum in order", () => {
...@@ -11,6 +11,18 @@ describe("ghosttyKeyForCode", () => {...@@ -11,6 +11,18 @@ describe("ghosttyKeyForCode", () => {
11 });11 });
12});12});
1313
14describe("ghosttyConsumedMods", () => {
15 const shifted = { altKey: false, ctrlKey: false, key: "@", metaKey: false, shiftKey: true };
16
17 it("only consumes a lone Shift producing a character", () => {
18 expect(ghosttyConsumedMods(shifted)).toBe(1);
19 expect(ghosttyConsumedMods({ ...shifted, ctrlKey: true })).toBe(0);
20 expect(ghosttyConsumedMods({ ...shifted, key: "Tab" })).toBe(0);
21 // Deliberate: Shift+Space collapses to Space so it still types one.
22 expect(ghosttyConsumedMods({ ...shifted, key: " " })).toBe(1);
23 });
24});
25
14describe("ghosttyUnshiftedCodepoint", () => {26describe("ghosttyUnshiftedCodepoint", () => {
15 it("provides the logical base character for Kitty keyboard encoding", () => {27 it("provides the logical base character for Kitty keyboard encoding", () => {
16 expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "c", shiftKey: false })).toBe(28 expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "c", shiftKey: false })).toBe(
apps/web/src/terminal/ghostty/keyCodes.ts+9
...@@ -233,6 +233,15 @@ export function loadGhosttyKeyboardLayoutMap(): Promise<GhosttyKeyboardLayoutMap...@@ -233,6 +233,15 @@ export function loadGhosttyKeyboardLayoutMap(): Promise<GhosttyKeyboardLayoutMap
233 return promise;233 return promise;
234}234}
235235
236// Browsers do not expose consumed modifiers; treat Shift as consumed for
237// unchorded character input.
238export function ghosttyConsumedMods(
239 event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey">,
240): number {
241 if (!event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return 0;
242 return [...event.key].length === 1 ? 1 : 0;
243}
244
236export function ghosttyUnshiftedCodepoint(245export function ghosttyUnshiftedCodepoint(
237 event: Pick<KeyboardEvent, "code" | "key" | "shiftKey">,246 event: Pick<KeyboardEvent, "code" | "key" | "shiftKey">,
238 layoutMap?: GhosttyKeyboardLayoutMap,247 layoutMap?: GhosttyKeyboardLayoutMap,