@andocorp/cli 0.1.1 → 0.1.2
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/dist/index.js +13911 -0
- package/package.json +5 -5
- package/src/adapters.test.ts +0 -50
- package/src/adapters.ts +0 -215
- package/src/args.test.ts +0 -28
- package/src/args.ts +0 -98
- package/src/cli-helpers.test.ts +0 -82
- package/src/cli-helpers.ts +0 -149
- package/src/client.test.ts +0 -235
- package/src/client.ts +0 -378
- package/src/components/prompt-line.ts +0 -179
- package/src/components/transcript-pane.test.ts +0 -26
- package/src/components/transcript-pane.ts +0 -457
- package/src/config.ts +0 -53
- package/src/emoji-suggestions.ts +0 -152
- package/src/format.test.ts +0 -54
- package/src/format.ts +0 -611
- package/src/help.test.ts +0 -13
- package/src/help.ts +0 -48
- package/src/index.ts +0 -466
- package/src/interactive.ts +0 -832
- package/src/test-helpers.ts +0 -207
- package/src/types.ts +0 -24
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
import { getEmojiSuggestions } from "../emoji-suggestions.js";
|
|
3
|
-
import { truncate } from "../format.js";
|
|
4
|
-
|
|
5
|
-
export type PromptLineOptions = {
|
|
6
|
-
enableEmojiSuggestions?: boolean;
|
|
7
|
-
enableNumberSelect?: boolean;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export async function promptLine(
|
|
11
|
-
label: string,
|
|
12
|
-
options: PromptLineOptions = {}
|
|
13
|
-
) {
|
|
14
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
15
|
-
const rl = readline.createInterface({
|
|
16
|
-
input: process.stdin,
|
|
17
|
-
output: process.stdout,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const answer = await new Promise<string>((resolve) => {
|
|
21
|
-
rl.question(label, (value) => resolve(value));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
rl.close();
|
|
25
|
-
return answer.trim();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
|
|
29
|
-
process.stdin.setRawMode(true);
|
|
30
|
-
process.stdin.resume();
|
|
31
|
-
readline.emitKeypressEvents(process.stdin);
|
|
32
|
-
|
|
33
|
-
let value = "";
|
|
34
|
-
let cursor = 0;
|
|
35
|
-
let currentSuggestions = getEmojiSuggestions("", 5);
|
|
36
|
-
let suggestionLineCount = 0;
|
|
37
|
-
|
|
38
|
-
const render = () => {
|
|
39
|
-
currentSuggestions = options.enableEmojiSuggestions
|
|
40
|
-
? getEmojiSuggestions(value, 5)
|
|
41
|
-
: [];
|
|
42
|
-
const width = process.stdout.columns ?? 120;
|
|
43
|
-
const suggestionLine =
|
|
44
|
-
currentSuggestions.length === 0
|
|
45
|
-
? ""
|
|
46
|
-
: truncate(
|
|
47
|
-
`Suggestions: ${currentSuggestions
|
|
48
|
-
.map(
|
|
49
|
-
(suggestion, index) =>
|
|
50
|
-
`[${index + 1}] ${suggestion.emoji} :${suggestion.shortcode}:`
|
|
51
|
-
)
|
|
52
|
-
.join(" ")}`,
|
|
53
|
-
width
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
readline.cursorTo(process.stdout, 0);
|
|
57
|
-
readline.clearScreenDown(process.stdout);
|
|
58
|
-
process.stdout.write(`${label}${value}`);
|
|
59
|
-
|
|
60
|
-
suggestionLineCount = suggestionLine === "" ? 0 : 1;
|
|
61
|
-
if (suggestionLineCount > 0) {
|
|
62
|
-
process.stdout.write(`\n${suggestionLine}`);
|
|
63
|
-
readline.moveCursor(process.stdout, 0, -1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
readline.cursorTo(process.stdout, label.length + cursor);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const answer = await new Promise<string>((resolve) => {
|
|
70
|
-
const handleResize = () => {
|
|
71
|
-
render();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const handleKeypress = (chunk: string, key: readline.Key) => {
|
|
75
|
-
if (key.ctrl && key.name === "c") {
|
|
76
|
-
cleanup();
|
|
77
|
-
process.stdout.write("\u001Bc");
|
|
78
|
-
process.exit(0);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (key.name === "return" || key.name === "enter") {
|
|
83
|
-
cleanup();
|
|
84
|
-
process.stdout.write("\n");
|
|
85
|
-
if (suggestionLineCount > 0) {
|
|
86
|
-
readline.clearLine(process.stdout, 0);
|
|
87
|
-
readline.cursorTo(process.stdout, 0);
|
|
88
|
-
}
|
|
89
|
-
resolve(value.trim());
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (key.name === "escape") {
|
|
94
|
-
cleanup();
|
|
95
|
-
process.stdout.write("\n");
|
|
96
|
-
if (suggestionLineCount > 0) {
|
|
97
|
-
readline.clearLine(process.stdout, 0);
|
|
98
|
-
readline.cursorTo(process.stdout, 0);
|
|
99
|
-
}
|
|
100
|
-
resolve("");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (options.enableNumberSelect && chunk >= "1" && chunk <= "5") {
|
|
105
|
-
const suggestionIndex = Number(chunk) - 1;
|
|
106
|
-
const selectedSuggestion = currentSuggestions[suggestionIndex];
|
|
107
|
-
if (selectedSuggestion != null) {
|
|
108
|
-
cleanup();
|
|
109
|
-
process.stdout.write("\n");
|
|
110
|
-
if (suggestionLineCount > 0) {
|
|
111
|
-
readline.clearLine(process.stdout, 0);
|
|
112
|
-
readline.cursorTo(process.stdout, 0);
|
|
113
|
-
}
|
|
114
|
-
resolve(selectedSuggestion.emoji);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (key.name === "backspace") {
|
|
120
|
-
if (cursor > 0) {
|
|
121
|
-
value = `${value.slice(0, Math.max(0, cursor - 1))}${value.slice(cursor)}`;
|
|
122
|
-
cursor -= 1;
|
|
123
|
-
}
|
|
124
|
-
render();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (key.name === "delete") {
|
|
129
|
-
if (cursor < value.length) {
|
|
130
|
-
value = `${value.slice(0, cursor)}${value.slice(cursor + 1)}`;
|
|
131
|
-
}
|
|
132
|
-
render();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (key.name === "left") {
|
|
137
|
-
cursor = Math.max(0, cursor - 1);
|
|
138
|
-
render();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (key.name === "right") {
|
|
143
|
-
cursor = Math.min(value.length, cursor + 1);
|
|
144
|
-
render();
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (key.ctrl && key.name === "a") {
|
|
149
|
-
cursor = 0;
|
|
150
|
-
render();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (key.ctrl && key.name === "e") {
|
|
155
|
-
cursor = value.length;
|
|
156
|
-
render();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (chunk !== "" && !key.ctrl && !key.meta && key.name !== "tab") {
|
|
161
|
-
value = `${value.slice(0, cursor)}${chunk}${value.slice(cursor)}`;
|
|
162
|
-
cursor += chunk.length;
|
|
163
|
-
render();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const cleanup = () => {
|
|
168
|
-
process.stdin.off("keypress", handleKeypress);
|
|
169
|
-
process.stdout.off("resize", handleResize);
|
|
170
|
-
process.stdin.setRawMode(wasRaw);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
process.stdin.on("keypress", handleKeypress);
|
|
174
|
-
process.stdout.on("resize", handleResize);
|
|
175
|
-
render();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return answer;
|
|
179
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { renderTranscriptLines } from "./transcript-pane.js";
|
|
3
|
-
import { createMessage } from "../test-helpers.js";
|
|
4
|
-
|
|
5
|
-
describe("transcript pane", () => {
|
|
6
|
-
it("keeps wrapped markdown links blue without showing raw markdown", () => {
|
|
7
|
-
const url =
|
|
8
|
-
"https://open.spotify.com/track/1CGzr7mkl2JRD3dqNRG2pS?si=77035efd1f8342ab";
|
|
9
|
-
const result = renderTranscriptLines({
|
|
10
|
-
items: [
|
|
11
|
-
createMessage({
|
|
12
|
-
markdown_content: `[${url}](${url})`,
|
|
13
|
-
}),
|
|
14
|
-
],
|
|
15
|
-
selectedIndex: 0,
|
|
16
|
-
width: 40,
|
|
17
|
-
bodyRows: 8,
|
|
18
|
-
isFocusedPane: true,
|
|
19
|
-
currentMemberId: null,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const joinedOutput = result.lines.join("\n");
|
|
23
|
-
expect(joinedOutput).toContain("\u001b[34mhttps://open.spotify.com/track/");
|
|
24
|
-
expect(joinedOutput).not.toContain(`[${url}](${url})`);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
formatDateSeparator,
|
|
3
|
-
formatMessageFooter,
|
|
4
|
-
formatMessageHeaderParts,
|
|
5
|
-
getMessageBody,
|
|
6
|
-
getMessageBodyLines,
|
|
7
|
-
getVisibleTextLength,
|
|
8
|
-
LINK_END_MARKER,
|
|
9
|
-
LINK_SEPARATOR_MARKER,
|
|
10
|
-
LINK_START_MARKER,
|
|
11
|
-
renderCodeBlockText,
|
|
12
|
-
renderTerminalText,
|
|
13
|
-
truncate,
|
|
14
|
-
} from "../format.js";
|
|
15
|
-
import { Message } from "../types.js";
|
|
16
|
-
|
|
17
|
-
const ANSI_RESET = "\u001b[0m";
|
|
18
|
-
const ANSI_MESSAGE_HEADER = "\u001b[38;5;248m";
|
|
19
|
-
const ANSI_SELECTION_MARKER = "\u001b[38;2;255;215;0m";
|
|
20
|
-
|
|
21
|
-
type InlineSegment = {
|
|
22
|
-
text: string;
|
|
23
|
-
url?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type InlineToken = {
|
|
27
|
-
text: string;
|
|
28
|
-
visibleLength: number;
|
|
29
|
-
url?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function getSelectedItemPrefix(params: {
|
|
33
|
-
isFocusedPane: boolean;
|
|
34
|
-
isSelected: boolean;
|
|
35
|
-
}) {
|
|
36
|
-
if (!params.isSelected) {
|
|
37
|
-
return " ";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return `${ANSI_SELECTION_MARKER}${params.isFocusedPane ? ">" : "-"}${ANSI_RESET}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function formatLinkedText(url: string, text: string) {
|
|
44
|
-
return `${LINK_START_MARKER}${url}${LINK_SEPARATOR_MARKER}${text}${LINK_END_MARKER}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseInlineSegments(text: string): InlineSegment[] {
|
|
48
|
-
const segments: InlineSegment[] = [];
|
|
49
|
-
let buffer = "";
|
|
50
|
-
let index = 0;
|
|
51
|
-
|
|
52
|
-
const flushBuffer = () => {
|
|
53
|
-
if (buffer === "") {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
segments.push({ text: buffer });
|
|
58
|
-
buffer = "";
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
while (index < text.length) {
|
|
62
|
-
const character = text[index];
|
|
63
|
-
if (character == null) {
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (character !== LINK_START_MARKER) {
|
|
68
|
-
buffer += character;
|
|
69
|
-
index += 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
flushBuffer();
|
|
74
|
-
index += 1;
|
|
75
|
-
|
|
76
|
-
let url = "";
|
|
77
|
-
while (index < text.length && text[index] !== LINK_SEPARATOR_MARKER) {
|
|
78
|
-
const urlCharacter = text[index];
|
|
79
|
-
if (urlCharacter != null) {
|
|
80
|
-
url += urlCharacter;
|
|
81
|
-
}
|
|
82
|
-
index += 1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (text[index] === LINK_SEPARATOR_MARKER) {
|
|
86
|
-
index += 1;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let label = "";
|
|
90
|
-
while (index < text.length && text[index] !== LINK_END_MARKER) {
|
|
91
|
-
const labelCharacter = text[index];
|
|
92
|
-
if (labelCharacter != null) {
|
|
93
|
-
label += labelCharacter;
|
|
94
|
-
}
|
|
95
|
-
index += 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (text[index] === LINK_END_MARKER) {
|
|
99
|
-
index += 1;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
segments.push({ text: label, url });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
flushBuffer();
|
|
106
|
-
return segments;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function splitToken(token: InlineToken, width: number) {
|
|
110
|
-
const chunks: InlineToken[] = [];
|
|
111
|
-
for (let index = 0; index < token.text.length; index += width) {
|
|
112
|
-
const chunkText = token.text.slice(index, index + width);
|
|
113
|
-
chunks.push({
|
|
114
|
-
text: chunkText,
|
|
115
|
-
visibleLength: chunkText.length,
|
|
116
|
-
url: token.url,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return chunks;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function tokenizeInlineSegments(text: string): InlineToken[] {
|
|
124
|
-
const tokens: InlineToken[] = [];
|
|
125
|
-
|
|
126
|
-
for (const segment of parseInlineSegments(text)) {
|
|
127
|
-
let buffer = "";
|
|
128
|
-
for (const character of segment.text) {
|
|
129
|
-
if (character === " ") {
|
|
130
|
-
if (buffer !== "") {
|
|
131
|
-
tokens.push({
|
|
132
|
-
text: buffer,
|
|
133
|
-
visibleLength: buffer.length,
|
|
134
|
-
url: segment.url,
|
|
135
|
-
});
|
|
136
|
-
buffer = "";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
tokens.push({
|
|
140
|
-
text: " ",
|
|
141
|
-
visibleLength: 1,
|
|
142
|
-
});
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
buffer += character;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (buffer !== "") {
|
|
150
|
-
tokens.push({
|
|
151
|
-
text: buffer,
|
|
152
|
-
visibleLength: buffer.length,
|
|
153
|
-
url: segment.url,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return tokens;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function renderInlineTokens(tokens: InlineToken[]) {
|
|
162
|
-
return tokens
|
|
163
|
-
.map((token) =>
|
|
164
|
-
token.url == null ? token.text : formatLinkedText(token.url, token.text)
|
|
165
|
-
)
|
|
166
|
-
.join("");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function wrapLine(text: string, width: number) {
|
|
170
|
-
if (width <= 0) {
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const tokens = tokenizeInlineSegments(text);
|
|
175
|
-
const lines: string[] = [];
|
|
176
|
-
let currentTokens: InlineToken[] = [];
|
|
177
|
-
let currentLength = 0;
|
|
178
|
-
|
|
179
|
-
const pushCurrentLine = () => {
|
|
180
|
-
while (currentTokens[0]?.text === " ") {
|
|
181
|
-
currentTokens.shift();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
while (currentTokens[currentTokens.length - 1]?.text === " ") {
|
|
185
|
-
currentTokens.pop();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (currentTokens.length === 0) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
lines.push(renderInlineTokens(currentTokens));
|
|
193
|
-
currentTokens = [];
|
|
194
|
-
currentLength = 0;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
for (const token of tokens) {
|
|
198
|
-
if (token.text === " ") {
|
|
199
|
-
if (currentLength === 0) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (currentLength + 1 <= width) {
|
|
204
|
-
currentTokens.push(token);
|
|
205
|
-
currentLength += 1;
|
|
206
|
-
} else {
|
|
207
|
-
pushCurrentLine();
|
|
208
|
-
}
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (token.visibleLength <= width - currentLength) {
|
|
213
|
-
currentTokens.push(token);
|
|
214
|
-
currentLength += token.visibleLength;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (currentLength > 0) {
|
|
219
|
-
pushCurrentLine();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (token.visibleLength <= width) {
|
|
223
|
-
currentTokens.push(token);
|
|
224
|
-
currentLength = token.visibleLength;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const chunk of splitToken(token, width)) {
|
|
229
|
-
lines.push(renderInlineTokens([chunk]));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
pushCurrentLine();
|
|
234
|
-
return lines;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function wrapPreformattedLine(text: string, width: number) {
|
|
238
|
-
if (width <= 0) {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (getVisibleTextLength(text) <= width) {
|
|
243
|
-
return [text];
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const lines: string[] = [];
|
|
247
|
-
for (const segment of parseInlineSegments(text)) {
|
|
248
|
-
if (segment.text === "") {
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
for (let index = 0; index < segment.text.length; index += width) {
|
|
253
|
-
const chunk = segment.text.slice(index, index + width);
|
|
254
|
-
lines.push(
|
|
255
|
-
segment.url == null ? chunk : formatLinkedText(segment.url, chunk)
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return lines;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function centerText(text: string, width: number) {
|
|
264
|
-
if (width <= 0) {
|
|
265
|
-
return "";
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (text.length >= width) {
|
|
269
|
-
return truncate(text, width);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const leftPadding = Math.floor((width - text.length) / 2);
|
|
273
|
-
return `${" ".repeat(leftPadding)}${text}`;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function colorMessageHeader(text: string) {
|
|
277
|
-
return `${ANSI_MESSAGE_HEADER}${text}${ANSI_RESET}`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function getMessageDateKey(value: Date | string | number | null | undefined) {
|
|
281
|
-
if (value == null) {
|
|
282
|
-
return "";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const date = new Date(value);
|
|
286
|
-
if (Number.isNaN(date.getTime())) {
|
|
287
|
-
return "";
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function renderTranscriptLines(params: {
|
|
294
|
-
items: Message[];
|
|
295
|
-
selectedIndex: number;
|
|
296
|
-
width: number;
|
|
297
|
-
bodyRows: number;
|
|
298
|
-
isFocusedPane: boolean;
|
|
299
|
-
currentMemberId?: string | null;
|
|
300
|
-
}) {
|
|
301
|
-
const {
|
|
302
|
-
items,
|
|
303
|
-
selectedIndex,
|
|
304
|
-
width,
|
|
305
|
-
bodyRows,
|
|
306
|
-
isFocusedPane,
|
|
307
|
-
currentMemberId,
|
|
308
|
-
} = params;
|
|
309
|
-
|
|
310
|
-
if (width <= 0 || bodyRows <= 0) {
|
|
311
|
-
return {
|
|
312
|
-
lines: [],
|
|
313
|
-
hasAbove: false,
|
|
314
|
-
hasBelow: false,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const blocks = items.map((message, index) => {
|
|
319
|
-
const previousMessage = index > 0 ? items[index - 1] : null;
|
|
320
|
-
const showDateSeparator =
|
|
321
|
-
previousMessage == null ||
|
|
322
|
-
getMessageDateKey(previousMessage.created_at) !==
|
|
323
|
-
getMessageDateKey(message.created_at);
|
|
324
|
-
|
|
325
|
-
const prefix = getSelectedItemPrefix({
|
|
326
|
-
isFocusedPane,
|
|
327
|
-
isSelected: index === selectedIndex,
|
|
328
|
-
});
|
|
329
|
-
const contentWidth = Math.max(8, width - 4);
|
|
330
|
-
const bodyLines = getMessageBodyLines(message).flatMap((line) => {
|
|
331
|
-
const wrappedLine = line.preserveWhitespace
|
|
332
|
-
? wrapPreformattedLine(line.text, contentWidth)
|
|
333
|
-
: wrapLine(line.text, contentWidth);
|
|
334
|
-
|
|
335
|
-
if (wrappedLine.length > 0) {
|
|
336
|
-
return wrappedLine.map((text) => ({
|
|
337
|
-
text,
|
|
338
|
-
isCodeBlock: line.isCodeBlock === true,
|
|
339
|
-
}));
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return [{ text: line.text, isCodeBlock: line.isCodeBlock === true }];
|
|
343
|
-
});
|
|
344
|
-
const renderedBodyLines =
|
|
345
|
-
bodyLines.length > 0
|
|
346
|
-
? bodyLines
|
|
347
|
-
: [{ text: getMessageBody(message), isCodeBlock: false }];
|
|
348
|
-
const footer = formatMessageFooter(message);
|
|
349
|
-
|
|
350
|
-
const lines: { text: string; isCodeBlock?: boolean; isHeader: boolean }[] = [];
|
|
351
|
-
if (showDateSeparator) {
|
|
352
|
-
lines.push({
|
|
353
|
-
text: centerText(formatDateSeparator(message.created_at), width),
|
|
354
|
-
isHeader: false,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
lines.push({
|
|
359
|
-
text: (() => {
|
|
360
|
-
const header = formatMessageHeaderParts(message, { currentMemberId });
|
|
361
|
-
return `${prefix} ${colorMessageHeader(header.timestamp)} ${header.author}`;
|
|
362
|
-
})(),
|
|
363
|
-
isHeader: true,
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
for (let bodyLineIndex = 0; bodyLineIndex < renderedBodyLines.length; bodyLineIndex += 1) {
|
|
367
|
-
const line = renderedBodyLines[bodyLineIndex];
|
|
368
|
-
if (line == null) {
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (line.isCodeBlock === true) {
|
|
373
|
-
const codeBlockLines = [line.text];
|
|
374
|
-
let nextIndex = bodyLineIndex + 1;
|
|
375
|
-
while (nextIndex < renderedBodyLines.length) {
|
|
376
|
-
const nextLine = renderedBodyLines[nextIndex];
|
|
377
|
-
if (nextLine?.isCodeBlock !== true) {
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
codeBlockLines.push(nextLine.text);
|
|
382
|
-
nextIndex += 1;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const codeBlockWidth = Math.max(
|
|
386
|
-
1,
|
|
387
|
-
...codeBlockLines.map((codeLine) => getVisibleTextLength(codeLine))
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
lines.push({
|
|
391
|
-
text: ` ┌${"─".repeat(codeBlockWidth + 2)}┐`,
|
|
392
|
-
isHeader: false,
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
for (const codeLine of codeBlockLines) {
|
|
396
|
-
const visibleLength = getVisibleTextLength(codeLine);
|
|
397
|
-
lines.push({
|
|
398
|
-
text: ` │ ${renderCodeBlockText(codeLine)}${" ".repeat(codeBlockWidth - visibleLength)} │`,
|
|
399
|
-
isHeader: false,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
lines.push({
|
|
404
|
-
text: ` └${"─".repeat(codeBlockWidth + 2)}┘`,
|
|
405
|
-
isHeader: false,
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
bodyLineIndex = nextIndex - 1;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
lines.push({
|
|
413
|
-
text: ` ${line.text}`,
|
|
414
|
-
isHeader: false,
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (footer !== "") {
|
|
419
|
-
lines.push({ text: ` ${footer}`, isHeader: false });
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return { index, lines };
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
const selectedBlockIndex = blocks.findIndex((block) => block.index === selectedIndex);
|
|
426
|
-
const safeSelectedBlockIndex = selectedBlockIndex === -1 ? 0 : selectedBlockIndex;
|
|
427
|
-
|
|
428
|
-
let visibleStart = 0;
|
|
429
|
-
let visibleEnd = blocks.length;
|
|
430
|
-
let visibleLineCount = blocks.reduce((sum, block) => sum + block.lines.length, 0);
|
|
431
|
-
|
|
432
|
-
while (visibleLineCount > bodyRows && visibleStart < safeSelectedBlockIndex) {
|
|
433
|
-
visibleLineCount -= blocks[visibleStart]?.lines.length ?? 0;
|
|
434
|
-
visibleStart += 1;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
while (visibleLineCount > bodyRows && visibleEnd - 1 > safeSelectedBlockIndex) {
|
|
438
|
-
visibleEnd -= 1;
|
|
439
|
-
visibleLineCount -= blocks[visibleEnd]?.lines.length ?? 0;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const renderedLines = blocks
|
|
443
|
-
.slice(visibleStart, visibleEnd)
|
|
444
|
-
.flatMap((block) => block.lines)
|
|
445
|
-
.slice(0, bodyRows)
|
|
446
|
-
.map((line) => {
|
|
447
|
-
const truncatedText = truncate(line.text, width);
|
|
448
|
-
const coloredText = renderTerminalText(truncatedText);
|
|
449
|
-
return line.isHeader ? colorMessageHeader(coloredText) : coloredText;
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
lines: renderedLines,
|
|
454
|
-
hasAbove: visibleStart > 0,
|
|
455
|
-
hasBelow: visibleEnd < blocks.length,
|
|
456
|
-
};
|
|
457
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { SavedConfig } from "./types.js";
|
|
5
|
-
|
|
6
|
-
type SavedConfigFile = SavedConfig & {
|
|
7
|
-
savedAt: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function getConfigPath() {
|
|
11
|
-
return path.join(os.homedir(), ".config", "ando", "config.json");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function readSavedConfig(): Promise<SavedConfig | null> {
|
|
15
|
-
try {
|
|
16
|
-
const raw = await readFile(getConfigPath(), "utf8");
|
|
17
|
-
const parsed = JSON.parse(raw) as Partial<SavedConfigFile>;
|
|
18
|
-
if (
|
|
19
|
-
typeof parsed.sessionToken !== "string" ||
|
|
20
|
-
parsed.sessionToken.trim() === "" ||
|
|
21
|
-
typeof parsed.convexUrl !== "string" ||
|
|
22
|
-
parsed.convexUrl.trim() === ""
|
|
23
|
-
) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
sessionToken: parsed.sessionToken,
|
|
29
|
-
convexUrl: parsed.convexUrl,
|
|
30
|
-
};
|
|
31
|
-
} catch (error) {
|
|
32
|
-
const errorCode =
|
|
33
|
-
error instanceof Error && "code" in error ? error.code : undefined;
|
|
34
|
-
if (errorCode === "ENOENT") {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function saveConfig(config: SavedConfig) {
|
|
43
|
-
const configPath = getConfigPath();
|
|
44
|
-
await mkdir(path.dirname(configPath), { recursive: true });
|
|
45
|
-
const payload: SavedConfigFile = {
|
|
46
|
-
...config,
|
|
47
|
-
savedAt: new Date().toISOString(),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
await writeFile(configPath, JSON.stringify(payload, null, 2), {
|
|
51
|
-
mode: 0o600,
|
|
52
|
-
});
|
|
53
|
-
}
|