Skip to content

Commit 379ea7d

Browse files
telnet naws and arrow keys
1 parent 8877bff commit 379ea7d

2 files changed

Lines changed: 79 additions & 11 deletions

File tree

src/kernel/term_ctl.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,9 @@ export abstract class AbstractTerminal {
449449

450450
protected _simulate_typing(text: string) {
451451
// simulate key events for each character (lazy but it works great, no need to rewrite the key handler)
452-
for (const char of text) {
452+
for (let i = 0; i < text.length; i++) {
453+
const char = text[i];
454+
453455
let dom_event_code = `Key${char.toUpperCase()}`;
454456
let key = char;
455457

@@ -467,6 +469,35 @@ export abstract class AbstractTerminal {
467469
dom_event_code = "Space";
468470
}
469471

472+
// check for escape sequences e.g. arrow keys
473+
if (char === "\x1b") {
474+
if (i + 2 < text.length && text[i + 1] === "[") {
475+
const code = text[i + 2];
476+
let key_name = "";
477+
478+
switch (code) {
479+
case "A":
480+
key_name = "ArrowUp";
481+
break;
482+
case "B":
483+
key_name = "ArrowDown";
484+
break;
485+
case "C":
486+
key_name = "ArrowRight";
487+
break;
488+
case "D":
489+
key_name = "ArrowLeft";
490+
break;
491+
}
492+
493+
if (key_name) {
494+
dom_event_code = key_name;
495+
key = `\x1b[${code}`;
496+
i += 2; // skip the next two characters as they are part of the escape sequence
497+
}
498+
}
499+
}
500+
470501
this.#key_event_queue.push(({ key, domEvent: { code: dom_event_code } } as KeyEvent));
471502
}
472503

@@ -604,6 +635,7 @@ export abstract class AbstractTerminal {
604635
// TODO: term needs hardening and possibly userspace protection to ensure programs cant dispatch keys to auto accept elevation prompts
605636
// TODO: have now made key dispatch methods private, but need to prevent stuff like handle_kernel_panic being called by userspace programs
606637
// TODO: main thing is that when writing the proxy we need to think what xterm methods we expose to userspace programs
638+
// TODO: emit resize events
607639

608640
// as of 09/01/2026, the god class of WrappedTerminal is no more!
609641
// this used to be the kernel, shell, tty, and bootstrap all in one

src/programs/telnetd.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ class TelnetTerminal extends AbstractTerminal {
66
#x = 0;
77
#y = 0;
88

9+
#cols = 80;
10+
#rows = 24;
11+
912
#socket: UserspaceClientSocket;
1013

1114
input_enabled = true;
@@ -19,13 +22,11 @@ class TelnetTerminal extends AbstractTerminal {
1922
}
2023

2124
get rows() {
22-
// TODO: negotiate using telnet messages
23-
return 24;
25+
return this.#rows;
2426
}
2527

2628
get cols() {
27-
// TODO: negotiate using telnet messages
28-
return 80;
29+
return this.#cols;
2930
}
3031

3132
constructor(socket: UserspaceClientSocket) {
@@ -54,20 +55,52 @@ class TelnetTerminal extends AbstractTerminal {
5455
}
5556

5657
#on_socket_data = (data: Uint8Array) => {
57-
if (!this.input_enabled) {
58-
return;
59-
}
60-
61-
// filter out telnet commands (starting with 0xFF) for now
58+
// parse telnet commands and filter them out
6259
const filtered_data = new Uint8Array(data.length);
6360
let j = 0;
6461
for (let i = 0; i < data.length; i++) {
62+
// interpret as command (IAC)
6563
if (data[i] === 0xFF) {
66-
i += 2; // skip the command and its option
64+
const command = data[i + 1];
65+
66+
// subnegotiation (SB)
67+
if (command === 0xFA) {
68+
const option = data[i + 2];
69+
70+
// negotiate about window size (NAWS)
71+
if (option === 0x1F) {
72+
const cols = (data[i + 3] << 8) | data[i + 4];
73+
const rows = (data[i + 5] << 8) | data[i + 6];
74+
75+
this.#cols = cols;
76+
this.#rows = rows;
77+
}
78+
79+
// consume until subnegotiation end (SE)
80+
while (i < data.length) {
81+
if (data[i] === 0xFF && data[i + 1] === 0xF0) {
82+
i += 1;
83+
break;
84+
}
85+
i++;
86+
}
87+
} else if (command === 0xFF) {
88+
// escaped iac, the actual data is 0xFF
89+
filtered_data[j++] = 0xFF;
90+
i++;
91+
} else {
92+
// skip standard 3 byte commands (not implemented)
93+
i += 2;
94+
}
6795
} else {
6896
filtered_data[j++] = data[i];
6997
}
7098
}
99+
100+
if (!this.input_enabled) {
101+
return;
102+
}
103+
71104
const final_data = filtered_data.slice(0, j);
72105

73106
const text = new TextDecoder().decode(final_data);
@@ -180,6 +213,9 @@ export default {
180213

181214
// dont linemode
182215
255, 252, 34,
216+
217+
// do naws
218+
255, 253, 31
183219
]));
184220

185221
await socket.send("\r\nWelcome to the OllieOS Telnet service!\r\n\r\n");

0 commit comments

Comments
 (0)