coneko-term 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ import { type KeyEventLike } from "./keyboard.js";
2
+ /** 位置と大きさを決めるのに使う `style` の部分。 */
3
+ export interface StyleLike {
4
+ left: string;
5
+ top: string;
6
+ width: string;
7
+ height: string;
8
+ lineHeight: string;
9
+ fontFamily: string;
10
+ fontSize: string;
11
+ }
12
+ /** 見えない `textarea` のうち、ここで使う部分。 */
13
+ export interface TextareaLike {
14
+ value: string;
15
+ readonly style: StyleLike;
16
+ }
17
+ /** 変換中の字を見せる要素のうち、ここで使う部分。 */
18
+ export interface CompositionViewLike {
19
+ textContent: string | null;
20
+ readonly style: StyleLike;
21
+ getBoundingClientRect(): {
22
+ width: number;
23
+ height: number;
24
+ };
25
+ show(): void;
26
+ hide(): void;
27
+ }
28
+ /** 変換中の字を出す位置(欄の左上からの CSS ピクセル)。見せないなら `undefined`。 */
29
+ export interface CompositionAnchor {
30
+ left: number;
31
+ top: number;
32
+ cellHeight: number;
33
+ }
34
+ /**
35
+ * 変換(IME)の間は送らず、確定した字を送る。変換中の字は、カーソルの位置に見せる。
36
+ * xterm.js の `CompositionHelper` と同じ流れ。
37
+ */
38
+ export declare class CompositionHelper {
39
+ private readonly textarea;
40
+ private readonly view;
41
+ private readonly send;
42
+ private readonly anchor;
43
+ private readonly font;
44
+ private readonly schedule;
45
+ private composing;
46
+ private sending;
47
+ private position;
48
+ private dataAlreadySent;
49
+ constructor(textarea: TextareaLike, view: CompositionViewLike, send: (data: string) => void, anchor: () => CompositionAnchor | undefined, font: () => {
50
+ family: string;
51
+ size: number;
52
+ }, schedule?: (f: () => void) => void);
53
+ get isComposing(): boolean;
54
+ compositionstart(): void;
55
+ compositionupdate(data: string): void;
56
+ compositionend(): void;
57
+ /**
58
+ * キーが押されたとき、変換のためのキーなら `false`(呼ぶ側は何もしない)、
59
+ * 普通のキーなら `true`。変換の途中で普通のキーが来たら、先に変換を確定する。
60
+ */
61
+ keydown(ev: Pick<KeyEventLike, "keyCode">): boolean;
62
+ private finalize;
63
+ private handleAnyTextareaChanges;
64
+ /** 変換中の字を、カーソルの位置へ動かす。`textarea` も同じ位置に置く(IME の候補の窓が出る場所)。 */
65
+ updateCompositionElements(dontRecurse?: boolean): void;
66
+ }
67
+ /** `KeyHandler` が読む、プラットフォームとモード。 */
68
+ export interface KeyHandlerHost {
69
+ /** PTY へ送る(ユーザーが打った入力として)。 */
70
+ send(data: string): void;
71
+ /** DECCKM(#5)。 */
72
+ applicationCursorKeys(): boolean;
73
+ /** kitty keyboard protocol のいまのフラグ。 */
74
+ keyboardFlags(): number;
75
+ /** 表示位置を `n` ページ動かす(負で上)。 */
76
+ scrollPages(n: number): void;
77
+ /** 一番下へ戻す(`scrollOnUserInput` が true のとき)。 */
78
+ scrollToBottom(): void;
79
+ readonly isMac: boolean;
80
+ readonly isWindows: boolean;
81
+ readonly macOptionIsMeta: boolean;
82
+ /** `false` を返すと、ターミナルはそのキーを処理しない(xterm.js の `attachCustomKeyEventHandler`)。 */
83
+ customKeyHandler(): ((ev: KeyboardEvent) => boolean) | undefined;
84
+ }
85
+ /** イベントのうち、ここで触る部分。 */
86
+ type KeyDownLike = KeyEventLike & {
87
+ readonly type?: string;
88
+ preventDefault(): void;
89
+ stopPropagation(): void;
90
+ getModifierState?(key: string): boolean;
91
+ };
92
+ /**
93
+ * `textarea` の `keydown`・`keypress`・`keyup`・`input` と変換のイベントを受けて、送る字を決める。
94
+ * キーを送るバイト列は `evaluateKeyboardEvent`(xterm.js 6.0.0 と同じ)。
95
+ */
96
+ export declare class KeyHandler {
97
+ private readonly host;
98
+ private readonly textarea;
99
+ readonly composition: CompositionHelper;
100
+ private keyDownSeen;
101
+ private keyPressHandled;
102
+ private unprocessedDeadKey;
103
+ private readonly reported;
104
+ constructor(host: KeyHandlerHost, textarea: TextareaLike, composition: CompositionHelper);
105
+ private cancel;
106
+ /** macOS の Option(`macOptionIsMeta` が false のとき)と、Windows の AltGr は、字を打つキー。 */
107
+ private isThirdLevelShift;
108
+ keyDown(ev: KeyDownLike): boolean | undefined;
109
+ /** 押したままフォーカスを失うと keyup が来ないので、`textarea` の blur で呼ぶ。 */
110
+ reset(): void;
111
+ keyUp(ev: KeyDownLike): void;
112
+ keyPress(ev: KeyDownLike & {
113
+ readonly charCode: number;
114
+ readonly which?: number | null;
115
+ }): boolean;
116
+ /** `input` イベント。絵文字の IME など、keydown・keypress を通らない字を送る。 */
117
+ input(ev: {
118
+ readonly data: string | null;
119
+ readonly inputType: string;
120
+ readonly composed: boolean;
121
+ preventDefault(): void;
122
+ stopPropagation(): void;
123
+ }): boolean;
124
+ }
125
+ /** `navigator` から、macOS か・Windows かを決める(xterm.js の `Browser.isMac`・`isWindows`)。 */
126
+ export declare function detectPlatform(): {
127
+ isMac: boolean;
128
+ isWindows: boolean;
129
+ };
130
+ export {};
package/dist/input.js ADDED
@@ -0,0 +1,313 @@
1
+ // キーボードと IME の入力を受ける部分。xterm.js 6.0.0 の `CoreBrowserTerminal` の
2
+ // `_keyDown`・`_keyPress`・`_inputEvent` と `CompositionHelper` の動きに合わせる。
3
+ // DOM の要素そのものではなく、使う部分だけの形(`TextareaLike` など)に頼るので、DOM の無い
4
+ // テストでも動かせる。
5
+ import { evaluateKeyboardEvent, KITTY_REPORT_EVENT_TYPES, } from "./keyboard.js";
6
+ /**
7
+ * 変換(IME)の間は送らず、確定した字を送る。変換中の字は、カーソルの位置に見せる。
8
+ * xterm.js の `CompositionHelper` と同じ流れ。
9
+ */
10
+ export class CompositionHelper {
11
+ textarea;
12
+ view;
13
+ send;
14
+ anchor;
15
+ font;
16
+ schedule;
17
+ composing = false;
18
+ sending = false;
19
+ position = { start: 0, end: 0 };
20
+ dataAlreadySent = "";
21
+ constructor(textarea, view, send, anchor, font, schedule = (f) => void setTimeout(f, 0)) {
22
+ this.textarea = textarea;
23
+ this.view = view;
24
+ this.send = send;
25
+ this.anchor = anchor;
26
+ this.font = font;
27
+ this.schedule = schedule;
28
+ }
29
+ get isComposing() {
30
+ return this.composing;
31
+ }
32
+ compositionstart() {
33
+ this.composing = true;
34
+ this.position.start = this.textarea.value.length;
35
+ this.view.textContent = "";
36
+ this.dataAlreadySent = "";
37
+ this.view.show();
38
+ }
39
+ compositionupdate(data) {
40
+ this.view.textContent = data;
41
+ this.updateCompositionElements();
42
+ this.schedule(() => {
43
+ this.position.end = this.textarea.value.length;
44
+ });
45
+ }
46
+ compositionend() {
47
+ this.finalize(true);
48
+ }
49
+ /**
50
+ * キーが押されたとき、変換のためのキーなら `false`(呼ぶ側は何もしない)、
51
+ * 普通のキーなら `true`。変換の途中で普通のキーが来たら、先に変換を確定する。
52
+ */
53
+ keydown(ev) {
54
+ if (this.composing || this.sending) {
55
+ // 20 は CapsLock、229 は変換中のキー
56
+ if (ev.keyCode === 20 || ev.keyCode === 229)
57
+ return false;
58
+ // 修飾キーだけなら変換を続ける
59
+ if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
60
+ return false;
61
+ }
62
+ // Enter などで、コマンドが走る前に確定した字を送る
63
+ this.finalize(false);
64
+ }
65
+ if (ev.keyCode === 229) {
66
+ // 変換の字ではない字(数字や記号)が、IME が有効なまま打たれた
67
+ this.handleAnyTextareaChanges();
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+ finalize(waitForPropagation) {
73
+ this.view.hide();
74
+ this.composing = false;
75
+ if (!waitForPropagation) {
76
+ this.sending = false;
77
+ const input = this.textarea.value.substring(this.position.start, this.position.end);
78
+ this.send(input);
79
+ return;
80
+ }
81
+ // 多くのブラウザーで、composition のイベントは `textarea` の値が変わる前に来る。
82
+ // `compositionend` の `data` はあてにならないので、一度待って値を読む
83
+ const current = { ...this.position };
84
+ this.sending = true;
85
+ this.schedule(() => {
86
+ if (!this.sending)
87
+ return;
88
+ this.sending = false;
89
+ // keydown で先に送った分を引く(二重に打たれるのを防ぐ)
90
+ current.start += this.dataAlreadySent.length;
91
+ const input = this.composing
92
+ ? this.textarea.value.substring(current.start, this.position.start)
93
+ : this.textarea.value.substring(current.start);
94
+ if (input.length > 0)
95
+ this.send(input);
96
+ });
97
+ }
98
+ handleAnyTextareaChanges() {
99
+ const oldValue = this.textarea.value;
100
+ this.schedule(() => {
101
+ if (this.composing)
102
+ return;
103
+ const newValue = this.textarea.value;
104
+ const diff = newValue.replace(oldValue, "");
105
+ this.dataAlreadySent = diff;
106
+ if (newValue.length > oldValue.length)
107
+ this.send(diff);
108
+ else if (newValue.length < oldValue.length)
109
+ this.send("\x7f");
110
+ else if (newValue !== oldValue)
111
+ this.send(newValue);
112
+ });
113
+ }
114
+ /** 変換中の字を、カーソルの位置へ動かす。`textarea` も同じ位置に置く(IME の候補の窓が出る場所)。 */
115
+ updateCompositionElements(dontRecurse = false) {
116
+ if (!this.composing)
117
+ return;
118
+ const a = this.anchor();
119
+ if (a) {
120
+ const f = this.font();
121
+ const vs = this.view.style;
122
+ vs.left = `${a.left}px`;
123
+ vs.top = `${a.top}px`;
124
+ vs.height = `${a.cellHeight}px`;
125
+ vs.lineHeight = `${a.cellHeight}px`;
126
+ vs.fontFamily = f.family;
127
+ vs.fontSize = `${f.size}px`;
128
+ const b = this.view.getBoundingClientRect();
129
+ const ts = this.textarea.style;
130
+ ts.left = `${a.left}px`;
131
+ ts.top = `${a.top}px`;
132
+ // 1x1 より小さいと動かない IME がある
133
+ ts.width = `${Math.max(b.width, 1)}px`;
134
+ ts.height = `${Math.max(b.height, 1)}px`;
135
+ ts.lineHeight = `${b.height}px`;
136
+ }
137
+ if (!dontRecurse)
138
+ this.schedule(() => this.updateCompositionElements(true));
139
+ }
140
+ }
141
+ /**
142
+ * `textarea` の `keydown`・`keypress`・`keyup`・`input` と変換のイベントを受けて、送る字を決める。
143
+ * キーを送るバイト列は `evaluateKeyboardEvent`(xterm.js 6.0.0 と同じ)。
144
+ */
145
+ export class KeyHandler {
146
+ host;
147
+ textarea;
148
+ composition;
149
+ keyDownSeen = false;
150
+ keyPressHandled = false;
151
+ unprocessedDeadKey = false;
152
+ // keydown で `CSI` の形で送ったキー。離すを送るのは、これに入っているキーだけ。
153
+ // 変換の確定の Enter のように、押したのを送っていないキーの離すだけを送ってしまわないため
154
+ reported = new Set();
155
+ constructor(host, textarea, composition) {
156
+ this.host = host;
157
+ this.textarea = textarea;
158
+ this.composition = composition;
159
+ }
160
+ cancel(ev) {
161
+ ev.preventDefault();
162
+ ev.stopPropagation();
163
+ }
164
+ /** macOS の Option(`macOptionIsMeta` が false のとき)と、Windows の AltGr は、字を打つキー。 */
165
+ isThirdLevelShift(ev) {
166
+ const h = this.host;
167
+ const thirdLevel = (h.isMac && !h.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
168
+ (h.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey) ||
169
+ (h.isWindows && !!ev.getModifierState?.("AltGraph"));
170
+ if (ev.type === "keypress")
171
+ return thirdLevel;
172
+ // 矢印や Backspace などは、keydown では三段目のシフトとして扱わない
173
+ return thirdLevel && (!ev.keyCode || ev.keyCode > 47);
174
+ }
175
+ keyDown(ev) {
176
+ this.keyDownSeen = true;
177
+ const custom = this.host.customKeyHandler();
178
+ if (custom && custom(ev) === false)
179
+ return false;
180
+ const h = this.host;
181
+ // macOptionIsMeta のとき、Option の変換は使わない
182
+ const ignoreComposition = h.isMac && h.macOptionIsMeta && ev.altKey;
183
+ if (!ignoreComposition && !this.composition.keydown(ev)) {
184
+ // 変換のキー。変換中の字は一番下のカーソルの位置に出すので、さかのぼっていたら戻す(xterm.js と同じ)
185
+ h.scrollToBottom();
186
+ return false;
187
+ }
188
+ // Cmd を押したキーは送らない(アプリのショートカットに任せる)。変換の確定のあとで判断する
189
+ if (h.isMac && ev.metaKey)
190
+ return true;
191
+ if (!ignoreComposition && (ev.key === "Dead" || ev.key === "AltGraph")) {
192
+ this.unprocessedDeadKey = true;
193
+ }
194
+ const flags = h.keyboardFlags();
195
+ const result = evaluateKeyboardEvent(ev, h.applicationCursorKeys(), h.isMac, h.macOptionIsMeta, flags, ev.repeat ? 2 : 1);
196
+ if (result.type === "pageUp" || result.type === "pageDown") {
197
+ h.scrollPages(result.type === "pageUp" ? -1 : 1);
198
+ this.cancel(ev);
199
+ return false;
200
+ }
201
+ if (this.isThirdLevelShift(ev))
202
+ return true;
203
+ if (result.cancel)
204
+ this.cancel(ev);
205
+ if (!result.key)
206
+ return true;
207
+ // 大文字は keypress で処理する(CapsLock のとき小文字が打てない macOS の IME への対策)。
208
+ // kitty の `CSI` の形で送るキーは keypress を通らないので、ここで送る
209
+ if (result.key === ev.key &&
210
+ !ev.ctrlKey &&
211
+ !ev.altKey &&
212
+ !ev.metaKey &&
213
+ ev.key.length === 1) {
214
+ const c = ev.key.charCodeAt(0);
215
+ if (c >= 65 && c <= 90)
216
+ return true;
217
+ }
218
+ if (this.unprocessedDeadKey) {
219
+ this.unprocessedDeadKey = false;
220
+ return true;
221
+ }
222
+ // xterm.js と同じく、Ctrl+C と Enter のあとは `textarea` に字をためない
223
+ if (result.key === "\x03" ||
224
+ result.key === "\r" ||
225
+ ev.keyCode === 13 ||
226
+ (ev.ctrlKey && ev.keyCode === 67)) {
227
+ this.textarea.value = "";
228
+ }
229
+ // アプリケーションカーソルキーの `ESC O A` などもここに入れる
230
+ if (flags & KITTY_REPORT_EVENT_TYPES &&
231
+ (result.key.startsWith("\x1b[") || result.key.startsWith("\x1bO"))) {
232
+ this.reported.add(keyId(ev));
233
+ }
234
+ h.send(result.key);
235
+ this.cancel(ev);
236
+ return false;
237
+ }
238
+ /** 押したままフォーカスを失うと keyup が来ないので、`textarea` の blur で呼ぶ。 */
239
+ reset() {
240
+ this.reported.clear();
241
+ }
242
+ keyUp(ev) {
243
+ this.keyDownSeen = false;
244
+ const custom = this.host.customKeyHandler();
245
+ if (custom && custom(ev) === false)
246
+ return;
247
+ this.keyPressHandled = false;
248
+ // 離す(kitty のフラグ 2)。押したのを `CSI` の形で送ったキーだけ
249
+ if (!this.reported.delete(keyId(ev)))
250
+ return;
251
+ const h = this.host;
252
+ const flags = h.keyboardFlags();
253
+ if (!(flags & KITTY_REPORT_EVENT_TYPES))
254
+ return;
255
+ // 変換の途中の keyup は送らない(変換の動きは変えない)
256
+ if (this.composition.isComposing || ev.keyCode === 229)
257
+ return;
258
+ const result = evaluateKeyboardEvent(ev, h.applicationCursorKeys(), h.isMac, h.macOptionIsMeta, flags, 3);
259
+ if (result.key)
260
+ h.send(result.key);
261
+ }
262
+ keyPress(ev) {
263
+ this.keyPressHandled = false;
264
+ const custom = this.host.customKeyHandler();
265
+ if (custom && custom(ev) === false)
266
+ return false;
267
+ this.cancel(ev);
268
+ let key;
269
+ if (ev.charCode)
270
+ key = ev.charCode;
271
+ else if (ev.which === null || ev.which === undefined)
272
+ key = ev.keyCode;
273
+ else if (ev.which !== 0 && ev.charCode !== 0)
274
+ key = ev.which;
275
+ else
276
+ return false;
277
+ if (!key || ((ev.altKey || ev.ctrlKey || ev.metaKey) && !this.isThirdLevelShift(ev))) {
278
+ return false;
279
+ }
280
+ this.host.send(String.fromCharCode(key));
281
+ this.keyPressHandled = true;
282
+ this.unprocessedDeadKey = false;
283
+ return true;
284
+ }
285
+ /** `input` イベント。絵文字の IME など、keydown・keypress を通らない字を送る。 */
286
+ input(ev) {
287
+ if (ev.data &&
288
+ ev.inputType === "insertText" &&
289
+ (!ev.composed || !this.keyDownSeen)) {
290
+ if (this.keyPressHandled)
291
+ return false;
292
+ this.unprocessedDeadKey = false;
293
+ this.host.send(ev.data);
294
+ this.cancel(ev);
295
+ return true;
296
+ }
297
+ return false;
298
+ }
299
+ }
300
+ /** 押したキーと離したキーを結びつける印。 */
301
+ function keyId(ev) {
302
+ return ev.code || String(ev.keyCode);
303
+ }
304
+ /** `navigator` から、macOS か・Windows かを決める(xterm.js の `Browser.isMac`・`isWindows`)。 */
305
+ export function detectPlatform() {
306
+ const nav = globalThis.navigator;
307
+ const platform = nav?.platform ?? "";
308
+ const ua = nav?.userAgent ?? "";
309
+ return {
310
+ isMac: ["Macintosh", "MacIntel", "MacPPC", "Mac68K"].includes(platform),
311
+ isWindows: ["Windows", "Win16", "Win32", "WinCE"].includes(platform) || /Windows/.test(ua),
312
+ };
313
+ }
@@ -0,0 +1,46 @@
1
+ /** `evaluateKeyboardEvent` が読むキーボードイベントの部分。`KeyboardEvent` がそのまま入る。 */
2
+ export interface KeyEventLike {
3
+ readonly keyCode: number;
4
+ readonly key: string;
5
+ readonly code: string;
6
+ readonly shiftKey: boolean;
7
+ readonly altKey: boolean;
8
+ readonly ctrlKey: boolean;
9
+ readonly metaKey: boolean;
10
+ /** 押しっぱなしの繰り返しか(`KeyboardEvent.repeat`)。 */
11
+ readonly repeat?: boolean;
12
+ }
13
+ /** `evaluateKeyboardEvent` の結果の種類。 */
14
+ export type KeyResultType = "send" | "pageUp" | "pageDown";
15
+ /** `evaluateKeyboardEvent` の結果。 */
16
+ export interface KeyResult {
17
+ /** `send` なら `key` を送る。`pageUp`・`pageDown` は表示位置を 1 ページ動かす(Shift+PageUp・PageDown)。 */
18
+ type: KeyResultType;
19
+ /** イベントの既定の動きを止めるか。 */
20
+ cancel: boolean;
21
+ /** 送る文字列。送らないなら `undefined`。 */
22
+ key: string | undefined;
23
+ }
24
+ import { type KeyEventType } from "./kitty-keyboard.js";
25
+ export { KITTY_DISAMBIGUATE, KITTY_REPORT_ALL_KEYS, KITTY_REPORT_ALTERNATE_KEYS, KITTY_REPORT_EVENT_TYPES, KITTY_REPORT_TEXT, type KeyEventType, } from "./kitty-keyboard.js";
26
+ /**
27
+ * キーが押された(離された)とき、PTY へ送る文字列を決める。kitty のフラグが 0 なら、
28
+ * xterm.js 6.0.0 の `evaluateKeyboardEvent` と同じ。
29
+ *
30
+ * - `applicationCursorMode`: DECCKM(#5)。矢印・Home・End が `CSI` か `SS3` かを決める。
31
+ * - `kittyFlags`: kitty keyboard protocol のフラグ(`Terminal.keyboardFlags`)。
32
+ * - 1: Esc や、Ctrl・Alt つきの字などを `CSI ... u` で送る。
33
+ * - 2: `eventType` を `CSI` の形で送る(繰り返し・離す)。
34
+ * - 4: `CSI` の形の字のキーに、Shift をかけた字とベースのレイアウトの字を添える。
35
+ * - 8: すべてのキー(字・Enter・Tab・修飾キーだけ…)を `CSI` の形で送る。
36
+ * - 16: フラグ 8 のとき、字を作るキーにその字を添える。
37
+ * - `eventType`: 1 押す、2 繰り返し、3 離す。フラグ 2 が無ければ無視する(繰り返しも押すとして扱う)。
38
+ * 離すときは、`CSI` の形で送るキーだけ `key` が入る。Enter・Tab・Backspace の離すは、フラグ 8 のときだけ。
39
+ * - `isMac`: macOS か。Option は `macOptionIsMeta` が false なら字を打つキーとして扱い、ここでは送らない。
40
+ * - Cmd(`metaKey`)つきのキーをアプリに任せるかどうかは呼ぶ側が決める(`KeyHandler` が送らない)。
41
+ */
42
+ export declare function evaluateKeyboardEvent(ev: KeyEventLike, applicationCursorMode: boolean, isMac: boolean, macOptionIsMeta: boolean, kittyFlags?: number, eventType?: KeyEventType): KeyResult;
43
+ /** 貼り付ける文字の改行を `\r` にそろえる(xterm.js の `prepareTextForTerminal`)。 */
44
+ export declare function prepareTextForTerminal(text: string): string;
45
+ /** bracketed paste(2004)が有効なら、`ESC [200~` と `ESC [201~` で囲む(xterm.js の `bracketTextForPaste`)。 */
46
+ export declare function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string;