deel-local-cli 1.4.2 → 1.5.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.
- package/README.en.md +997 -908
- package/README.md +988 -907
- package/package.json +60 -60
- package/src/i18n/en.js +1 -0
- package/src/i18n/ko.js +1 -0
- package/src/pack/selfpack.js +9 -3
- package/src/repl.js +73 -4
- package/src/ui/inputbox.js +557 -460
- package/src/ui/motion.js +312 -5
- package/src/ui/office.js +334 -0
- package/src/ui/screen.js +319 -312
- package/src/ui/status.js +1 -0
- package/src/ui/wrap.js +11 -0
package/src/ui/inputbox.js
CHANGED
|
@@ -1,460 +1,557 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 입력 상자.
|
|
3
|
-
*
|
|
4
|
-
* 아래처럼 생겼다. **이것만** 다시 그린다 — 위쪽 대화는 안 건드린다.
|
|
5
|
-
*
|
|
6
|
-
* ▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱ 22% ▏ ◎ 종합 · auto
|
|
7
|
-
* ╭──────────────────────────────────────────────────────────╮
|
|
8
|
-
* │ ❯ 집계 함수도 줄여줘 │
|
|
9
|
-
* ╰──────────────────────────────────────────────────────────╯
|
|
10
|
-
*
|
|
11
|
-
* ── 왜 이렇게 바꿨나 ────────────────────────────────────────────────────
|
|
12
|
-
*
|
|
13
|
-
* 전에는 터미널을 통째로 빌려(대체 화면) 대화 칸·파일 칸·할 일 칸을 나눠
|
|
14
|
-
* 그렸다. 보기에는 그럴듯했지만 **슬래시 명령이 전부 먹통**이 됐다.
|
|
15
|
-
*
|
|
16
|
-
* commands.js · setup.js · scanui.js · sessionui.js · report.js 는
|
|
17
|
-
* 화면 객체를 안 거치고 stdout 에 바로 쓴다. 전체화면은 매번 화면 전체를
|
|
18
|
-
* 자기 버퍼에서 다시 그리므로, 그 글은 찍히자마자 덮여 사라졌다.
|
|
19
|
-
* 명령이 안 도는 게 아니라 **결과가 안 보이는** 것이라 더 나빴다.
|
|
20
|
-
*
|
|
21
|
-
* 고치려면 stdout 에 쓰는 자리를 전부 화면 객체로 꿰야 한다. 지금 여섯 곳이고
|
|
22
|
-
* 앞으로 늘 것이며, 하나라도 빠뜨리면 같은 증상이 조용히 돌아온다.
|
|
23
|
-
*
|
|
24
|
-
* 그래서 반대로 간다. **대화는 터미널에 그냥 흘려보낸다.** 그러면 stdout 에
|
|
25
|
-
* 쓰는 모든 코드가 손 안 대고 그대로 맞다. 스크롤·복사·`| tee` 도 그대로다.
|
|
26
|
-
* 우리가 관리하는 것은 화면 맨 아래 **입력 상자 몇 줄뿐**이다.
|
|
27
|
-
*
|
|
28
|
-
* ── 지키는 선 ───────────────────────────────────────────────────────────
|
|
29
|
-
*
|
|
30
|
-
* 1) 줄 편집은 readline 이 계속 맡는다. 한글 조합·붙여넣기·위아래 이력·
|
|
31
|
-
* 백스페이스·Ctrl+A 를 우리가 다시 짜지 않는다. 우리는 readline 이 들고
|
|
32
|
-
* 있는 글(rl.line)을 **그리기만** 한다.
|
|
33
|
-
* 2) 그릴 것을 값으로 내놓는다(프레임). 검사는 파이프로 도니 화면을 못 본다 —
|
|
34
|
-
* 값으로 내놓아야 한 칸도 안 어긋나는지 잴 수 있다.
|
|
35
|
-
*/
|
|
36
|
-
import { c, width, cursor, clip } from './ansi.js';
|
|
37
|
-
import { isLocalHost } from '../safety/network.js';
|
|
38
|
-
import { statusLine, contextWarning } from './status.js';
|
|
39
|
-
import { 접어쓰기 } from './wrap.js';
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* @param {
|
|
72
|
-
* @param {string} o
|
|
73
|
-
* @param {
|
|
74
|
-
* @param {
|
|
75
|
-
* @param {
|
|
76
|
-
* @
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (일감.대기) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
this
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
this
|
|
459
|
-
|
|
460
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 입력 상자.
|
|
3
|
+
*
|
|
4
|
+
* 아래처럼 생겼다. **이것만** 다시 그린다 — 위쪽 대화는 안 건드린다.
|
|
5
|
+
*
|
|
6
|
+
* ▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱ 22% ▏ ◎ 종합 · auto
|
|
7
|
+
* ╭──────────────────────────────────────────────────────────╮
|
|
8
|
+
* │ ❯ 집계 함수도 줄여줘 │
|
|
9
|
+
* ╰──────────────────────────────────────────────────────────╯
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 이렇게 바꿨나 ────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 전에는 터미널을 통째로 빌려(대체 화면) 대화 칸·파일 칸·할 일 칸을 나눠
|
|
14
|
+
* 그렸다. 보기에는 그럴듯했지만 **슬래시 명령이 전부 먹통**이 됐다.
|
|
15
|
+
*
|
|
16
|
+
* commands.js · setup.js · scanui.js · sessionui.js · report.js 는
|
|
17
|
+
* 화면 객체를 안 거치고 stdout 에 바로 쓴다. 전체화면은 매번 화면 전체를
|
|
18
|
+
* 자기 버퍼에서 다시 그리므로, 그 글은 찍히자마자 덮여 사라졌다.
|
|
19
|
+
* 명령이 안 도는 게 아니라 **결과가 안 보이는** 것이라 더 나빴다.
|
|
20
|
+
*
|
|
21
|
+
* 고치려면 stdout 에 쓰는 자리를 전부 화면 객체로 꿰야 한다. 지금 여섯 곳이고
|
|
22
|
+
* 앞으로 늘 것이며, 하나라도 빠뜨리면 같은 증상이 조용히 돌아온다.
|
|
23
|
+
*
|
|
24
|
+
* 그래서 반대로 간다. **대화는 터미널에 그냥 흘려보낸다.** 그러면 stdout 에
|
|
25
|
+
* 쓰는 모든 코드가 손 안 대고 그대로 맞다. 스크롤·복사·`| tee` 도 그대로다.
|
|
26
|
+
* 우리가 관리하는 것은 화면 맨 아래 **입력 상자 몇 줄뿐**이다.
|
|
27
|
+
*
|
|
28
|
+
* ── 지키는 선 ───────────────────────────────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* 1) 줄 편집은 readline 이 계속 맡는다. 한글 조합·붙여넣기·위아래 이력·
|
|
31
|
+
* 백스페이스·Ctrl+A 를 우리가 다시 짜지 않는다. 우리는 readline 이 들고
|
|
32
|
+
* 있는 글(rl.line)을 **그리기만** 한다.
|
|
33
|
+
* 2) 그릴 것을 값으로 내놓는다(프레임). 검사는 파이프로 도니 화면을 못 본다 —
|
|
34
|
+
* 값으로 내놓아야 한 칸도 안 어긋나는지 잴 수 있다.
|
|
35
|
+
*/
|
|
36
|
+
import { c, width, cursor, clip } from './ansi.js';
|
|
37
|
+
import { isLocalHost } from '../safety/network.js';
|
|
38
|
+
import { statusLine, contextWarning } from './status.js';
|
|
39
|
+
import { 접어쓰기 } from './wrap.js';
|
|
40
|
+
import {
|
|
41
|
+
켜나 as 사무실켜나, 사무실줄들, 상태만들기 as 사무실상태,
|
|
42
|
+
} from './office.js';
|
|
43
|
+
import { 문구고르기, 걸린시간, 느긋해질때, 문구주기 } from './working.js';
|
|
44
|
+
import { 그림고르기 } from './motion.js';
|
|
45
|
+
import { 최대추천 } from './complete.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 이 연결이 이 컴퓨터 밖으로 나가나.
|
|
49
|
+
*
|
|
50
|
+
* 못 읽는 주소면 **거짓**으로 본다. 여기서 애매한 것을 노랗게 칠해 버리면,
|
|
51
|
+
* 진짜로 밖으로 나갈 때의 노랑이 아무 뜻도 없어진다 — 늘 노란 테두리는
|
|
52
|
+
* 테두리가 회색인 것과 같다.
|
|
53
|
+
*/
|
|
54
|
+
function 바깥으로가나(session) {
|
|
55
|
+
try { return !isLocalHost(new URL(session.conn.base).hostname); } catch { return false; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const 줄끝지움 = '\x1b[K';
|
|
59
|
+
const 위로 = (n) => (n > 0 ? `\x1b[${n}A` : '');
|
|
60
|
+
// 돌아가는 표시를 한 칸 넘기는 주기. 사람 눈에 부드럽고, 다시 그리는 값은 싸다.
|
|
61
|
+
const 돌림주기 = 90;
|
|
62
|
+
|
|
63
|
+
// 상자 안쪽 왼쪽에 붙는 것: ' ' + '│' + ' ' + '❯' + ' ' → 5칸
|
|
64
|
+
const 앞머리 = 5;
|
|
65
|
+
// 상자가 차지할 최대 줄 수. 긴 글을 붙여넣어도 화면을 다 먹지 않게.
|
|
66
|
+
export const 안쪽최대 = 8;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 상자를 이룰 줄들과 커서 자리를 값으로 내놓는다.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} o
|
|
72
|
+
* @param {string} o.글 사람이 지금까지 친 것
|
|
73
|
+
* @param {number} o.커서 글 안에서의 커서 위치(글자 수)
|
|
74
|
+
* @param {number} o.폭 터미널 칸 수
|
|
75
|
+
* @param {string} o.상태 상태줄 (없으면 안 그린다)
|
|
76
|
+
* @param {string} o.경고 컨텍스트 경고 (없으면 안 그린다)
|
|
77
|
+
* @param {string} o.곁말 상자 아래 흐린 한 줄 (도움말 등)
|
|
78
|
+
* @param {object[]} o.추천 자동완성 후보 [{이름, 설명, 인자}]
|
|
79
|
+
* @returns {{줄들: string[], 커서: {위: number, 열: number}}}
|
|
80
|
+
* 커서.위 = 마지막 줄에서 몇 줄 위인가, 커서.열 = 1부터 세는 칸
|
|
81
|
+
*/
|
|
82
|
+
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [], 바깥 = false, 사무실 = null } = {}) {
|
|
83
|
+
const 칸 = Math.max(20, 폭);
|
|
84
|
+
/*
|
|
85
|
+
* ── 테두리가 곧 경계선이다 ───────────────────────────────────────────
|
|
86
|
+
*
|
|
87
|
+
* 이 상자는 사람이 글을 치는 자리이자, 그 글이 어디까지 가는지의 울타리다.
|
|
88
|
+
* 이 컴퓨터 안으로만 갈 때는 늘 보던 회색 테두리 그대로 두고, 바깥
|
|
89
|
+
* 게이트웨이로 붙어 있을 때만 **노랗게** 그린다.
|
|
90
|
+
*
|
|
91
|
+
* 상태줄 글자 한 칸(⌂/↗)으로도 되지 않느냐 싶지만, 그건 좁으면 밀릴 수 있고
|
|
92
|
+
* 무엇보다 사람이 안 본다. 테두리는 안 볼 수가 없다 — 치는 글을 감싸고 있어서,
|
|
93
|
+
* 눈이 글자를 따라가는 내내 시야에 들어온다. 칸도 한 칸 안 먹는다.
|
|
94
|
+
*
|
|
95
|
+
* 시끄러운 것이 맞고, 그게 뜻이다. 내 소스가 밖으로 나가는 중이라면
|
|
96
|
+
* 화면이 그만큼은 말해 줘야 한다.
|
|
97
|
+
*/
|
|
98
|
+
const 테 = 바깥 ? c.hyellow : c.gray;
|
|
99
|
+
const 안쪽 = Math.max(4, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
|
|
100
|
+
const 줄들 = [];
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* 사무실은 **상태줄 위**에 얹힌다.
|
|
104
|
+
*
|
|
105
|
+
* 여기 넣는 것이 중요하다. 상자가 이미 '아래 몇 줄을 붙박아 두고 그 자리에서
|
|
106
|
+
* 다시 그리기' 를 하고 있으므로, 사무실은 그 붙박이 자리가 몇 줄 길어지는
|
|
107
|
+
* 것일 뿐이다. 새 화면 방식을 안 들이는 것 — 예전 전체화면(tui.js)이 대화
|
|
108
|
+
* 흐름과 다투다 죽었다.
|
|
109
|
+
*/
|
|
110
|
+
if (사무실?.length) 줄들.push(...사무실);
|
|
111
|
+
|
|
112
|
+
if (상태) 줄들.push(상태);
|
|
113
|
+
if (경고) 줄들.push(` ${경고}`);
|
|
114
|
+
|
|
115
|
+
const 가로 = '─'.repeat(칸 - 3);
|
|
116
|
+
줄들.push(테(` ╭${가로}╮`));
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
* 일하는 중.
|
|
120
|
+
*
|
|
121
|
+
* 상자를 없애고 대신 한 줄을 흘려보내는 길도 있었다. 그런데 그러면 일할 때만
|
|
122
|
+
* 화면 아래가 텅 비어, 사람이 보기에 **프로그램이 다른 것으로 바뀐 것처럼**
|
|
123
|
+
* 보인다. 테두리를 그대로 두고 안엣것만 바꾼다 — 같은 자리에서 같은 상자가
|
|
124
|
+
* 계속 살아 있는 편이 '돌아가는 중' 이라는 신호로 훨씬 낫다.
|
|
125
|
+
*/
|
|
126
|
+
if (일감) {
|
|
127
|
+
/*
|
|
128
|
+
* 여기서 쓸 수 있는 칸은 평소보다 **두 칸 넓다.**
|
|
129
|
+
*
|
|
130
|
+
* 평소에는 왼쪽에 `❯ ` 가 붙지만(앞머리 5칸) 일하는 중에는 돌아가는 표시가
|
|
131
|
+
* 본문 안에 들어가므로 그 두 칸이 본문 몫이 된다. 이걸 안 더하면 테두리가
|
|
132
|
+
* 딱 두 칸 짧아진다 — 눈으로는 거의 안 보이는데 실제 터미널에서는 줄이
|
|
133
|
+
* 어긋나 보인다.
|
|
134
|
+
*/
|
|
135
|
+
const 일안쪽 = 안쪽 + 2;
|
|
136
|
+
const 왼쪽 = `${c.hcyan(일감.돌림 ?? '⠋')} ${c.white(일감.말 ?? '')}${c.gray('…')}`;
|
|
137
|
+
const 오른쪽 = c.gray(일감.곁 ?? '');
|
|
138
|
+
const 남 = 일안쪽 - width(왼쪽) - width(오른쪽);
|
|
139
|
+
// 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
|
|
140
|
+
const 안 = 남 >= 2 ? `${왼쪽}${' '.repeat(남)}${오른쪽}` : 왼쪽;
|
|
141
|
+
줄들.push(` ${테('│')} ${안}${' '.repeat(Math.max(0, 일안쪽 - width(안)))} ${테('│')}`);
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* 일하는 도중에 미리 쳐 두는 자리.
|
|
145
|
+
*
|
|
146
|
+
* 로컬 모델은 한 턴에 몇 분이 걸린다. 그 동안 생각난 것을 못 적어 두고
|
|
147
|
+
* 끝나기를 지켜보고 있어야 한다면, 기다리는 시간이 통째로 버려진다.
|
|
148
|
+
* 사실 readline 은 그 동안에도 글을 받아 두고 있었다 — 다만 화면에
|
|
149
|
+
* 아무것도 안 보여서, 쳐도 안 먹는 줄 알았을 뿐이다.
|
|
150
|
+
*
|
|
151
|
+
* 그래서 보여만 준다. 새로 받는 게 아니라 **이미 받고 있던 것**이다.
|
|
152
|
+
*/
|
|
153
|
+
if (일감.대기) {
|
|
154
|
+
const 대기줄들 = 접어쓰기(일감.대기, 안쪽).slice(0, 3);
|
|
155
|
+
for (const [i, 한줄] of 대기줄들.entries()) {
|
|
156
|
+
const 표 = i === 0 ? c.hcyan('❯') : ' ';
|
|
157
|
+
줄들.push(` ${테('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 안쪽 - width(한줄)))} ${테('│')}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
줄들.push(테(` ╰${가로}╯`));
|
|
162
|
+
if (일감.대기) 줄들.push(` ${c.gray('Enter 를 치면 지금 일이 끝난 뒤에 보냅니다')}`);
|
|
163
|
+
else if (곁말) 줄들.push(` ${c.gray(곁말)}`);
|
|
164
|
+
|
|
165
|
+
// 커서는 미리 치는 중일 때만 보여 준다. 아니면 칠 자리가 아니다.
|
|
166
|
+
if (일감.대기) {
|
|
167
|
+
const 마지막 = 접어쓰기(일감.대기, 안쪽).slice(0, 3).at(-1) ?? '';
|
|
168
|
+
return { 줄들, 커서: { 위: 2, 열: Math.min(앞머리 + 1 + width(마지막), 칸 - 1) } };
|
|
169
|
+
}
|
|
170
|
+
return { 줄들, 커서: { 위: (곁말 ? 1 : 0), 열: 1 } };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* 긴 입력은 접는다. 자르면 사람이 친 글이 안 보이는데, 안 보이는 채로
|
|
175
|
+
* Enter 를 치게 되는 것이 제일 나쁘다 — 무엇을 보내는지 모르게 된다.
|
|
176
|
+
*/
|
|
177
|
+
const 안쪽줄 = 글 ? 접어쓰기(글, 안쪽) : [''];
|
|
178
|
+
const 넘침 = 안쪽줄.length > 안쪽최대;
|
|
179
|
+
const 보일줄 = 넘침 ? 안쪽줄.slice(안쪽줄.length - 안쪽최대) : 안쪽줄;
|
|
180
|
+
|
|
181
|
+
for (const [i, 한줄] of 보일줄.entries()) {
|
|
182
|
+
const 표 = i === 0 ? (넘침 ? c.gray('…') : c.hcyan('❯')) : ' ';
|
|
183
|
+
const 남 = 안쪽 - width(한줄);
|
|
184
|
+
줄들.push(` ${테('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 남))} ${테('│')}`);
|
|
185
|
+
}
|
|
186
|
+
줄들.push(테(` ╰${가로}╯`));
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* 자동완성 추천은 상자 **아래**에 놓는다.
|
|
190
|
+
*
|
|
191
|
+
* 안에 넣으면 치던 글이 밀려 올라가서, 무엇을 치고 있었는지가 안 보인다.
|
|
192
|
+
* 아래에 두면 목록이 몇 줄이든 치던 자리는 늘 같은 데 있다.
|
|
193
|
+
*/
|
|
194
|
+
let 추천줄수 = 0;
|
|
195
|
+
if (추천?.length) {
|
|
196
|
+
const 보일것 = 추천.slice(0, 최대추천);
|
|
197
|
+
// 이름은 상자 안 글자와 같은 자리(6칸째)에서 시작한다 — 눈이 안 흔들린다.
|
|
198
|
+
const 이름들 = 보일것.map((x) => `/${x.이름}${x.인자 ? ` ${x.인자}` : ''}`);
|
|
199
|
+
// 이름칸이 넓어지면 설명이 그만큼 잘린다. 제일 긴 이름 하나 때문에 설명이
|
|
200
|
+
// 다 사라지는 일이 없게 위쪽을 막아 둔다.
|
|
201
|
+
const 이름폭 = Math.min(Math.max(...이름들.map(width)), Math.max(12, Math.floor(칸 * 0.38)));
|
|
202
|
+
const 설명폭 = 칸 - 앞머리 - 이름폭 - 3;
|
|
203
|
+
|
|
204
|
+
for (const [i, x] of 보일것.entries()) {
|
|
205
|
+
// 첫째 것만 밝게. Tab 을 누르면 대개 이것으로 채워진다.
|
|
206
|
+
const 표 = i === 0 ? c.hcyan('›') : ' ';
|
|
207
|
+
const 칠 = i === 0 ? c.white : c.gray;
|
|
208
|
+
const 이름 = clip(이름들[i], 이름폭);
|
|
209
|
+
const 벌림 = ' '.repeat(Math.max(1, 이름폭 - width(이름) + 2));
|
|
210
|
+
// 좁은 터미널에서는 설명을 버린다. 이름이 본문이다.
|
|
211
|
+
const 설명 = 설명폭 >= 8 ? c.gray(clip(x.설명 ?? '', 설명폭)) : '';
|
|
212
|
+
줄들.push(` ${표} ${칠(이름)}${설명 ? 벌림 + 설명 : ''}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
추천줄수 = 보일것.length;
|
|
216
|
+
if (추천.length > 보일것.length) {
|
|
217
|
+
줄들.push(` ${c.gray(`그 밖에 ${추천.length - 보일것.length}개 더`)}`);
|
|
218
|
+
추천줄수 += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (곁말) 줄들.push(` ${c.gray(곁말)}`);
|
|
223
|
+
|
|
224
|
+
/*
|
|
225
|
+
* 커서 자리.
|
|
226
|
+
*
|
|
227
|
+
* 글자 수가 아니라 **폭**으로 세야 한다. 한글은 두 칸이라, 글자 수로 세면
|
|
228
|
+
* '가나다' 를 친 뒤 커서가 세 칸 왼쪽에 가 있게 된다. 백스페이스를 누르면
|
|
229
|
+
* 엉뚱한 자리가 지워지는 것처럼 보인다.
|
|
230
|
+
*/
|
|
231
|
+
const 앞글 = 커서자리 == null ? 글 : String(글).slice(0, 커서자리);
|
|
232
|
+
const 앞줄들 = 앞글 ? 접어쓰기(앞글, 안쪽) : [''];
|
|
233
|
+
|
|
234
|
+
// 넘쳐서 앞부분을 잘라 냈으면 그만큼 줄 번호가 밀린다.
|
|
235
|
+
const 숨긴줄 = 안쪽줄.length - 보일줄.length;
|
|
236
|
+
const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 숨긴줄), 보일줄.length - 1);
|
|
237
|
+
|
|
238
|
+
// 커서 아래에 몇 줄이 더 있나 — 닫는 테두리 + 추천 목록 + 곁말.
|
|
239
|
+
const 아래 = 1 + 추천줄수 + (곁말 ? 1 : 0);
|
|
240
|
+
const 위 = 아래 + (보일줄.length - 1 - 커서줄);
|
|
241
|
+
// 1부터 세는 칸. 앞머리(5칸) 다음이 6칸째다 — 거기가 첫 글자 자리다.
|
|
242
|
+
const 열 = Math.min(앞머리 + 1 + width(앞줄들.at(-1) ?? ''), 칸 - 1);
|
|
243
|
+
|
|
244
|
+
return { 줄들, 커서: { 위, 열 } };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 터미널 맨 아래에 상자를 붙여 두는 화면.
|
|
249
|
+
*
|
|
250
|
+
* 대화는 stdout 으로 그냥 나간다(줄 화면과 같다). 우리가 지우고 다시 그리는
|
|
251
|
+
* 것은 상자 몇 줄뿐이다.
|
|
252
|
+
*/
|
|
253
|
+
export class InputBox {
|
|
254
|
+
constructor() {
|
|
255
|
+
this.그린줄 = 0; // 지금 화면에 붙어 있는 상자의 줄 수
|
|
256
|
+
/*
|
|
257
|
+
* 커서를 상자 맨 아랫줄에서 몇 줄 위에 놓아 뒀나.
|
|
258
|
+
*
|
|
259
|
+
* 이걸 안 들고 있으면 지울 때가 어긋난다. 상자가 한 줄짜리일 때는
|
|
260
|
+
* 커서가 늘 아래에서 첫째 줄이라 티가 안 나는데, 긴 글이 접혀 상자가
|
|
261
|
+
* 세 줄이 되거나 아래에 곁말이 붙는 순간 **몇 줄을 내려가야 하는지**가
|
|
262
|
+
* 달라진다. 고정값으로 내려가면 화면 맨 아래에 부딪혀 멈추고, 그 뒤로는
|
|
263
|
+
* 엉뚱한 줄을 지운다 — 대화가 한 줄씩 갉여 나간다.
|
|
264
|
+
*/
|
|
265
|
+
this.커서위 = 0;
|
|
266
|
+
this.session = null;
|
|
267
|
+
this.곁말 = '';
|
|
268
|
+
|
|
269
|
+
// 일하는 중 상태. null 이면 평소(입력 기다리는 중)다.
|
|
270
|
+
this.일감 = null;
|
|
271
|
+
this.박자 = null; // 돌아가는 표시를 돌리는 시계
|
|
272
|
+
this.틱 = 0;
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* 대화가 **줄 중간**에 멈춰 있나.
|
|
276
|
+
*
|
|
277
|
+
* 이걸 안 들고 있으면 답이 잘려 보인다. 실제로 나갔던 결함이다.
|
|
278
|
+
*
|
|
279
|
+
* 모델이 답을 흘리는 동안 글은 토막토막 온다. 한 토막을 찍으면 커서는
|
|
280
|
+
* 그 줄 한가운데에 선다. 그런데 돌아가는 표시는 90ms 마다 제 시계로
|
|
281
|
+
* 상자를 다시 그리므로, 그 순간 **상자가 답 줄에 얹혀 그려진다.**
|
|
282
|
+
*
|
|
283
|
+
* ▌ **계획표** ▏myproject · qwen… ▏ ▰▱▱▱▱ 13% ▏ ◎ 종합
|
|
284
|
+
*
|
|
285
|
+
* 그 다음 지우기는 `\r` 로 1칸째에 간 뒤 `\x1b[K` 로 줄을 지운다.
|
|
286
|
+
* 상자만 지우려던 것인데 **같은 줄에 있던 답까지 같이 지워진다.**
|
|
287
|
+
* 사람 눈에는 답의 앞부분이 사라진 것으로 보인다 — `**계획표**` 가
|
|
288
|
+
* `표**` 만 남는 식이다. 글자가 안 온 게 아니라 지워진 것이라,
|
|
289
|
+
* 모델을 아무리 바꿔도 똑같이 나온다.
|
|
290
|
+
*
|
|
291
|
+
* 그래서 규칙을 하나로 못 박는다 — **줄 중간에서는 아무것도 안 그린다.**
|
|
292
|
+
*/
|
|
293
|
+
this.줄중간 = false;
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* 지난번에 화면에 찍어 둔 줄들. **안 바뀐 줄을 안 보내려고** 들고 있다.
|
|
297
|
+
*
|
|
298
|
+
* 상자는 90ms 마다 다시 그려지는데, 그때마다 열두 줄을 통째로 보내면
|
|
299
|
+
* 대부분이 지난번과 똑같은 글자다. 재 보니 한 판에 실제로 바뀌는 것은
|
|
300
|
+
* 한 줄뿐이었다 — 나머지 열한 줄은 같은 것을 다시 보내는 셈이다.
|
|
301
|
+
* 줄마다 견줘서 바뀐 것만 보내면 터미널로 나가는 양이 91% 줄어든다.
|
|
302
|
+
*
|
|
303
|
+
* null 이면 견줄 것이 없다는 뜻이다(막 지웠거나 줄 수가 달라졌거나).
|
|
304
|
+
*/
|
|
305
|
+
this.앞줄들 = null;
|
|
306
|
+
/*
|
|
307
|
+
* 몇 판째 견주기로만 그렸나.
|
|
308
|
+
*
|
|
309
|
+
* 견주기는 '화면이 내가 아는 그대로다' 를 믿는다. 그런데 이 저장소에는
|
|
310
|
+
* 화면 객체를 안 거치고 stdout 에 바로 쓰는 자리가 몇 군데 있다(그 때문에
|
|
311
|
+
* 예전 전체화면이 죽었다). 그런 글이 상자를 덮으면 견주기는 그 줄을
|
|
312
|
+
* '안 바뀌었다' 며 안 고치고, 깨진 화면이 그대로 남는다.
|
|
313
|
+
*
|
|
314
|
+
* 통째로 그리던 예전 방식은 매번 덮어써서 저절로 나았다. 그 자가 치유를
|
|
315
|
+
* 잃지 않으려고, 가끔 한 번은 통째로 그린다. 4초에 한 번이라 눈에 안 띈다.
|
|
316
|
+
*/
|
|
317
|
+
this.견준판 = 0;
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* 사무실 화이트보드에 붙일 할 일.
|
|
321
|
+
*
|
|
322
|
+
* 할 일은 세션이 아니라 턴 문맥(ctx.todos)에 있어서 상자가 스스로 못
|
|
323
|
+
* 읽는다. 그래서 아는 쪽에서 넣어 준다(할일갱신). 안 넣어 주면 빈 보드로
|
|
324
|
+
* 둔다 — **모르는 것을 그럴듯하게 채우지 않는다.**
|
|
325
|
+
*/
|
|
326
|
+
this.할일들 = [];
|
|
327
|
+
// 지금 도는 하위 작업 수 (사무실 자리 수). 모르면 0 이다.
|
|
328
|
+
this.도는하위 = 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 할 일이 바뀌었다. 사무실 화이트보드가 이걸 본다. */
|
|
332
|
+
할일갱신(할일들) {
|
|
333
|
+
this.할일들 = Array.isArray(할일들) ? 할일들 : [];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** 도는 하위 작업 수. 사무실이 자리를 이만큼 채운다. */
|
|
337
|
+
하위갱신(n) {
|
|
338
|
+
this.도는하위 = Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 대화 글이 줄 중간까지 쓰였다. 줄이 끝날 때까지 상자를 안 세운다. */
|
|
342
|
+
줄중간표시() {
|
|
343
|
+
this.지우기();
|
|
344
|
+
this.줄중간 = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** 줄이 끝났다. 이제 아래에 상자를 세워도 안전하다. */
|
|
348
|
+
줄끝남() { this.줄중간 = false; }
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 일하기 시작. 상자 안엣것이 입력칸에서 '돌아가는 중' 으로 바뀐다.
|
|
352
|
+
*
|
|
353
|
+
* 스스로 다시 그리는 시계를 단다. 모델이 30초를 말없이 생각할 때 우리 쪽에서
|
|
354
|
+
* 아무 일도 안 일어나면 표시가 얼어붙는데, 그건 멈춘 것과 화면상 구분이 안 된다.
|
|
355
|
+
*/
|
|
356
|
+
일시작(session, 갈래 = '기본') {
|
|
357
|
+
this.session = session ?? this.session;
|
|
358
|
+
// 대기 = 일하는 도중에 미리 치고 있는 글, 예약수 = 이미 Enter 를 친 것들.
|
|
359
|
+
this.일감 = { 갈래, 시작: Date.now(), 곁정보: '', 대기: '', 예약수: 0 };
|
|
360
|
+
this.틱 = 0;
|
|
361
|
+
this.일그리기();
|
|
362
|
+
if (this.박자) clearInterval(this.박자);
|
|
363
|
+
this.박자 = setInterval(() => { this.틱 += 1; this.일그리기(); }, 돌림주기);
|
|
364
|
+
// 이 시계 때문에 프로그램이 안 끝나면 안 된다. 끝맺음이 걸려 버린다.
|
|
365
|
+
if (typeof this.박자.unref === 'function') this.박자.unref();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** 하는 일이 바뀌었다 (파일 읽기 → 코드 고치기 처럼). */
|
|
369
|
+
일바꿈(갈래, 곁정보 = null) {
|
|
370
|
+
if (!this.일감) return;
|
|
371
|
+
// 갈래가 바뀌면 문구를 그 자리에서 갈아 준다. 4초를 기다리면 이미 다음
|
|
372
|
+
// 일로 넘어가 있어서, 화면이 늘 한 걸음 늦은 말을 하게 된다.
|
|
373
|
+
if (갈래 && 갈래 !== this.일감.갈래) { this.일감.갈래 = 갈래; this.틱 = 0; }
|
|
374
|
+
if (곁정보 !== null) this.일감.곁정보 = 곁정보;
|
|
375
|
+
this.일그리기();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
일끝() {
|
|
379
|
+
if (this.박자) { clearInterval(this.박자); this.박자 = null; }
|
|
380
|
+
this.일감 = null;
|
|
381
|
+
this.지우기();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 일하는 도중에 사람이 치고 있는 글 · 예약해 둔 개수.
|
|
386
|
+
*
|
|
387
|
+
* 답이 흘러나오는 동안(줄 중간)에는 안 그려진다 — 그건 일그리기() 가
|
|
388
|
+
* 알아서 넘긴다. 글은 readline 이 계속 들고 있으므로 잃지 않는다.
|
|
389
|
+
*/
|
|
390
|
+
대기갱신(글 = '', 예약수 = 0) {
|
|
391
|
+
if (!this.일감) return;
|
|
392
|
+
if (this.일감.대기 === 글 && this.일감.예약수 === 예약수) return;
|
|
393
|
+
this.일감.대기 = 글;
|
|
394
|
+
this.일감.예약수 = 예약수;
|
|
395
|
+
this.일그리기();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
일그리기() {
|
|
399
|
+
// 줄 중간이면 안 그린다. 여기가 시계로 저절로 도는 유일한 자리라,
|
|
400
|
+
// 답이 흘러나오는 동안 상자가 답 줄에 얹히는 것을 여기서 막는다.
|
|
401
|
+
if (!this.일감 || this.줄중간) return;
|
|
402
|
+
const 지난 = Date.now() - this.일감.시작;
|
|
403
|
+
const 갈래 = 지난 >= 느긋해질때 ? '느긋' : this.일감.갈래;
|
|
404
|
+
const 회차 = Math.floor((this.틱 * 돌림주기) / 문구주기);
|
|
405
|
+
this.그리기(this.session, '', 0, {
|
|
406
|
+
// 갈래에 맞는 작은 그림. 무엇을 하는 중인지 글자와 그림이 같이 말한다.
|
|
407
|
+
// DEEL_NO_MOTION=1 이면 예전처럼 한 칸짜리 돌림표가 온다.
|
|
408
|
+
돌림: 그림고르기(갈래, this.틱),
|
|
409
|
+
말: 문구고르기(갈래, 회차),
|
|
410
|
+
곁: [
|
|
411
|
+
걸린시간(지난),
|
|
412
|
+
this.일감.곁정보,
|
|
413
|
+
// 예약해 둔 것이 있으면 잊지 않게 세어 준다. 몇 분짜리 일이 끝난 뒤
|
|
414
|
+
// 엉뚱한 말이 저절로 나가는 것처럼 보이면 안 된다.
|
|
415
|
+
this.일감.예약수 ? `${this.일감.예약수}건 예약됨` : '',
|
|
416
|
+
'Ctrl+C 중단',
|
|
417
|
+
].filter(Boolean).join(' · '),
|
|
418
|
+
대기: this.일감.대기,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 상자를 지운다. 위쪽 대화는 안 건드린다. */
|
|
423
|
+
지우기() {
|
|
424
|
+
if (!this.그린줄) return;
|
|
425
|
+
// 커서가 있는 자리에서 맨 아랫줄까지 내려간 뒤, 위로 훑어 올라가며 지운다.
|
|
426
|
+
let out = '\r' + (this.커서위 > 0 ? `\x1b[${this.커서위}B` : '') + '\r';
|
|
427
|
+
for (let i = 0; i < this.그린줄; i++) {
|
|
428
|
+
out += 줄끝지움;
|
|
429
|
+
if (i < this.그린줄 - 1) out += '\x1b[1A';
|
|
430
|
+
}
|
|
431
|
+
out += '\r';
|
|
432
|
+
process.stdout.write(out);
|
|
433
|
+
this.그린줄 = 0;
|
|
434
|
+
this.커서위 = 0;
|
|
435
|
+
// 지운 자리를 '지난번에 찍어 둔 것' 으로 믿으면 안 된다.
|
|
436
|
+
this.앞줄들 = null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 상자를 그린다(있으면 지우고 다시).
|
|
441
|
+
*
|
|
442
|
+
* @param {object} session 상태줄을 그리는 데 쓴다
|
|
443
|
+
* @param {string} 글 readline 이 들고 있는 글
|
|
444
|
+
* @param {number} 커서 그 안에서의 커서 위치
|
|
445
|
+
* @param {object} 일감 일하는 중이면 그 상태
|
|
446
|
+
* @param {object[]} 추천 자동완성 후보
|
|
447
|
+
*/
|
|
448
|
+
그리기(session, 글 = '', 커서자리 = null, 일감 = null, 추천 = []) {
|
|
449
|
+
this.session = session ?? this.session;
|
|
450
|
+
/*
|
|
451
|
+
* 줄 중간에서는 절대 안 그린다 — 여기가 그 규칙을 지키는 마지막 자리다.
|
|
452
|
+
*
|
|
453
|
+
* 시계로 도는 일그리기() 는 아예 안 그리고 넘어가지만, 사람이 입력을
|
|
454
|
+
* 기다리는 자리(입력자리·입력갱신)는 그리긴 그려야 한다. 그때는 줄을
|
|
455
|
+
* 먼저 끝내고 그린다. 안 그러면 상자가 대화 줄에 얹히고, 다음 지우기가
|
|
456
|
+
* 그 줄을 통째로 지운다.
|
|
457
|
+
*/
|
|
458
|
+
if (this.줄중간) {
|
|
459
|
+
process.stdout.write('\n');
|
|
460
|
+
this.줄중간 = false;
|
|
461
|
+
// 화면이 한 줄 밀렸다 — 지난번 줄들은 이제 견줄 것이 못 된다.
|
|
462
|
+
this.앞줄들 = null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/*
|
|
466
|
+
* 사무실은 켠 사람에게만, 그리고 화면이 넉넉할 때만 그린다.
|
|
467
|
+
* 켜나() 가 그 판단을 다 한다 — 여기서는 켜졌으면 줄을 받아 얹을 뿐이다.
|
|
468
|
+
*/
|
|
469
|
+
const 폭 = process.stdout.columns ?? 80;
|
|
470
|
+
const 방 = 사무실켜나()
|
|
471
|
+
? 사무실줄들(사무실상태(this.session, this.일감, this.틱, this.할일들, this.도는하위), 폭)
|
|
472
|
+
: null;
|
|
473
|
+
|
|
474
|
+
const { 줄들, 커서: 자리 } = 프레임({
|
|
475
|
+
사무실: 방,
|
|
476
|
+
글,
|
|
477
|
+
커서: 커서자리,
|
|
478
|
+
폭,
|
|
479
|
+
상태: this.session ? statusLine(this.session) : '',
|
|
480
|
+
경고: this.session ? (contextWarning(this.session) ?? '') : '',
|
|
481
|
+
곁말: this.곁말,
|
|
482
|
+
일감,
|
|
483
|
+
추천,
|
|
484
|
+
// 테두리 색이 경계선을 말한다. 못 읽는 주소면 평소대로 둔다 —
|
|
485
|
+
// 애매한 것을 노랗게 칠하면, 진짜로 밖으로 나갈 때의 노랑이 안 먹힌다.
|
|
486
|
+
바깥: this.session ? 바깥으로가나(this.session) : false,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
/*
|
|
490
|
+
* ── 지우지 않고 덮어쓴다. 그리고 한 번에 내보낸다 ────────────────────
|
|
491
|
+
*
|
|
492
|
+
* 예전에는 이랬다: 지우기() 로 상자를 싹 지우고 → 새로 그리고 → 커서를
|
|
493
|
+
* 되돌린다. 세 번을 따로 내보냈다. 터미널은 받는 대로 그리므로 **그 사이가
|
|
494
|
+
* 사람 눈에 보인다** — 상자가 한 번 비었다가 다시 차는 것이 백스페이스를
|
|
495
|
+
* 누를 때마다 반복되어, 화면이 깜빡 꺼지는 것처럼 느껴졌다. 글자를 지울
|
|
496
|
+
* 때는 한 번에 한 글자씩 여러 번 누르므로 특히 심했다.
|
|
497
|
+
*
|
|
498
|
+
* 그래서 두 가지를 바꿨다.
|
|
499
|
+
* 1) 지우고 다시 그리지 않는다. 상자 맨 윗줄로 올라가 **덮어쓴다.**
|
|
500
|
+
* 줄 끝마다 줄끝지움이 붙으므로 전에 있던 글자가 남을 자리가 없다.
|
|
501
|
+
* 빈 화면이 스쳐 가는 순간 자체가 사라진다.
|
|
502
|
+
* 2) 지우기·그리기·커서 되돌리기를 한 덩이로 묶어 **한 번만** 쓴다.
|
|
503
|
+
* 중간 상태가 화면에 못 나간다.
|
|
504
|
+
*
|
|
505
|
+
* 상자가 전보다 짧아졌을 때만 남는 줄이 생긴다. 그때만 그 줄을 지운다.
|
|
506
|
+
*/
|
|
507
|
+
const 이전줄 = this.그린줄;
|
|
508
|
+
// 커서는 상자 아래에서 커서위 번째 줄에 있다. 맨 윗줄까지 남은 거리.
|
|
509
|
+
const 윗줄까지 = 이전줄 ? Math.max(0, 이전줄 - 1 - this.커서위) : 0;
|
|
510
|
+
const 남은줄 = Math.max(0, 이전줄 - 줄들.length);
|
|
511
|
+
|
|
512
|
+
/*
|
|
513
|
+
* 바뀐 줄만 보낸다.
|
|
514
|
+
*
|
|
515
|
+
* 줄 수가 그대로일 때만 견준다 — 줄 수가 달라졌으면 지울 줄까지 생겨서
|
|
516
|
+
* 셈이 얽힌다. 그때는 그냥 통째로 그린다. 어차피 드물다(글이 접히거나
|
|
517
|
+
* 추천 목록이 뜨고 지는 순간뿐이고, 그건 애니메이션이 아니다).
|
|
518
|
+
*
|
|
519
|
+
* 안 보낸 줄에서도 '\n' 은 보내야 한다 — 커서를 다음 줄로 내려야 하기
|
|
520
|
+
* 때문이다. 글자 없이 줄만 넘어간다.
|
|
521
|
+
*/
|
|
522
|
+
const 통째로 = this.앞줄들 === null
|
|
523
|
+
|| this.앞줄들.length !== 줄들.length
|
|
524
|
+
|| 남은줄 > 0
|
|
525
|
+
|| this.견준판 >= 45;
|
|
526
|
+
|
|
527
|
+
let 나갈것 = cursor.숨김 + '\r' + 위로(윗줄까지);
|
|
528
|
+
if (통째로) {
|
|
529
|
+
나갈것 += 줄들.map((l) => l + 줄끝지움).join('\n') + '\n';
|
|
530
|
+
for (let i = 0; i < 남은줄; i++) 나갈것 += 줄끝지움 + '\n';
|
|
531
|
+
this.견준판 = 0;
|
|
532
|
+
} else {
|
|
533
|
+
for (const [i, l] of 줄들.entries()) {
|
|
534
|
+
if (l !== this.앞줄들[i]) 나갈것 += l + 줄끝지움;
|
|
535
|
+
나갈것 += '\n';
|
|
536
|
+
}
|
|
537
|
+
this.견준판 += 1;
|
|
538
|
+
}
|
|
539
|
+
/*
|
|
540
|
+
* 방금 찍은 마지막 줄 **다음** 줄에 커서가 있다(남은 줄을 지웠으면 그만큼
|
|
541
|
+
* 더 아래). 거기서 되짚어 올라간다.
|
|
542
|
+
*
|
|
543
|
+
* 오른쪽으로 옮길 때 한 칸이 어긋나기 쉽다. \r 은 커서를 1칸째에 놓고,
|
|
544
|
+
* \x1b[nC 는 거기서 n칸 **더** 간다 — 그래서 1부터 세는 목표 칸으로
|
|
545
|
+
* 가려면 열-1 을 보내야 한다. 이걸 틀리면 친 글자가 한 칸씩 밀려 보이고,
|
|
546
|
+
* 백스페이스가 엉뚱한 자리를 지우는 것처럼 보인다.
|
|
547
|
+
*/
|
|
548
|
+
const 오른쪽 = Math.max(0, 자리.열 - 1);
|
|
549
|
+
나갈것 += 위로(남은줄 + 자리.위 + 1) + '\r' + (오른쪽 ? `\x1b[${오른쪽}C` : '');
|
|
550
|
+
나갈것 += cursor.보임;
|
|
551
|
+
process.stdout.write(나갈것);
|
|
552
|
+
|
|
553
|
+
this.그린줄 = 줄들.length;
|
|
554
|
+
this.커서위 = 자리.위;
|
|
555
|
+
this.앞줄들 = 줄들;
|
|
556
|
+
}
|
|
557
|
+
}
|