deel-local-cli 1.0.0 → 1.0.1
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 +43 -43
- package/README.md +45 -43
- package/bin/deel.js +4 -4
- package/package.json +1 -1
- package/src/repl.js +101 -29
- package/src/ui/inputbox.js +184 -0
- package/src/ui/screen.js +73 -28
- package/src/ui/wrap.js +68 -0
- package/src/ui/tui.js +0 -419
|
@@ -0,0 +1,184 @@
|
|
|
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 } from './ansi.js';
|
|
37
|
+
import { statusLine, contextWarning } from './status.js';
|
|
38
|
+
import { 접어쓰기 } from './wrap.js';
|
|
39
|
+
|
|
40
|
+
const 줄끝지움 = '\x1b[K';
|
|
41
|
+
const 위로 = (n) => (n > 0 ? `\x1b[${n}A` : '');
|
|
42
|
+
|
|
43
|
+
// 상자 안쪽 왼쪽에 붙는 것: ' ' + '│' + ' ' + '❯' + ' ' → 5칸
|
|
44
|
+
const 앞머리 = 5;
|
|
45
|
+
// 상자가 차지할 최대 줄 수. 긴 글을 붙여넣어도 화면을 다 먹지 않게.
|
|
46
|
+
export const 안쪽최대 = 8;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 상자를 이룰 줄들과 커서 자리를 값으로 내놓는다.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} o
|
|
52
|
+
* @param {string} o.글 사람이 지금까지 친 것
|
|
53
|
+
* @param {number} o.커서 글 안에서의 커서 위치(글자 수)
|
|
54
|
+
* @param {number} o.폭 터미널 칸 수
|
|
55
|
+
* @param {string} o.상태 상태줄 (없으면 안 그린다)
|
|
56
|
+
* @param {string} o.경고 컨텍스트 경고 (없으면 안 그린다)
|
|
57
|
+
* @param {string} o.곁말 상자 아래 흐린 한 줄 (도움말 등)
|
|
58
|
+
* @returns {{줄들: string[], 커서: {위: number, 열: number}}}
|
|
59
|
+
* 커서.위 = 마지막 줄에서 몇 줄 위인가, 커서.열 = 1부터 세는 칸
|
|
60
|
+
*/
|
|
61
|
+
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '' } = {}) {
|
|
62
|
+
const 칸 = Math.max(20, 폭);
|
|
63
|
+
const 안쪽 = Math.max(4, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
|
|
64
|
+
const 줄들 = [];
|
|
65
|
+
|
|
66
|
+
if (상태) 줄들.push(상태);
|
|
67
|
+
if (경고) 줄들.push(` ${경고}`);
|
|
68
|
+
|
|
69
|
+
const 가로 = '─'.repeat(칸 - 3);
|
|
70
|
+
줄들.push(c.gray(` ╭${가로}╮`));
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* 긴 입력은 접는다. 자르면 사람이 친 글이 안 보이는데, 안 보이는 채로
|
|
74
|
+
* Enter 를 치게 되는 것이 제일 나쁘다 — 무엇을 보내는지 모르게 된다.
|
|
75
|
+
*/
|
|
76
|
+
const 안쪽줄 = 글 ? 접어쓰기(글, 안쪽) : [''];
|
|
77
|
+
const 넘침 = 안쪽줄.length > 안쪽최대;
|
|
78
|
+
const 보일줄 = 넘침 ? 안쪽줄.slice(안쪽줄.length - 안쪽최대) : 안쪽줄;
|
|
79
|
+
|
|
80
|
+
for (const [i, 한줄] of 보일줄.entries()) {
|
|
81
|
+
const 표 = i === 0 ? (넘침 ? c.gray('…') : c.hcyan('❯')) : ' ';
|
|
82
|
+
const 남 = 안쪽 - width(한줄);
|
|
83
|
+
줄들.push(` ${c.gray('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 남))} ${c.gray('│')}`);
|
|
84
|
+
}
|
|
85
|
+
줄들.push(c.gray(` ╰${가로}╯`));
|
|
86
|
+
if (곁말) 줄들.push(` ${c.gray(곁말)}`);
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* 커서 자리.
|
|
90
|
+
*
|
|
91
|
+
* 글자 수가 아니라 **폭**으로 세야 한다. 한글은 두 칸이라, 글자 수로 세면
|
|
92
|
+
* '가나다' 를 친 뒤 커서가 세 칸 왼쪽에 가 있게 된다. 백스페이스를 누르면
|
|
93
|
+
* 엉뚱한 자리가 지워지는 것처럼 보인다.
|
|
94
|
+
*/
|
|
95
|
+
const 앞글 = 커서자리 == null ? 글 : String(글).slice(0, 커서자리);
|
|
96
|
+
const 앞줄들 = 앞글 ? 접어쓰기(앞글, 안쪽) : [''];
|
|
97
|
+
|
|
98
|
+
// 넘쳐서 앞부분을 잘라 냈으면 그만큼 줄 번호가 밀린다.
|
|
99
|
+
const 숨긴줄 = 안쪽줄.length - 보일줄.length;
|
|
100
|
+
const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 숨긴줄), 보일줄.length - 1);
|
|
101
|
+
|
|
102
|
+
const 아래 = (곁말 ? 1 : 0) + 1; // 곁말 + 닫는 테두리
|
|
103
|
+
const 위 = 아래 + (보일줄.length - 1 - 커서줄);
|
|
104
|
+
// 1부터 세는 칸. 앞머리(5칸) 다음이 6칸째다 — 거기가 첫 글자 자리다.
|
|
105
|
+
const 열 = Math.min(앞머리 + 1 + width(앞줄들.at(-1) ?? ''), 칸 - 1);
|
|
106
|
+
|
|
107
|
+
return { 줄들, 커서: { 위, 열 } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 터미널 맨 아래에 상자를 붙여 두는 화면.
|
|
112
|
+
*
|
|
113
|
+
* 대화는 stdout 으로 그냥 나간다(줄 화면과 같다). 우리가 지우고 다시 그리는
|
|
114
|
+
* 것은 상자 몇 줄뿐이다.
|
|
115
|
+
*/
|
|
116
|
+
export class InputBox {
|
|
117
|
+
constructor() {
|
|
118
|
+
this.그린줄 = 0; // 지금 화면에 붙어 있는 상자의 줄 수
|
|
119
|
+
/*
|
|
120
|
+
* 커서를 상자 맨 아랫줄에서 몇 줄 위에 놓아 뒀나.
|
|
121
|
+
*
|
|
122
|
+
* 이걸 안 들고 있으면 지울 때가 어긋난다. 상자가 한 줄짜리일 때는
|
|
123
|
+
* 커서가 늘 아래에서 첫째 줄이라 티가 안 나는데, 긴 글이 접혀 상자가
|
|
124
|
+
* 세 줄이 되거나 아래에 곁말이 붙는 순간 **몇 줄을 내려가야 하는지**가
|
|
125
|
+
* 달라진다. 고정값으로 내려가면 화면 맨 아래에 부딪혀 멈추고, 그 뒤로는
|
|
126
|
+
* 엉뚱한 줄을 지운다 — 대화가 한 줄씩 갉여 나간다.
|
|
127
|
+
*/
|
|
128
|
+
this.커서위 = 0;
|
|
129
|
+
this.session = null;
|
|
130
|
+
this.곁말 = '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 상자를 지운다. 위쪽 대화는 안 건드린다. */
|
|
134
|
+
지우기() {
|
|
135
|
+
if (!this.그린줄) return;
|
|
136
|
+
// 커서가 있는 자리에서 맨 아랫줄까지 내려간 뒤, 위로 훑어 올라가며 지운다.
|
|
137
|
+
let out = '\r' + (this.커서위 > 0 ? `\x1b[${this.커서위}B` : '') + '\r';
|
|
138
|
+
for (let i = 0; i < this.그린줄; i++) {
|
|
139
|
+
out += 줄끝지움;
|
|
140
|
+
if (i < this.그린줄 - 1) out += '\x1b[1A';
|
|
141
|
+
}
|
|
142
|
+
out += '\r';
|
|
143
|
+
process.stdout.write(out);
|
|
144
|
+
this.그린줄 = 0;
|
|
145
|
+
this.커서위 = 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 상자를 그린다(있으면 지우고 다시).
|
|
150
|
+
*
|
|
151
|
+
* @param {object} session 상태줄을 그리는 데 쓴다
|
|
152
|
+
* @param {string} 글 readline 이 들고 있는 글
|
|
153
|
+
* @param {number} 커서 그 안에서의 커서 위치
|
|
154
|
+
*/
|
|
155
|
+
그리기(session, 글 = '', 커서자리 = null) {
|
|
156
|
+
this.session = session ?? this.session;
|
|
157
|
+
this.지우기();
|
|
158
|
+
|
|
159
|
+
const { 줄들, 커서: 자리 } = 프레임({
|
|
160
|
+
글,
|
|
161
|
+
커서: 커서자리,
|
|
162
|
+
폭: process.stdout.columns ?? 80,
|
|
163
|
+
상태: this.session ? statusLine(this.session) : '',
|
|
164
|
+
경고: this.session ? (contextWarning(this.session) ?? '') : '',
|
|
165
|
+
곁말: this.곁말,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
cursor.hide();
|
|
169
|
+
process.stdout.write(줄들.map((l) => l + 줄끝지움).join('\n') + '\n');
|
|
170
|
+
/*
|
|
171
|
+
* 방금 찍은 마지막 줄 **다음** 줄에 커서가 있다. 거기서 되짚어 올라간다.
|
|
172
|
+
*
|
|
173
|
+
* 오른쪽으로 옮길 때 한 칸이 어긋나기 쉽다. \r 은 커서를 1칸째에 놓고,
|
|
174
|
+
* \x1b[nC 는 거기서 n칸 **더** 간다 — 그래서 1부터 세는 목표 칸으로
|
|
175
|
+
* 가려면 열-1 을 보내야 한다. 이걸 틀리면 친 글자가 한 칸씩 밀려 보이고,
|
|
176
|
+
* 백스페이스가 엉뚱한 자리를 지우는 것처럼 보인다.
|
|
177
|
+
*/
|
|
178
|
+
const 오른쪽 = Math.max(0, 자리.열 - 1);
|
|
179
|
+
process.stdout.write(위로(자리.위 + 1) + '\r' + (오른쪽 ? `\x1b[${오른쪽}C` : ''));
|
|
180
|
+
cursor.show();
|
|
181
|
+
this.그린줄 = 줄들.length;
|
|
182
|
+
this.커서위 = 자리.위;
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/ui/screen.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 왜 이 파일이 생겼나:
|
|
5
5
|
* repl.js 안에 say()·process.stdout.write 가 79군데 흩어져 있었다. 그 상태로는
|
|
6
|
-
*
|
|
6
|
+
* 화면을 하나 더 얹을 수가 없다 — 어디로 무엇이 나가는지 한 군데서 못 잡으니
|
|
7
7
|
* 두 화면이 같은 코드를 나눠 쓰려다 결국 둘 다 어중간해진다.
|
|
8
8
|
*
|
|
9
9
|
* 그래서 '무엇을 그린다' 와 '어떻게 그린다' 를 가른다. repl.js 는 이제
|
|
@@ -11,33 +11,46 @@
|
|
|
11
11
|
* 칸에 담아 다시 그릴지는 여기서 정한다.
|
|
12
12
|
*
|
|
13
13
|
* 두 가지 구현이 있다:
|
|
14
|
-
* 줄화면(LineScreen)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 줄화면(LineScreen) 지금까지의 그 화면. 위에서 아래로 흘러간다.
|
|
15
|
+
* 파이프·기록·CI·`deel run`·검사가 전부 이것을 읽는다.
|
|
16
|
+
* 상자화면(BoxScreen) ui/inputbox.js. 사람이 터미널 앞에 앉아 있을 때만.
|
|
17
|
+
* **대화는 똑같이 흘려보내고** 맨 아래 입력 상자만
|
|
18
|
+
* 우리가 지우고 다시 그린다.
|
|
17
19
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* ── 한 번 틀렸던 길 ─────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* 처음에는 터미널을 통째로 빌려(대체 화면) 대화 칸·파일 칸·할 일 칸을 나눠
|
|
23
|
+
* 그렸다. 보기에는 그럴듯했는데 **슬래시 명령이 전부 먹통**이 됐다.
|
|
24
|
+
* commands.js 를 비롯한 여섯 모듈이 화면 객체를 안 거치고 stdout 에 바로 쓰는데,
|
|
25
|
+
* 전체화면은 매번 자기 버퍼에서 화면을 다시 그리므로 그 글이 찍히자마자
|
|
26
|
+
* 덮여 사라졌다. 명령이 안 도는 게 아니라 결과가 안 보이는 것이라 더 나빴다.
|
|
27
|
+
*
|
|
28
|
+
* 고치려면 stdout 에 쓰는 자리를 전부 화면 객체로 꿰야 하는데, 지금 여섯 곳이고
|
|
29
|
+
* 앞으로 늘 것이며, 하나라도 빠뜨리면 같은 증상이 조용히 돌아온다. 그래서
|
|
30
|
+
* 대화는 그냥 흘려보내기로 했다. 그러면 stdout 에 쓰는 모든 코드가 손 안 대고
|
|
31
|
+
* 그대로 맞다. 스크롤·복사·`| tee` 도 그대로다.
|
|
21
32
|
*/
|
|
22
33
|
import { c, say, cursor, box, cols } from './ansi.js';
|
|
23
34
|
import { statusLine, contextWarning } from './status.js';
|
|
24
35
|
import { spin } from './spinner.js';
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
|
-
* 지금 이 자리에서
|
|
38
|
+
* 지금 이 자리에서 입력 상자를 써도 되는가.
|
|
39
|
+
*
|
|
40
|
+
* 하나라도 아니면 줄화면이다 — 애매하면 줄화면이 맞다. 잘못 켜면 사용자
|
|
41
|
+
* 화면이 깨지지만, 잘못 안 켜면 그냥 지금까지의 화면일 뿐이다.
|
|
28
42
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 잘못 안 켜면 그냥 지금까지의 화면일 뿐이다.
|
|
43
|
+
* 상자는 커서를 위로 올려 가며 자기가 그린 줄을 지운다. 그 앞자리가
|
|
44
|
+
* 터미널이 아니면(파이프·기록) 제어문자가 그대로 글에 섞인다.
|
|
32
45
|
*/
|
|
33
|
-
export function
|
|
46
|
+
export function 상자쓸까({ tui = null } = {}) {
|
|
34
47
|
if (tui === false) return false; // deel --no-tui
|
|
35
48
|
if (!process.stdout.isTTY) return false; // 파이프·기록·CI
|
|
36
49
|
if (!process.stdin.isTTY) return false; // 입력이 파이프로 들어옴 (검사·데모)
|
|
37
50
|
if (process.env.TERM === 'dumb') return false;
|
|
38
51
|
if (process.env.CI) return false;
|
|
39
|
-
//
|
|
40
|
-
if ((process.stdout.columns ?? 0) <
|
|
52
|
+
// 너무 좁으면 상자 안에 남는 자리가 없다. 테두리만 남고 글이 안 보인다.
|
|
53
|
+
if ((process.stdout.columns ?? 0) < 40) return false;
|
|
41
54
|
if (tui === true) return true;
|
|
42
55
|
return true;
|
|
43
56
|
}
|
|
@@ -96,7 +109,7 @@ export class LineScreen {
|
|
|
96
109
|
*
|
|
97
110
|
* Shift+Tab 으로 작업 모드를 돌릴 때처럼, 입력을 기다리는 도중에 화면에
|
|
98
111
|
* 한 줄을 끼워 넣어야 하는 자리가 있다. 안 지우면 `❯ ` 뒤에 새 글이 붙는다.
|
|
99
|
-
*
|
|
112
|
+
* 상자화면에서는 상자를 통째로 걷어낸다(BoxScreen 이 갈아 끼운다).
|
|
100
113
|
*/
|
|
101
114
|
입력지움() { cursor.clearLine(); }
|
|
102
115
|
|
|
@@ -134,13 +147,8 @@ export class LineScreen {
|
|
|
134
147
|
process.stdout.write(` ${c.hcyan('❯')} `);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
할일칸() { /* 할 일은 나올 때마다 줄로 흘려보낸다 (repl.js 가 그린다) */ }
|
|
141
|
-
|
|
142
|
-
/** 창 크기가 바뀌었다. 줄화면은 다시 그릴 것이 없다. */
|
|
143
|
-
다시그림() { }
|
|
150
|
+
/** 사람이 치는 중인 글이 바뀌었다. 줄화면은 readline 이 알아서 되비춘다. */
|
|
151
|
+
입력갱신() { }
|
|
144
152
|
|
|
145
153
|
close() {
|
|
146
154
|
this.돌림멈춤();
|
|
@@ -148,22 +156,59 @@ export class LineScreen {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
/**
|
|
160
|
+
* 대화는 그대로 흘려보내고, 맨 아래 입력 상자만 우리가 관리하는 화면.
|
|
161
|
+
*
|
|
162
|
+
* LineScreen 을 그대로 물려받는다 — 대화가 나가는 길이 **한 글자도 다르면
|
|
163
|
+
* 안 되기** 때문이다. 다르면 검사가 읽는 화면과 사람이 보는 화면이 갈라진다.
|
|
164
|
+
* 여기서 갈아 끼우는 것은 입력 자리 세 가지뿐이다.
|
|
165
|
+
*/
|
|
166
|
+
export class BoxScreen extends LineScreen {
|
|
167
|
+
constructor(상자) {
|
|
168
|
+
super();
|
|
169
|
+
this.kind = 'box';
|
|
170
|
+
this.상자 = 상자;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 대화 한 줄이 나가기 전에 상자를 걷어낸다. 안 걷으면 새 글이 상자 위에
|
|
174
|
+
// 겹쳐 찍히고, 지울 때 대화까지 같이 지워진다.
|
|
175
|
+
줄(s = '') { this.상자.지우기(); super.줄(s); }
|
|
176
|
+
붙임(s) { this.상자.지우기(); super.붙임(s); }
|
|
177
|
+
|
|
178
|
+
/** 입력 자리 = 상태줄 + 상자. 커서는 상자 안에 놓인다. */
|
|
179
|
+
입력자리(session) {
|
|
180
|
+
this.임시지움();
|
|
181
|
+
say('');
|
|
182
|
+
this.상자.그리기(session, '', 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 사람이 치는 중인 글이 바뀌었다 (repl.js 의 keypress 가 부른다). */
|
|
186
|
+
입력갱신(session, 글, 커서) { this.상자.그리기(session, 글, 커서); }
|
|
187
|
+
|
|
188
|
+
입력지움() { this.상자.지우기(); }
|
|
189
|
+
|
|
190
|
+
close() {
|
|
191
|
+
this.상자.지우기();
|
|
192
|
+
super.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
151
196
|
/**
|
|
152
197
|
* 상황에 맞는 화면을 하나 고른다.
|
|
153
198
|
*
|
|
154
|
-
*
|
|
199
|
+
* 상자 쪽은 필요할 때만 읽어 들인다. 줄화면으로 돌 때 inputbox.js 를
|
|
155
200
|
* 파싱조차 안 하게 하려는 것이다 — `deel run` 이 조금이라도 빨리 시작해야 한다.
|
|
156
201
|
*/
|
|
157
202
|
export async function 화면고르기(opts = {}) {
|
|
158
|
-
if (
|
|
203
|
+
if (!상자쓸까(opts)) return new LineScreen();
|
|
159
204
|
try {
|
|
160
|
-
const {
|
|
161
|
-
return new
|
|
205
|
+
const { InputBox } = await import('./inputbox.js');
|
|
206
|
+
return new BoxScreen(new InputBox());
|
|
162
207
|
} catch (e) {
|
|
163
|
-
//
|
|
208
|
+
// 상자를 못 세우면 조용히 줄화면으로 간다. 화면 하나 때문에
|
|
164
209
|
// 프로그램이 안 뜨는 일은 없어야 한다.
|
|
165
210
|
const s = new LineScreen();
|
|
166
|
-
s.줄(` ${c.gray(
|
|
211
|
+
s.줄(` ${c.gray(`입력 상자를 못 켰습니다 — 줄 화면으로 갑니다. (${e.message})`)}`);
|
|
167
212
|
return s;
|
|
168
213
|
}
|
|
169
214
|
}
|
package/src/ui/wrap.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 색을 유지하면서 폭에 맞춰 접기.
|
|
2
|
+
//
|
|
3
|
+
// 전에는 전체화면(tui.js)에만 쓰던 것이라 거기 있었다. 전체화면을 걷어내면서
|
|
4
|
+
// 여기로 옮긴다 — 입력 상자가 긴 입력을 접을 때 같은 것이 필요하다.
|
|
5
|
+
import { width, clip } from './ansi.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 색을 유지하면서 폭에 맞춰 접는다.
|
|
9
|
+
*
|
|
10
|
+
* clip 은 잘라 버리지만 사람이 친 글은 잘리면 안 된다 — 뒷부분이 통째로
|
|
11
|
+
* 사라진다. 그래서 접는다. 색 코드는 폭이 0 이므로 세지 않고, 줄을 넘길 때
|
|
12
|
+
* 마지막으로 켜져 있던 색을 다음 줄 앞에 다시 켜 준다. 안 그러면 접힌 줄부터
|
|
13
|
+
* 색이 풀려 화면이 얼룩덜룩해진다.
|
|
14
|
+
*/
|
|
15
|
+
export function 접어쓰기(글, 폭) {
|
|
16
|
+
const s = String(글 ?? '');
|
|
17
|
+
if (폭 < 2) return [s];
|
|
18
|
+
if (width(s) <= 폭) return [s];
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* 접힌 줄에 앞 들여쓰기를 물려 준다.
|
|
22
|
+
*
|
|
23
|
+
* 안 물려 주면 ` ▌ 모델의 답…` 이 접히는 순간 다음 줄이 왼쪽 끝에서
|
|
24
|
+
* 시작한다. 화면에서 세로줄이 끊겨 보이고, 도구 결과의 ` └ …` 도
|
|
25
|
+
* 두 번째 줄부터 갑자기 튀어나온다 — 무엇에 딸린 글인지 알 수 없게 된다.
|
|
26
|
+
* 들여쓴 만큼은 글자 자리에서 빼야 하므로 접는 폭도 같이 줄인다.
|
|
27
|
+
*/
|
|
28
|
+
const 들여 = (/^ */.exec(s)[0] ?? '').slice(0, Math.max(0, 폭 - 8));
|
|
29
|
+
const 이어폭 = Math.max(2, 폭 - 들여.length);
|
|
30
|
+
|
|
31
|
+
const 줄들 = [];
|
|
32
|
+
let 지금 = '';
|
|
33
|
+
let w = 0;
|
|
34
|
+
let 색 = ''; // 지금 켜져 있는 SGR
|
|
35
|
+
for (let i = 0; i < s.length;) {
|
|
36
|
+
// 색 코드는 통째로 옮긴다. 폭에는 안 센다.
|
|
37
|
+
if (s[i] === '\x1b') {
|
|
38
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
39
|
+
if (m) {
|
|
40
|
+
지금 += m[0];
|
|
41
|
+
색 = /\x1b\[0?m/.test(m[0]) ? '' : 색 + m[0];
|
|
42
|
+
i += m[0].length;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const ch = s[i];
|
|
47
|
+
const cw = width(ch);
|
|
48
|
+
// 첫 줄은 원래 폭, 접힌 줄부터는 들여쓴 만큼 좁아진다.
|
|
49
|
+
const 한도 = 줄들.length === 0 ? 폭 : 이어폭 + 들여.length;
|
|
50
|
+
if (w + cw > 한도) {
|
|
51
|
+
줄들.push(지금 + (색 ? '\x1b[0m' : ''));
|
|
52
|
+
지금 = 들여 + 색;
|
|
53
|
+
w = 들여.length;
|
|
54
|
+
}
|
|
55
|
+
지금 += ch;
|
|
56
|
+
w += cw;
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
if (지금.trim() !== '' || 줄들.length === 0) 줄들.push(지금 + (색 ? '\x1b[0m' : ''));
|
|
60
|
+
return 줄들;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 폭에 맞춰 오른쪽을 공백으로 채운다. 안 채우면 앞 그림이 남는다. */
|
|
64
|
+
export function 채움(글, 폭) {
|
|
65
|
+
const w = width(글);
|
|
66
|
+
if (w > 폭) return clip(글, 폭);
|
|
67
|
+
return 글 + ' '.repeat(폭 - w);
|
|
68
|
+
}
|