fix(web-ssh): send the physical key, and drop a protocol message nobody sends

Two things the key testing turned up.

Alt was wrong on macOS. Option+a arrives as the composed character "å", so
sending e.key put that in the shell instead of Meta-a. Both Alt and Ctrl now
take the letter from e.code (the physical key), falling back to e.key when a
browser does not report it; non-letters keep the plain ASCII mapping, which is
what makes Ctrl+[ an Escape.

The "bye" message was dead: the page never sends it, so the handler arm could
never run.
This commit is contained in:
bluebluesoda committed 2026-09-17 12:33:00 +00:00
1 parent 5e28fc2983
commit 69f1e3a47b
2 files changed
+17 -10

No files matched your search

+16 -4
View File
@@ -605,11 +605,23 @@
var ss3 = { 1: "P", 2: "Q", 3: "R", 4: "S" };
send = ss3[f] ? "\x1bO" + ss3[f] : "\x1b[" + [15, 17, 18, 19, 20, 21, 23, 24][f - 5] + "~";
} else if (e.ctrlKey && !e.altKey && e.key.length === 1) {
var c = e.key.toUpperCase().charCodeAt(0);
if (c >= 64 && c < 128) send = String.fromCharCode(c & 0x1f);
// The physical key is what a terminal wants: on a layout where Ctrl+A
// composes into something else, e.key is that something else.
var ck = /^Key([A-Z])$/.exec(e.code || "");
if (ck) send = String.fromCharCode(ck[1].charCodeAt(0) - 64);
else if (e.key === " ") send = "\x00";
} else if (e.altKey && e.key.length === 1) {
send = "\x1b" + e.key;
else {
// Non-letters keep the plain ASCII mapping, which is what makes
// Ctrl+[ an Escape and Ctrl+6 Ctrl+^.
var cc = e.key.toUpperCase().charCodeAt(0);
if (cc >= 64 && cc < 128) send = String.fromCharCode(cc & 0x1f);
}
} else if (e.altKey && !e.metaKey) {
// Again the physical key: on macOS Option+a arrives as "å", so sending
// e.key would put that in the shell instead of Meta-a.
var ak = /^Key([A-Z])$/.exec(e.code || "");
if (ak) send = "\x1b" + ak[1].toLowerCase();
else if (e.key.length === 1) send = "\x1b" + e.key;
} else if (!e.ctrlKey && !e.metaKey && e.key.length === 1) {
send = e.key;
}
+1 -6
View File
@@ -58,7 +58,7 @@ var termJS []byte
// termIn is a message from the browser. Terminal output travels the other way
// as raw binary frames; only control messages are JSON.
type termIn struct {
T string `json:"t"` // hello | input | resize | takeover | cancel | bye
T string `json:"t"` // hello | input | resize | takeover
Token string `json:"token,omitempty"`
Data string `json:"data,omitempty"`
Cols int `json:"cols,omitempty"`
@@ -557,11 +557,6 @@ func (s *Server) serveTermClient(conn *websocket.Conn, sock *termSocket, sess *t
_ = s.db.AddAuditLog("000+"+user, "terminal.takeover")
}
s.notifyOthers(user)
case "bye":
// The window is going away for good: end the shell now rather than
// holding it for the grace period.
sess.close()
return
}
}
}