bet-cli 0.1.4 → 0.3.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.md +26 -10
- package/dist/commands/edit.js +48 -0
- package/dist/commands/ignore.js +1 -1
- package/dist/commands/info.js +16 -16
- package/dist/commands/list.js +32 -17
- package/dist/commands/update.js +13 -6
- package/dist/index.js +8 -29
- package/dist/lib/config.js +12 -0
- package/dist/lib/editor.js +97 -0
- package/dist/lib/git.js +14 -8
- package/dist/lib/help.js +7 -3
- package/dist/lib/readme.js +5 -2
- package/dist/main.js +50 -0
- package/dist/ui/markdown.js +8 -9
- package/dist/ui/search.js +6 -7
- package/dist/ui/select.js +9 -11
- package/package.json +4 -1
- package/src/commands/edit.ts +56 -0
- package/src/commands/ignore.ts +1 -1
- package/src/commands/info.tsx +225 -136
- package/src/commands/list.ts +81 -53
- package/src/commands/update.ts +203 -148
- package/src/index.ts +8 -34
- package/src/lib/config.ts +20 -1
- package/src/lib/editor.ts +131 -0
- package/src/lib/git.ts +20 -10
- package/src/lib/help.ts +15 -15
- package/src/lib/readme.ts +6 -1
- package/src/lib/types.ts +4 -0
- package/src/main.ts +57 -0
- package/src/ui/markdown.tsx +14 -0
- package/src/ui/search.tsx +55 -24
- package/src/ui/select.tsx +48 -27
- package/tests/config.test.ts +71 -0
- package/tests/editor.test.ts +167 -0
package/src/lib/config.ts
CHANGED
|
@@ -68,11 +68,25 @@ function normalizeIgnoredPaths(parsed: unknown): string[] | undefined {
|
|
|
68
68
|
return list.length === 0 ? undefined : list;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function normalizeEditor(parsed: unknown): string | undefined {
|
|
72
|
+
if (typeof parsed !== "string") return undefined;
|
|
73
|
+
const trimmed = parsed.trim();
|
|
74
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
async function readAppConfig(): Promise<AppConfig> {
|
|
72
78
|
try {
|
|
73
79
|
const raw = await fs.readFile(CONFIG_PATH, "utf8");
|
|
74
|
-
const parsed = JSON.parse(raw) as {
|
|
80
|
+
const parsed = JSON.parse(raw) as {
|
|
81
|
+
version?: number;
|
|
82
|
+
roots?: unknown;
|
|
83
|
+
editor?: unknown;
|
|
84
|
+
ignores?: unknown;
|
|
85
|
+
ignoredPaths?: unknown;
|
|
86
|
+
slugParentFolders?: unknown;
|
|
87
|
+
};
|
|
75
88
|
const roots = normalizeRoots(parsed.roots ?? []);
|
|
89
|
+
const editor = normalizeEditor(parsed.editor);
|
|
76
90
|
const ignores = normalizeIgnores(parsed.ignores);
|
|
77
91
|
const ignoredPaths = normalizeIgnoredPaths(parsed.ignoredPaths);
|
|
78
92
|
const slugParentFolders = normalizeSlugParentFolders(parsed.slugParentFolders);
|
|
@@ -80,6 +94,7 @@ async function readAppConfig(): Promise<AppConfig> {
|
|
|
80
94
|
...DEFAULT_APP_CONFIG,
|
|
81
95
|
version: parsed.version ?? 1,
|
|
82
96
|
roots,
|
|
97
|
+
...(editor !== undefined && { editor }),
|
|
83
98
|
...(ignores !== undefined && { ignores }),
|
|
84
99
|
...(ignoredPaths !== undefined && { ignoredPaths }),
|
|
85
100
|
...(slugParentFolders !== undefined && { slugParentFolders }),
|
|
@@ -95,6 +110,7 @@ async function readProjectsConfig(): Promise<ProjectsConfig> {
|
|
|
95
110
|
const parsed = JSON.parse(raw) as ProjectsConfig;
|
|
96
111
|
return {
|
|
97
112
|
projects: parsed.projects ?? {},
|
|
113
|
+
...(parsed.updatedAt != null && typeof parsed.updatedAt === "string" && { updatedAt: parsed.updatedAt }),
|
|
98
114
|
};
|
|
99
115
|
} catch (error) {
|
|
100
116
|
return { ...DEFAULT_PROJECTS_CONFIG };
|
|
@@ -123,6 +139,7 @@ export async function readConfig(): Promise<Config> {
|
|
|
123
139
|
return {
|
|
124
140
|
...appConfig,
|
|
125
141
|
projects: normalizedProjects,
|
|
142
|
+
...(projectsConfig.updatedAt != null && { updatedAt: projectsConfig.updatedAt }),
|
|
126
143
|
};
|
|
127
144
|
}
|
|
128
145
|
|
|
@@ -144,12 +161,14 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
144
161
|
const appConfig: AppConfig = {
|
|
145
162
|
version: config.version,
|
|
146
163
|
roots: config.roots,
|
|
164
|
+
...(config.editor !== undefined && { editor: config.editor }),
|
|
147
165
|
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
148
166
|
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
149
167
|
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
|
150
168
|
};
|
|
151
169
|
const projectsConfig: ProjectsConfig = {
|
|
152
170
|
projects: config.projects,
|
|
171
|
+
...(config.updatedAt !== undefined && { updatedAt: config.updatedAt }),
|
|
153
172
|
};
|
|
154
173
|
await Promise.all([
|
|
155
174
|
writeAppConfig(appConfig),
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type LaunchCommand = {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function tokenizeCommand(input: string): string[] {
|
|
9
|
+
const tokens: string[] = [];
|
|
10
|
+
let current = "";
|
|
11
|
+
let inSingle = false;
|
|
12
|
+
let inDouble = false;
|
|
13
|
+
let escaped = false;
|
|
14
|
+
|
|
15
|
+
for (const char of input) {
|
|
16
|
+
if (escaped) {
|
|
17
|
+
current += char;
|
|
18
|
+
escaped = false;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (char === "\\") {
|
|
23
|
+
escaped = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (char === "'" && !inDouble) {
|
|
28
|
+
inSingle = !inSingle;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (char === '"' && !inSingle) {
|
|
33
|
+
inDouble = !inDouble;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!inSingle && !inDouble && /\s/.test(char)) {
|
|
38
|
+
if (current.length > 0) {
|
|
39
|
+
tokens.push(current);
|
|
40
|
+
current = "";
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
current += char;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (escaped || inSingle || inDouble) {
|
|
49
|
+
throw new Error("Invalid editor command in config.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (current.length > 0) {
|
|
53
|
+
tokens.push(current);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseEditorCommand(editor: string): LaunchCommand {
|
|
60
|
+
const trimmed = editor.trim();
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
throw new Error("Config editor must not be empty.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tokens = tokenizeCommand(trimmed);
|
|
66
|
+
if (tokens.length === 0) {
|
|
67
|
+
throw new Error("Config editor must not be empty.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [command, ...args] = tokens;
|
|
71
|
+
return { command, args };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getSystemOpenCommand(
|
|
75
|
+
targetPath: string,
|
|
76
|
+
platform: NodeJS.Platform = process.platform,
|
|
77
|
+
): LaunchCommand {
|
|
78
|
+
if (platform === "darwin") {
|
|
79
|
+
return { command: "open", args: [targetPath] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (platform === "win32") {
|
|
83
|
+
return { command: "cmd", args: ["/c", "start", "", targetPath] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { command: "xdg-open", args: [targetPath] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getEnvEditor(env: NodeJS.ProcessEnv): string | undefined {
|
|
90
|
+
const visual = env.VISUAL?.trim();
|
|
91
|
+
if (visual) return visual;
|
|
92
|
+
|
|
93
|
+
const editor = env.EDITOR?.trim();
|
|
94
|
+
if (editor) return editor;
|
|
95
|
+
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function spawnDetached(command: string, args: string[]): Promise<void> {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn(command, args, {
|
|
102
|
+
detached: true,
|
|
103
|
+
stdio: "ignore",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
child.once("error", (error) => {
|
|
107
|
+
reject(error);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
child.once("spawn", () => {
|
|
111
|
+
child.unref();
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function openProjectInEditor(
|
|
118
|
+
projectPath: string,
|
|
119
|
+
configuredEditor?: string,
|
|
120
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const preferredEditor = configuredEditor?.trim() || getEnvEditor(env);
|
|
123
|
+
if (preferredEditor) {
|
|
124
|
+
const parsed = parseEditorCommand(preferredEditor);
|
|
125
|
+
await spawnDetached(parsed.command, [...parsed.args, projectPath]);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fallback = getSystemOpenCommand(projectPath);
|
|
130
|
+
await spawnDetached(fallback.command, fallback.args);
|
|
131
|
+
}
|
package/src/lib/git.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { promisify } from
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
5
|
|
|
6
6
|
async function runGit(cwd: string, args: string[]): Promise<string | null> {
|
|
7
7
|
try {
|
|
8
|
-
const { stdout } = await execFileAsync(
|
|
9
|
-
encoding:
|
|
8
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
|
|
9
|
+
encoding: "utf8",
|
|
10
10
|
});
|
|
11
11
|
return stdout.trim();
|
|
12
12
|
} catch {
|
|
@@ -14,18 +14,28 @@ async function runGit(cwd: string, args: string[]): Promise<string | null> {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export async function getFirstCommitDate(
|
|
18
|
-
|
|
17
|
+
export async function getFirstCommitDate(
|
|
18
|
+
cwd: string,
|
|
19
|
+
): Promise<string | undefined> {
|
|
20
|
+
const output = await runGit(cwd, [
|
|
21
|
+
"log",
|
|
22
|
+
"--max-parents=0",
|
|
23
|
+
"--format=%cd",
|
|
24
|
+
"--date=iso-strict",
|
|
25
|
+
"HEAD",
|
|
26
|
+
]);
|
|
19
27
|
return output || undefined;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
export async function getDirtyStatus(
|
|
23
|
-
|
|
30
|
+
export async function getDirtyStatus(
|
|
31
|
+
cwd: string,
|
|
32
|
+
): Promise<boolean | undefined> {
|
|
33
|
+
const output = await runGit(cwd, ["status", "--porcelain"]);
|
|
24
34
|
if (output === null) return undefined;
|
|
25
35
|
return output.length > 0;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
export async function isInsideGitRepo(cwd: string): Promise<boolean> {
|
|
29
|
-
const output = await runGit(cwd, [
|
|
30
|
-
return output ===
|
|
39
|
+
const output = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
40
|
+
return output === "true";
|
|
31
41
|
}
|
package/src/lib/help.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Help } from "commander";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
|
|
4
|
-
const GROUP_1: string[] = ["list", "search", "info", "go", "path"];
|
|
4
|
+
const GROUP_1: string[] = ["list", "search", "info", "go", "edit", "path"];
|
|
5
5
|
const GROUP_2: string[] = ["shell", "completion"];
|
|
6
6
|
const GROUP_3: string[] = ["update", "ignore", "help"];
|
|
7
7
|
|
|
@@ -55,23 +55,24 @@ export class GroupedHelp extends Help {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Arguments
|
|
58
|
-
const argumentList = helper
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
const argumentList = helper
|
|
59
|
+
.visibleArguments(cmd)
|
|
60
|
+
.map((argument) =>
|
|
61
|
+
formatItem(
|
|
62
|
+
helper.argumentTerm(argument),
|
|
63
|
+
helper.argumentDescription(argument),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
64
66
|
if (argumentList.length > 0) {
|
|
65
67
|
output.push("Arguments:", formatList(argumentList), "");
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
// Options
|
|
69
|
-
const optionList = helper
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
helper.optionDescription(option),
|
|
73
|
-
)
|
|
74
|
-
);
|
|
71
|
+
const optionList = helper
|
|
72
|
+
.visibleOptions(cmd)
|
|
73
|
+
.map((option) =>
|
|
74
|
+
formatItem(helper.optionTerm(option), helper.optionDescription(option)),
|
|
75
|
+
);
|
|
75
76
|
if (optionList.length > 0) {
|
|
76
77
|
output.push("Options:", formatList(optionList), "");
|
|
77
78
|
}
|
|
@@ -105,8 +106,7 @@ export class GroupedHelp extends Help {
|
|
|
105
106
|
const groupIndices = [...byGroup.keys()].sort((a, b) => a - b);
|
|
106
107
|
for (const idx of groupIndices) {
|
|
107
108
|
const commands = byGroup.get(idx)!;
|
|
108
|
-
const heading =
|
|
109
|
-
idx < GROUPS.length ? GROUPS[idx].heading : "Commands";
|
|
109
|
+
const heading = idx < GROUPS.length ? GROUPS[idx].heading : "Commands";
|
|
110
110
|
const commandList = commands.map((sub) =>
|
|
111
111
|
formatItem(
|
|
112
112
|
helper.subcommandTerm(sub),
|
package/src/lib/readme.ts
CHANGED
|
@@ -20,6 +20,7 @@ export async function readReadmePath(
|
|
|
20
20
|
|
|
21
21
|
export async function readReadmeContent(
|
|
22
22
|
projectPath: string,
|
|
23
|
+
options?: { full?: boolean },
|
|
23
24
|
): Promise<string | undefined> {
|
|
24
25
|
const readmePath = await readReadmePath(projectPath);
|
|
25
26
|
if (!readmePath) return undefined;
|
|
@@ -28,7 +29,11 @@ export async function readReadmeContent(
|
|
|
28
29
|
// remove html tags
|
|
29
30
|
content = content.replace(/<[^>]*>?/g, "");
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
if (options?.full) {
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// only grab the first 500 characters, and add ... if there are more
|
|
32
37
|
const truncated = content.slice(0, 500);
|
|
33
38
|
if (content.length > 500) {
|
|
34
39
|
return `${truncated}...`;
|
package/src/lib/types.ts
CHANGED
|
@@ -33,6 +33,7 @@ export type RootConfig = {
|
|
|
33
33
|
export type AppConfig = {
|
|
34
34
|
version: number;
|
|
35
35
|
roots: RootConfig[];
|
|
36
|
+
editor?: string;
|
|
36
37
|
ignores?: string[];
|
|
37
38
|
ignoredPaths?: string[];
|
|
38
39
|
slugParentFolders?: string[];
|
|
@@ -40,15 +41,18 @@ export type AppConfig = {
|
|
|
40
41
|
|
|
41
42
|
export type ProjectsConfig = {
|
|
42
43
|
projects: Record<string, Project>;
|
|
44
|
+
updatedAt?: string;
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
export type Config = {
|
|
46
48
|
version: number;
|
|
47
49
|
roots: RootConfig[];
|
|
50
|
+
editor?: string;
|
|
48
51
|
ignores?: string[];
|
|
49
52
|
ignoredPaths?: string[];
|
|
50
53
|
slugParentFolders?: string[];
|
|
51
54
|
projects: Record<string, Project>;
|
|
55
|
+
updatedAt?: string;
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
export type ProjectCandidate = {
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { GroupedHelp } from "./lib/help.js";
|
|
3
|
+
import { registerUpdate } from "./commands/update.js";
|
|
4
|
+
import { registerList } from "./commands/list.js";
|
|
5
|
+
import { registerSearch } from "./commands/search.js";
|
|
6
|
+
import { registerInfo } from "./commands/info.js";
|
|
7
|
+
import { registerGo } from "./commands/go.js";
|
|
8
|
+
import { registerPath } from "./commands/path.js";
|
|
9
|
+
import { registerShell } from "./commands/shell.js";
|
|
10
|
+
import { registerCompletion } from "./commands/completion.js";
|
|
11
|
+
import { registerIgnore } from "./commands/ignore.js";
|
|
12
|
+
import { registerEdit } from "./commands/edit.js";
|
|
13
|
+
|
|
14
|
+
const ASCII_HEADER = `
|
|
15
|
+
░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
16
|
+
░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
17
|
+
░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
18
|
+
░░░░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
19
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
20
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
21
|
+
░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
22
|
+
░░░░░░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
23
|
+
░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
24
|
+
░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
25
|
+
░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
26
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
27
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
28
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
29
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
30
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const program = new Command();
|
|
34
|
+
|
|
35
|
+
program.createHelp = function createHelp(this: Command) {
|
|
36
|
+
return Object.assign(new GroupedHelp(), this.configureHelp());
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.name("bet")
|
|
41
|
+
.description("Explore and jump between local projects.")
|
|
42
|
+
.version("0.3.0");
|
|
43
|
+
|
|
44
|
+
registerUpdate(program);
|
|
45
|
+
registerList(program);
|
|
46
|
+
registerSearch(program);
|
|
47
|
+
registerInfo(program);
|
|
48
|
+
registerGo(program);
|
|
49
|
+
registerEdit(program);
|
|
50
|
+
registerPath(program);
|
|
51
|
+
registerShell(program);
|
|
52
|
+
registerCompletion(program);
|
|
53
|
+
registerIgnore(program);
|
|
54
|
+
|
|
55
|
+
program.addHelpText("before", ASCII_HEADER);
|
|
56
|
+
|
|
57
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import { marked, type MarkedExtension } from "marked";
|
|
3
|
+
import { markedTerminal, type TerminalRendererOptions } from "marked-terminal";
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
export type Props = TerminalRendererOptions & {
|
|
7
|
+
children: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Markdown({ children, ...options }: Props) {
|
|
11
|
+
marked.use(markedTerminal(options) as MarkedExtension);
|
|
12
|
+
const parsedMarkdown = marked.parse(children, { async: false }) as string;
|
|
13
|
+
return React.createElement(Text, null, parsedMarkdown.trim());
|
|
14
|
+
}
|
package/src/ui/search.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Box, Text, useInput } from 'ink';
|
|
4
3
|
import { SelectEntry } from './select.js';
|
|
5
4
|
|
|
@@ -67,10 +66,22 @@ export function SearchSelect<T>({
|
|
|
67
66
|
|
|
68
67
|
if (items.length === 0) {
|
|
69
68
|
return (
|
|
70
|
-
<Box flexDirection="column">
|
|
71
|
-
{title
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
70
|
+
{title ? (
|
|
71
|
+
<Box marginBottom={1}>
|
|
72
|
+
<Text bold color="cyan">
|
|
73
|
+
{title}
|
|
74
|
+
</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
) : null}
|
|
77
|
+
<Box marginBottom={1} flexDirection="row">
|
|
78
|
+
<Text bold color="yellow">Search: </Text>
|
|
79
|
+
<Text color="cyan">{query || '…'}</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
<Text color="yellow">No results.</Text>
|
|
82
|
+
<Box marginTop={1}>
|
|
83
|
+
<Text color="gray">Press Esc to exit.</Text>
|
|
84
|
+
</Box>
|
|
74
85
|
</Box>
|
|
75
86
|
);
|
|
76
87
|
}
|
|
@@ -86,25 +97,45 @@ export function SearchSelect<T>({
|
|
|
86
97
|
const windowed = items.slice(windowStart, windowEnd);
|
|
87
98
|
|
|
88
99
|
return (
|
|
89
|
-
<Box flexDirection="column">
|
|
90
|
-
{title
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
101
|
+
{title ? (
|
|
102
|
+
<Box marginBottom={1}>
|
|
103
|
+
<Text bold color="cyan">
|
|
104
|
+
{title}
|
|
105
|
+
</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
) : null}
|
|
108
|
+
<Box marginBottom={1} flexDirection="row">
|
|
109
|
+
<Text bold color="yellow">Search: </Text>
|
|
110
|
+
<Text color="green">{query || '…'}</Text>
|
|
111
|
+
{showCount ? (
|
|
112
|
+
<Text color="cyan"> · {items.length} result{items.length !== 1 ? 's' : ''}</Text>
|
|
113
|
+
) : null}
|
|
114
|
+
</Box>
|
|
115
|
+
<Box flexDirection="column">
|
|
116
|
+
{windowed.map((row, idx) => {
|
|
117
|
+
const absoluteIndex = windowStart + idx;
|
|
118
|
+
const selected = absoluteIndex === selectedRowIndex;
|
|
119
|
+
return (
|
|
120
|
+
<Box key={`item-${absoluteIndex}`} flexDirection="row">
|
|
121
|
+
<Text color={selected ? 'green' : undefined} bold={selected}>
|
|
122
|
+
{selected ? '› ' : ' '}
|
|
123
|
+
{row.label}
|
|
124
|
+
</Text>
|
|
125
|
+
{row.hint ? <Text color="gray"> {row.hint}</Text> : null}
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</Box>
|
|
130
|
+
<Box marginTop={1} flexDirection="row">
|
|
131
|
+
<Text color="yellow">Type to filter</Text>
|
|
132
|
+
<Text color="gray"> · </Text>
|
|
133
|
+
<Text color="yellow">↑/↓ or j/k</Text>
|
|
134
|
+
<Text color="gray"> · </Text>
|
|
135
|
+
<Text color="green">Enter</Text>
|
|
136
|
+
<Text color="gray"> to select · </Text>
|
|
137
|
+
<Text color="red">Esc</Text>
|
|
138
|
+
<Text color="gray"> to cancel</Text>
|
|
108
139
|
</Box>
|
|
109
140
|
</Box>
|
|
110
141
|
);
|
package/src/ui/select.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Box, Text, useInput } from 'ink';
|
|
4
3
|
|
|
5
4
|
export type SelectGroup = {
|
|
@@ -65,11 +64,17 @@ export function SelectList<T>({
|
|
|
65
64
|
|
|
66
65
|
if (items.length === 0) {
|
|
67
66
|
return (
|
|
68
|
-
<Box flexDirection="column">
|
|
69
|
-
{title
|
|
70
|
-
|
|
67
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
68
|
+
{title ? (
|
|
69
|
+
<Box marginBottom={1}>
|
|
70
|
+
<Text bold color="cyan">
|
|
71
|
+
{title}
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
) : null}
|
|
75
|
+
<Text color="yellow">No results.</Text>
|
|
71
76
|
<Box marginTop={1}>
|
|
72
|
-
|
|
77
|
+
<Text color="gray">Press Esc to exit.</Text>
|
|
73
78
|
</Box>
|
|
74
79
|
</Box>
|
|
75
80
|
);
|
|
@@ -86,33 +91,49 @@ export function SelectList<T>({
|
|
|
86
91
|
const windowed = items.slice(windowStart, windowEnd);
|
|
87
92
|
|
|
88
93
|
return (
|
|
89
|
-
<Box flexDirection="column">
|
|
90
|
-
{title
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
95
|
+
{title ? (
|
|
96
|
+
<Box marginBottom={1}>
|
|
97
|
+
<Text bold color="cyan">
|
|
98
|
+
{title}
|
|
99
|
+
</Text>
|
|
100
|
+
</Box>
|
|
101
|
+
) : null}
|
|
102
|
+
<Box flexDirection="column">
|
|
103
|
+
{windowed.map((row, idx) => {
|
|
104
|
+
const absoluteIndex = windowStart + idx;
|
|
105
|
+
const selected = row.type === 'item' && absoluteIndex === selectedRowIndex;
|
|
106
|
+
|
|
107
|
+
if (row.type === 'group') {
|
|
108
|
+
return (
|
|
109
|
+
<Box key={`group-${absoluteIndex}`} marginTop={idx === 0 ? 0 : 1}>
|
|
110
|
+
<Text bold color={row.color ?? 'cyan'}>
|
|
111
|
+
[{row.label}]
|
|
112
|
+
</Text>
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
94
116
|
|
|
95
|
-
if (row.type === 'group') {
|
|
96
|
-
const colored = row.color ? chalk.hex(row.color)(`[${row.label}]`) : `[${row.label}]`;
|
|
97
117
|
return (
|
|
98
|
-
<Box key={`
|
|
99
|
-
<Text
|
|
118
|
+
<Box key={`item-${absoluteIndex}`} flexDirection="row">
|
|
119
|
+
<Text color={selected ? 'green' : undefined} bold={selected}>
|
|
120
|
+
{selected ? '› ' : ' '}
|
|
121
|
+
{row.label}
|
|
122
|
+
</Text>
|
|
123
|
+
{row.hint ? (
|
|
124
|
+
<Text color="gray"> {row.hint}</Text>
|
|
125
|
+
) : null}
|
|
100
126
|
</Box>
|
|
101
127
|
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<Box key={`item-${absoluteIndex}`}>
|
|
106
|
-
<Text>
|
|
107
|
-
{selected ? chalk.cyan.bold('› ') : ' '}
|
|
108
|
-
{selected ? chalk.cyan.bold(row.label) : row.label}
|
|
109
|
-
</Text>
|
|
110
|
-
{row.hint ? <Text>{chalk.dim(` ${row.hint}`)}</Text> : null}
|
|
111
|
-
</Box>
|
|
112
|
-
);
|
|
113
|
-
})}
|
|
128
|
+
})}
|
|
129
|
+
</Box>
|
|
114
130
|
<Box marginTop={1}>
|
|
115
|
-
<Text
|
|
131
|
+
<Text color="yellow">↑/↓ or j/k</Text>
|
|
132
|
+
<Text color="gray"> · </Text>
|
|
133
|
+
<Text color="green">Enter</Text>
|
|
134
|
+
<Text color="gray"> to select · </Text>
|
|
135
|
+
<Text color="red">Esc</Text>
|
|
136
|
+
<Text color="gray"> to cancel</Text>
|
|
116
137
|
</Box>
|
|
117
138
|
</Box>
|
|
118
139
|
);
|