@vigent/cli 0.2.0 → 0.2.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/README.md +8 -3
- package/dist/app.js +57 -18
- package/dist/choices.js +67 -3
- package/dist/cli.js +25 -3
- package/dist/config.js +7 -4
- package/dist/diffview.js +6 -3
- package/dist/follow.js +75 -0
- package/dist/icons.js +104 -0
- package/dist/import.js +2 -1
- package/dist/picker.js +77 -40
- package/dist/tree.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A vim-style, lightweight terminal app for running several coding agents at once,
|
|
|
12
12
|
npm install -g @vigent/cli
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The package is `@vigent/cli` (npm doesn't accept the plain name `vigent`, which it finds too close to `vitest`); the command is `vigent`.
|
|
15
|
+
The package is `@vigent/cli` (npm doesn't accept the plain name `vigent`, which it finds too close to `vitest`); the command is `vigent`. Run `vigent` in a project, or `vigent <folder>` (`vigent .`, `vigent ~/code/app`) from anywhere.
|
|
16
16
|
|
|
17
17
|
Requires Node.js 20 or newer and git. Then, inside any git repository:
|
|
18
18
|
|
|
@@ -94,7 +94,7 @@ The sidebar's ENGINES block shows each one: `●` signed in, `◐` installed but
|
|
|
94
94
|
| `codex` | `codex exec --json` (`exec resume` to continue) | `-c sandbox_mode="workspace-write"` |
|
|
95
95
|
| `opencode` | `opencode run --format json --dir <worktree>` | none |
|
|
96
96
|
|
|
97
|
-
**Model and effort:** `:model` (`<Space>M`) opens a picker with the models the agent's engine offers,
|
|
97
|
+
**Model and effort:** `:model` (`<Space>M`) opens a picker with the models the agent's engine offers (Claude Code's model names, Codex's own catalog from `codex debug models`, OpenCode's `opencode models`, or pi's for the providers you have a key for). In the picker, `j`/`k` move, `g`/`G` jump to the top or bottom, Enter picks and `q` or Esc closes. Press `/` to search, or to type any model name. `:effort` (`<Space>e`) sets the reasoning effort (pi: thinking level). Both apply from the next prompt, show in the winbar and survive a restart. With no agent open, they set the defaults for new agents.
|
|
98
98
|
|
|
99
99
|

|
|
100
100
|
|
|
@@ -201,8 +201,11 @@ vigent theme import kanagawa # any colorscheme your editor can load (plug
|
|
|
201
201
|
vigent theme import ./molokai.vim # a .vim file, no editor needed
|
|
202
202
|
vigent theme new mine --from nord # start your own theme, pre-filled with nord's colors
|
|
203
203
|
vigent --theme lazyvim # LazyVim's look, on your terminal's own (transparent) background
|
|
204
|
+
vigent --theme nvim # follow your Neovim: the colorscheme your config sets
|
|
204
205
|
```
|
|
205
206
|
|
|
207
|
+
**Follow your editor:** with `theme = "nvim"` (or `"vim"`) in config.toml, vigent uses whatever colorscheme your Neovim config and plugins set, whether that's AstroNvim's `astrodark`, LazyVim's `tokyonight` or anything else. Change it in Neovim, and vigent picks it up the next time it starts. vigent keeps a copy (`themes/nvim.json`) so it opens instantly, and checks Neovim in the background.
|
|
208
|
+
|
|
206
209
|
Inside vigent, `:colorscheme` opens a picker with live preview. `:hi Group guifg=… guibg=…` changes any color on the spot, `:set transparent` lets your terminal's background (its blur, opacity or wallpaper) show through any theme, and `:theme save mine` keeps what's on screen as your own theme file. To keep settings, put them in `~/.config/vigent/config.toml` (`vigent config init` writes a starter file):
|
|
207
210
|
|
|
208
211
|
```toml
|
|
@@ -210,7 +213,7 @@ theme = "tokyonight"
|
|
|
210
213
|
|
|
211
214
|
[ui]
|
|
212
215
|
sidebar_width = 34
|
|
213
|
-
icons = "unicode" # or "ascii"
|
|
216
|
+
icons = "unicode" # or "nerd" (Nerd Font icons), "ascii"
|
|
214
217
|
transparent = true # the terminal's background, with any theme
|
|
215
218
|
|
|
216
219
|
[highlights] # applied on top of any theme
|
|
@@ -218,6 +221,8 @@ Normal = { bg = "NONE" }
|
|
|
218
221
|
CursorLine = "guibg=#2a2f3a"
|
|
219
222
|
```
|
|
220
223
|
|
|
224
|
+
**Icons and fonts:** `icons = "nerd"` (or `:settings`, Icons) uses Nerd Font icons like AstroNvim and LazyVim: a git branch icon, status and engine icons, an icon per tool call (terminal, pencil, file, search…) and file-type icons in the diff view, colored from your theme. vigent draws in your terminal's font, the same as Neovim in a terminal, so for these icons set your terminal's font to a [Nerd Font](https://www.nerdfonts.com) (for example JetBrainsMono Nerd Font).
|
|
225
|
+
|
|
221
226
|
25 built in, including `lazyvim` (LazyVim's look, transparent), the tokyonight, catppuccin and rose-pine families, `dracula`, `onedark`, `everforest`, `nightfox`, `gruvbox`, `kanagawa`, `nord`, `monokai-pro`, `ayu-mirage`, and GitHub and solarized in dark and light. The picker shows each one's colors:
|
|
222
227
|
|
|
223
228
|

|
package/dist/app.js
CHANGED
|
@@ -24,7 +24,8 @@ import { TREE_GLYPHS } from "./tree.js";
|
|
|
24
24
|
import { transcriptMarkdown } from "./export.js";
|
|
25
25
|
import { renderMarkdown } from "./markdown.js";
|
|
26
26
|
import { ChoicePicker, PICKER_ICONS, ThemePicker } from "./picker.js";
|
|
27
|
-
import { EFFORTS,
|
|
27
|
+
import { EFFORTS, effortChoicesFor, modelChoices } from "./choices.js";
|
|
28
|
+
import { ICON_SETS, NERD, toolIcon } from "./icons.js";
|
|
28
29
|
import { mergeWorktree, openPullRequest } from "./review.js";
|
|
29
30
|
import { BUILTIN_THEMES, extractPalette, listThemes, loadTheme, RESET, sgr, themeFileFor, themesDir } from "./theme.js";
|
|
30
31
|
import { parseHighlightCommand } from "./vimcolors.js";
|
|
@@ -58,6 +59,27 @@ const ICONS = {
|
|
|
58
59
|
warn: "◐",
|
|
59
60
|
off: "○",
|
|
60
61
|
},
|
|
62
|
+
nerd: {
|
|
63
|
+
spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
64
|
+
state: { waiting: NERD.hourglass, idle: NERD.circleO, paused: NERD.pause, error: NERD.times },
|
|
65
|
+
tee: "├╴",
|
|
66
|
+
last: "└╴",
|
|
67
|
+
pipe: "│ ",
|
|
68
|
+
sep: "│",
|
|
69
|
+
worktree: `${NERD.branch} `,
|
|
70
|
+
prompt: "❯ ",
|
|
71
|
+
folded: NERD.caretRight,
|
|
72
|
+
open: NERD.caretDown,
|
|
73
|
+
cursor: "▍",
|
|
74
|
+
minus: "−",
|
|
75
|
+
bullet: "•",
|
|
76
|
+
queued: NERD.clock,
|
|
77
|
+
open_agent: "▸",
|
|
78
|
+
here: NERD.circle,
|
|
79
|
+
ok: NERD.check,
|
|
80
|
+
warn: NERD.warning,
|
|
81
|
+
off: NERD.circleO,
|
|
82
|
+
},
|
|
61
83
|
ascii: {
|
|
62
84
|
spinner: ["|", "/", "-", "\\"],
|
|
63
85
|
state: { waiting: "~", idle: "o", paused: "=", error: "x" },
|
|
@@ -948,7 +970,8 @@ export class App {
|
|
|
948
970
|
/** `:effort <level>` */
|
|
949
971
|
chooseEffort(level, t = this.modelTarget()) {
|
|
950
972
|
const allowed = ["default", ...EFFORTS[t.engine]];
|
|
951
|
-
if
|
|
973
|
+
// Codex's levels differ per model and version; Codex itself says if one doesn't fit
|
|
974
|
+
if (!allowed.includes(level) && !(t.engine === "codex" && /^[a-z]+$/.test(level))) {
|
|
952
975
|
this.setMessage(`${ENGINE_NAMES[t.engine]} takes: ${allowed.join(", ")}`, "ErrorMsg");
|
|
953
976
|
return;
|
|
954
977
|
}
|
|
@@ -985,8 +1008,16 @@ export class App {
|
|
|
985
1008
|
/** `:effort`: the reasoning effort / thinking levels the engine accepts. */
|
|
986
1009
|
openEffortPicker(t = this.modelTarget()) {
|
|
987
1010
|
const current = (t.session ? t.session.effort : this.newAgentEffort[t.engine]) ?? "default";
|
|
988
|
-
this.openChoices(`Effort · ${t.label}`, effortChoices(t.engine), current, false, (v) => this.chooseEffort(v, t));
|
|
1011
|
+
this.openChoices(`Effort · ${t.label}`, this.effortChoices(t.engine, this.modelOf(t)), current, false, (v) => this.chooseEffort(v, t));
|
|
1012
|
+
}
|
|
1013
|
+
/** the model an agent (or new agents) asked for, if any */
|
|
1014
|
+
modelOf(t) {
|
|
1015
|
+
if (!t.session)
|
|
1016
|
+
return this.newAgentModel[t.engine];
|
|
1017
|
+
return "chosenModel" in t.session ? t.session.chosenModel : t.session.model;
|
|
989
1018
|
}
|
|
1019
|
+
/** where effort levels come from (replaced in tests) */
|
|
1020
|
+
effortChoices = (engine, model) => effortChoicesFor(engine, model);
|
|
990
1021
|
/** where model lists come from (replaced in tests) */
|
|
991
1022
|
modelChoices = (engine) => modelChoices(engine);
|
|
992
1023
|
openChoices(title, items, current, custom, done) {
|
|
@@ -1033,7 +1064,13 @@ export class App {
|
|
|
1033
1064
|
},
|
|
1034
1065
|
hint: "the terminal's own background shows through (its blur, opacity or wallpaper)",
|
|
1035
1066
|
},
|
|
1036
|
-
{
|
|
1067
|
+
{
|
|
1068
|
+
section: "Look",
|
|
1069
|
+
label: "Icons",
|
|
1070
|
+
value: () => ui.icons,
|
|
1071
|
+
change: () => ((ui.icons = ICON_SETS[(ICON_SETS.indexOf(ui.icons) + 1) % ICON_SETS.length]), refresh()),
|
|
1072
|
+
hint: "nerd: Nerd Font icons (needs a Nerd Font) · ascii: fonts without box-drawing characters",
|
|
1073
|
+
},
|
|
1037
1074
|
{ section: "Look", label: "24-bit color", value: () => onOff(ui.termguicolors), change: () => ((ui.termguicolors = !ui.termguicolors), refresh()), hint: "off: the nearest of the 256 standard colors (vim's 'termguicolors')" },
|
|
1038
1075
|
{ section: "Layout", label: "Sidebar", value: () => onOff(ui.sidebar), change: () => ((ui.sidebar = !ui.sidebar), refresh()) },
|
|
1039
1076
|
{
|
|
@@ -1575,7 +1612,7 @@ export class App {
|
|
|
1575
1612
|
}
|
|
1576
1613
|
const width = this.terminal.columns - 4;
|
|
1577
1614
|
const unified = unifiedView(text);
|
|
1578
|
-
const side = splitView(text, width - 3);
|
|
1615
|
+
const side = splitView(text, width - 3, this.ui.icons === "nerd");
|
|
1579
1616
|
const n = unified.jumps.length;
|
|
1580
1617
|
const title = (layout) => `${wt.branch} vs ${wt.base ?? "base"} · ${n} hunk${n === 1 ? "" : "s"} · ]c [c jump · s ${layout}`;
|
|
1581
1618
|
const views = [
|
|
@@ -2042,7 +2079,7 @@ export class App {
|
|
|
2042
2079
|
const rows = [];
|
|
2043
2080
|
const add = (text, plain, indent = center(plain)) => rows.push({ text: indent + text, plain: indent + plain });
|
|
2044
2081
|
// LazyVim's dashboard: a big logo, then one key per action
|
|
2045
|
-
const logo = this.ui.icons
|
|
2082
|
+
const logo = this.ui.icons !== "ascii" && paneW >= LOGO[0].length + 2 && paneH >= 22;
|
|
2046
2083
|
if (logo) {
|
|
2047
2084
|
const groups = ["Function", "Function", "Special", "Special", "Keyword", "Keyword"];
|
|
2048
2085
|
LOGO.forEach((l, i) => add(P.p(groups[i] ?? "Title", l), l));
|
|
@@ -2054,7 +2091,7 @@ export class App {
|
|
|
2054
2091
|
const tagline = "coding agents, the vim way · each one in its own git worktree";
|
|
2055
2092
|
add(P.p("Comment", tagline), tagline);
|
|
2056
2093
|
add("", "");
|
|
2057
|
-
const actions = DASHBOARD.map(([key, icon, label]) => [key, this.ui.icons === "unicode" ? icon : "", label]);
|
|
2094
|
+
const actions = DASHBOARD.map(([key, icon, label, nerd]) => [key, this.ui.icons === "nerd" ? nerd : this.ui.icons === "unicode" ? icon : "", label]);
|
|
2058
2095
|
const labelW = Math.max(...actions.map(([, , l]) => l.length));
|
|
2059
2096
|
const blockW = 2 + labelW + 6;
|
|
2060
2097
|
const indent = center("x".repeat(blockW));
|
|
@@ -2075,7 +2112,7 @@ export class App {
|
|
|
2075
2112
|
});
|
|
2076
2113
|
add(text, plain, indentE);
|
|
2077
2114
|
const ready = this.engines.filter((e) => e.connected).length;
|
|
2078
|
-
const foot = `${this.ui.icons === "unicode" ? "⚡ " : ""}vigent ${VERSION_TEXT} · ${listThemes().length} themes · ${ready} of ${this.engines.length} engines ready`;
|
|
2115
|
+
const foot = `${this.ui.icons === "nerd" ? `${NERD.bolt} ` : this.ui.icons === "unicode" ? "⚡ " : ""}vigent ${VERSION_TEXT} · ${listThemes().length} themes · ${ready} of ${this.engines.length} engines ready`;
|
|
2079
2116
|
add(P.p("Comment", foot), foot);
|
|
2080
2117
|
}
|
|
2081
2118
|
if (!this.workspace.root) {
|
|
@@ -2245,7 +2282,8 @@ export class App {
|
|
|
2245
2282
|
case "tool": {
|
|
2246
2283
|
const t = item.tool;
|
|
2247
2284
|
const fold = t.folded ? I.folded : I.open;
|
|
2248
|
-
|
|
2285
|
+
const icon = this.ui.icons === "nerd" ? `${toolIcon(t.name)} ` : "";
|
|
2286
|
+
push(P.p("NonText", ` ${fold} `) + P.p("Special", icon) + P.p("VigentTool", t.name) + P.p("Comment", ` ${t.summary}`), ` ${fold} ${icon}${t.name} ${t.summary}`, { item: n });
|
|
2249
2287
|
if (!t.folded && t.output) {
|
|
2250
2288
|
const block = nextBlock++;
|
|
2251
2289
|
const outLines = t.output.split("\n");
|
|
@@ -2474,14 +2512,15 @@ const LOGO = [
|
|
|
2474
2512
|
" ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ",
|
|
2475
2513
|
];
|
|
2476
2514
|
/** The dashboard's keys (active while no agent is open): key, icon, label. */
|
|
2515
|
+
/** key, unicode icon, label, Nerd Font icon */
|
|
2477
2516
|
const DASHBOARD = [
|
|
2478
|
-
["i", "✎", "New agent: type a task, Enter"],
|
|
2479
|
-
["n", "+", "New agent by name"],
|
|
2480
|
-
["t", "◐", "Theme"],
|
|
2481
|
-
["s", "⚙", "Settings"],
|
|
2482
|
-
["m", "◆", "Model"],
|
|
2483
|
-
["e", "↯", "Effort"],
|
|
2484
|
-
["c", "✓", "Check engines"],
|
|
2485
|
-
["?", "⌘", "Keys"],
|
|
2486
|
-
["q", "×", "Quit"],
|
|
2517
|
+
["i", "✎", "New agent: type a task, Enter", NERD.pencil],
|
|
2518
|
+
["n", "+", "New agent by name", NERD.plus],
|
|
2519
|
+
["t", "◐", "Theme", NERD.palette],
|
|
2520
|
+
["s", "⚙", "Settings", NERD.gear],
|
|
2521
|
+
["m", "◆", "Model", NERD.chip],
|
|
2522
|
+
["e", "↯", "Effort", NERD.bolt],
|
|
2523
|
+
["c", "✓", "Check engines", NERD.check],
|
|
2524
|
+
["?", "⌘", "Keys", NERD.keyboard],
|
|
2525
|
+
["q", "×", "Quit", NERD.power],
|
|
2487
2526
|
];
|
package/dist/choices.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What `:model` and `:effort` offer, per engine. Model lists come from the
|
|
3
|
-
* engine itself where it can say (pi-ai's catalog, `
|
|
3
|
+
* engine itself where it can say (pi-ai's catalog, `codex debug models`,
|
|
4
|
+
* `opencode models`); any
|
|
4
5
|
* name can still be typed in the picker.
|
|
5
6
|
*/
|
|
6
7
|
import { execFile } from "node:child_process";
|
|
8
|
+
import { ENGINE_BINS } from "./engines.js";
|
|
7
9
|
/** Reasoning effort / thinking levels each engine accepts. */
|
|
8
10
|
export const EFFORTS = {
|
|
9
11
|
pi: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
10
12
|
claude: ["low", "medium", "high", "xhigh", "max"],
|
|
11
|
-
|
|
13
|
+
// what Codex's models take between them; each model's own levels come from `codex debug models`
|
|
14
|
+
codex: ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"],
|
|
12
15
|
opencode: ["minimal", "low", "medium", "high", "max"],
|
|
13
16
|
};
|
|
14
17
|
export function effortChoices(engine) {
|
|
@@ -36,7 +39,10 @@ export async function modelChoices(engine, env = process.env) {
|
|
|
36
39
|
{ value: "haiku", note: "latest Haiku" },
|
|
37
40
|
];
|
|
38
41
|
case "codex":
|
|
39
|
-
return [
|
|
42
|
+
return [
|
|
43
|
+
{ value: "default", note: "Codex's own choice" },
|
|
44
|
+
...(await codexModels(env)).map((m) => ({ value: m.slug, note: m.description ? `${m.name} · ${m.description}` : m.name })),
|
|
45
|
+
];
|
|
40
46
|
case "opencode":
|
|
41
47
|
return [{ value: "default", note: "OpenCode's own choice" }, ...(await opencodeModels(env)).map((value) => ({ value }))];
|
|
42
48
|
case "pi": {
|
|
@@ -71,3 +77,61 @@ function opencodeModels(env) {
|
|
|
71
77
|
});
|
|
72
78
|
});
|
|
73
79
|
}
|
|
80
|
+
let codexCatalog;
|
|
81
|
+
/**
|
|
82
|
+
* The models your Codex offers, from `codex debug models` (its own catalog,
|
|
83
|
+
* so it follows your Codex version and account). Asked once per run; empty
|
|
84
|
+
* when Codex isn't installed or can't say.
|
|
85
|
+
*/
|
|
86
|
+
export function codexModels(env = process.env) {
|
|
87
|
+
if (env !== process.env)
|
|
88
|
+
return readCodexModels(env);
|
|
89
|
+
codexCatalog ??= readCodexModels(env).then((list) => {
|
|
90
|
+
if (!list.length)
|
|
91
|
+
codexCatalog = undefined; // try again next time
|
|
92
|
+
return list;
|
|
93
|
+
});
|
|
94
|
+
return codexCatalog;
|
|
95
|
+
}
|
|
96
|
+
function readCodexModels(env) {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
execFile(ENGINE_BINS.codex, ["debug", "models"], { env, timeout: 20000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
|
|
99
|
+
if (err)
|
|
100
|
+
return resolve([]);
|
|
101
|
+
resolve(parseCodexModels(String(stdout)));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** The models `codex debug models` lists for people to pick (not the hidden ones). */
|
|
106
|
+
export function parseCodexModels(json) {
|
|
107
|
+
try {
|
|
108
|
+
const models = JSON.parse(json)?.models ?? [];
|
|
109
|
+
return models
|
|
110
|
+
.filter((m) => m && typeof m.slug === "string" && (m.visibility === undefined || m.visibility === "list"))
|
|
111
|
+
.map((m) => ({
|
|
112
|
+
slug: m.slug,
|
|
113
|
+
name: typeof m.display_name === "string" ? m.display_name : m.slug,
|
|
114
|
+
description: typeof m.description === "string" ? m.description : undefined,
|
|
115
|
+
efforts: Array.isArray(m.supported_reasoning_levels)
|
|
116
|
+
? m.supported_reasoning_levels.map((l) => (typeof l === "string" ? l : l?.effort)).filter((e) => typeof e === "string")
|
|
117
|
+
: [],
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `:effort` for an agent: the levels its engine takes. For Codex, the levels
|
|
126
|
+
* of the model it uses (all of Codex's levels when that's its default).
|
|
127
|
+
*/
|
|
128
|
+
export async function effortChoicesFor(engine, model, env = process.env) {
|
|
129
|
+
if (engine !== "codex")
|
|
130
|
+
return effortChoices(engine);
|
|
131
|
+
const catalog = await codexModels(env);
|
|
132
|
+
const own = catalog.find((m) => m.slug === model)?.efforts;
|
|
133
|
+
const levels = own?.length ? own : [...new Set(catalog.flatMap((m) => m.efforts))];
|
|
134
|
+
if (!levels.length)
|
|
135
|
+
return effortChoices(engine);
|
|
136
|
+
return [{ value: "default", note: "what the model uses" }, ...EFFORTS.codex.filter((e) => levels.includes(e)).map((value) => ({ value })), ...levels.filter((e) => !EFFORTS.codex.includes(e)).map((value) => ({ value }))];
|
|
137
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createDemoSessions } from "./demo.js";
|
|
|
14
14
|
import { buildKeymap, KEY_MODES } from "./keys.js";
|
|
15
15
|
import { formatBytes, prepareViews, riftriBackends, riftriGc, riftriHealth, riftriStateDir } from "./riftri.js";
|
|
16
16
|
import { DemoSession, PiSession } from "./session.js";
|
|
17
|
+
import { followedColorscheme, followTheme, isFollow, refreshFollow } from "./follow.js";
|
|
17
18
|
import { BUILTIN_THEMES, listThemes, loadTheme, Painter, themesDir } from "./theme.js";
|
|
18
19
|
import { themeNewTemplate } from "./theme-new.js";
|
|
19
20
|
import { branchName, createWorktree, findWorktree, listWorktrees, removeWorktree, repoRoot, worktreesDir, } from "./worktree.js";
|
|
@@ -21,7 +22,7 @@ const HELP = `vigent — vim-native agent orchestrator (pi engine)
|
|
|
21
22
|
|
|
22
23
|
usage:
|
|
23
24
|
vigent --version which vigent this is (and the commit, from a clone)
|
|
24
|
-
vigent [--demo] [--theme <name>] [--model <id>] [--engine pi|claude|codex|opencode]
|
|
25
|
+
vigent [folder] [--demo] [--theme <name>] [--model <id>] [--engine pi|claude|codex|opencode]
|
|
25
26
|
|
|
26
27
|
engines (what runs new agents; :checkhealth shows which are signed in):
|
|
27
28
|
pi built in, uses ANTHROPIC_API_KEY / OPENAI_API_KEY / ... or pi's login
|
|
@@ -206,7 +207,9 @@ async function main() {
|
|
|
206
207
|
const keys = buildKeymap(config.leader, config.keys);
|
|
207
208
|
warnings.push(...keys.warnings);
|
|
208
209
|
try {
|
|
209
|
-
if (config.theme)
|
|
210
|
+
if (isFollow(config.theme))
|
|
211
|
+
followTheme(config.theme);
|
|
212
|
+
else if (config.theme)
|
|
210
213
|
loadTheme(config.theme);
|
|
211
214
|
}
|
|
212
215
|
catch (err) {
|
|
@@ -229,6 +232,17 @@ async function main() {
|
|
|
229
232
|
console.log(`created ${file}`);
|
|
230
233
|
return 0;
|
|
231
234
|
}
|
|
235
|
+
// `vigent <folder>` (like `code .` or `nvim .`) opens that folder; with none, the current one.
|
|
236
|
+
const VALUE_FLAGS = ["--theme", "--model", "--engine"];
|
|
237
|
+
const folder = argv.find((a, i) => !a.startsWith("-") && !VALUE_FLAGS.includes(argv[i - 1]));
|
|
238
|
+
if (folder !== undefined) {
|
|
239
|
+
const dir = path.resolve(folder);
|
|
240
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
241
|
+
console.error(`vigent: '${folder}' is not a folder (usage: vigent [folder])`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
process.chdir(dir);
|
|
245
|
+
}
|
|
232
246
|
const { config, warnings } = loadConfig();
|
|
233
247
|
const stale = staleBuild();
|
|
234
248
|
if (stale)
|
|
@@ -248,7 +262,8 @@ async function main() {
|
|
|
248
262
|
const themeName = flag(argv, "--theme") ?? config.theme ?? "default";
|
|
249
263
|
let theme = BUILTIN_THEMES.default;
|
|
250
264
|
try {
|
|
251
|
-
theme =
|
|
265
|
+
// theme = "nvim" / "vim": your editor's colorscheme (kept up to date below)
|
|
266
|
+
theme = isFollow(themeName) ? followTheme(themeName) : loadTheme(themeName);
|
|
252
267
|
}
|
|
253
268
|
catch (err) {
|
|
254
269
|
if (flag(argv, "--theme")) {
|
|
@@ -415,6 +430,13 @@ async function main() {
|
|
|
415
430
|
tui.start();
|
|
416
431
|
if (warnings.length > 0)
|
|
417
432
|
app.notify(warnings.join(" · "));
|
|
433
|
+
if (isFollow(themeName)) {
|
|
434
|
+
const editor = themeName;
|
|
435
|
+
refreshFollow(editor, (t) => {
|
|
436
|
+
app.setTheme(t);
|
|
437
|
+
app.notify(`colorscheme from ${editor}: ${followedColorscheme(editor) ?? editor}`, "Comment");
|
|
438
|
+
});
|
|
439
|
+
}
|
|
418
440
|
tui.requestRender(true);
|
|
419
441
|
return undefined;
|
|
420
442
|
}
|
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* sidebar_width = 30
|
|
9
9
|
* number = true
|
|
10
10
|
* tilde = true
|
|
11
|
-
* icons = "unicode" # or "ascii"
|
|
11
|
+
* icons = "unicode" # or "nerd" (a Nerd Font), "ascii"
|
|
12
12
|
*
|
|
13
13
|
* [highlights] # applied on top of whichever theme is active
|
|
14
14
|
* Normal = { bg = "NONE" } # transparent background
|
|
@@ -19,6 +19,7 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { parse as parseToml } from "smol-toml";
|
|
21
21
|
import { EFFORTS } from "./choices.js";
|
|
22
|
+
import { ICON_SETS } from "./icons.js";
|
|
22
23
|
import { isEngineId } from "./engines.js";
|
|
23
24
|
import { WORKTREE_BACKENDS } from "./riftri.js";
|
|
24
25
|
import { KEY_MODES } from "./keys.js";
|
|
@@ -223,8 +224,8 @@ export function setOption(ui, name, value) {
|
|
|
223
224
|
return;
|
|
224
225
|
}
|
|
225
226
|
case "icons":
|
|
226
|
-
if (value
|
|
227
|
-
return `'${name}' should be "unicode" or "ascii"`;
|
|
227
|
+
if (!ICON_SETS.includes(value))
|
|
228
|
+
return `'${name}' should be "unicode", "nerd" or "ascii"`;
|
|
228
229
|
ui.icons = value;
|
|
229
230
|
return;
|
|
230
231
|
case "transparent":
|
|
@@ -316,6 +317,8 @@ export const CONFIG_TEMPLATE = `# vigent config: ~/.config/vigent/config.toml
|
|
|
316
317
|
# Built-in: default, lazyvim (transparent), tokyonight, tokyonight-moon, gruvbox,
|
|
317
318
|
# catppuccin, catppuccin-latte, nord, rose-pine, kanagawa. Or any theme in ~/.config/vigent/themes, e.g. one made
|
|
318
319
|
# with vigent theme import (your nvim/vim colorscheme) or vigent theme new.
|
|
320
|
+
# "nvim" (or "vim") follows your editor: the colorscheme your config and plugins
|
|
321
|
+
# set (AstroNvim, LazyVim, NvChad...), updated each time vigent starts.
|
|
319
322
|
theme = "default"
|
|
320
323
|
|
|
321
324
|
# Model used for new pi agents.
|
|
@@ -347,7 +350,7 @@ sidebar = true
|
|
|
347
350
|
sidebar_width = 30
|
|
348
351
|
number = true # session numbers in the sidebar
|
|
349
352
|
tilde = true # ~ on empty sidebar rows
|
|
350
|
-
icons = "unicode" # "ascii" for fonts without box-drawing characters
|
|
353
|
+
icons = "unicode" # "nerd" for Nerd Font icons (like AstroNvim/LazyVim), "ascii" for fonts without box-drawing characters
|
|
351
354
|
timeoutlen = 1000 # ms to wait for the rest of a multi-key mapping
|
|
352
355
|
# termguicolors = false # for terminals without 24-bit color: use the nearest of 256
|
|
353
356
|
# transparent = true # use the terminal's background with any theme ("auto": the theme decides)
|
package/dist/diffview.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Both return the line indices of hunk headers so ]c / [c can jump.
|
|
9
9
|
*/
|
|
10
10
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
import { fileIcon } from "./icons.js";
|
|
11
12
|
function lineGroup(line) {
|
|
12
13
|
if (line.startsWith("+++") || line.startsWith("---"))
|
|
13
14
|
return "Title";
|
|
@@ -38,8 +39,8 @@ function cell(no, text, width, group) {
|
|
|
38
39
|
[group, body + " ".repeat(Math.max(0, width - 5 - visibleWidth(body)))],
|
|
39
40
|
];
|
|
40
41
|
}
|
|
41
|
-
/** Side-by-side layout for a window `width` columns wide (inside its border). */
|
|
42
|
-
export function splitView(text, width) {
|
|
42
|
+
/** Side-by-side layout for a window `width` columns wide (inside its border). With `icons`, file names get their Nerd Font icon. */
|
|
43
|
+
export function splitView(text, width, icons = false) {
|
|
43
44
|
const col = Math.max(12, Math.floor((width - 3) / 2));
|
|
44
45
|
const lines = [];
|
|
45
46
|
const jumps = [];
|
|
@@ -63,7 +64,9 @@ export function splitView(text, width) {
|
|
|
63
64
|
}
|
|
64
65
|
const file = /^\+\+\+ (?:b\/)?(.*)$/.exec(line);
|
|
65
66
|
if (file) {
|
|
66
|
-
|
|
67
|
+
const name = file[1] === "/dev/null" ? "(deleted file)" : file[1];
|
|
68
|
+
const [glyph, group] = fileIcon(name);
|
|
69
|
+
lines.push(icons ? [[group, `${glyph} `], ["Title", name]] : [["Title", name]]);
|
|
67
70
|
continue;
|
|
68
71
|
}
|
|
69
72
|
if (/^(---|index |new file|deleted file|similarity|rename |\\ No newline)/.test(line))
|
package/dist/follow.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `theme = "nvim"` (or `"vim"`): vigent uses the colorscheme your editor
|
|
3
|
+
* uses, with your config and plugins (AstroNvim, LazyVim, NvChad...).
|
|
4
|
+
*
|
|
5
|
+
* The editor's colors are kept as the theme file `nvim.json` (`vim.json`).
|
|
6
|
+
* At start vigent shows that copy at once, and imports again in the
|
|
7
|
+
* background: when you've changed your editor's colorscheme since, the new
|
|
8
|
+
* one replaces it while vigent runs. The first time there's no copy yet, so
|
|
9
|
+
* vigent imports before it opens.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { importTheme } from "./import.js";
|
|
15
|
+
import { loadTheme, themesDir } from "./theme.js";
|
|
16
|
+
export const FOLLOW_EDITORS = ["nvim", "vim"];
|
|
17
|
+
export function isFollow(theme) {
|
|
18
|
+
return FOLLOW_EDITORS.includes(theme ?? "");
|
|
19
|
+
}
|
|
20
|
+
/** The theme file that holds the editor's colors. */
|
|
21
|
+
export function followFile(editor, dir = themesDir()) {
|
|
22
|
+
return path.join(dir, `${editor}.json`);
|
|
23
|
+
}
|
|
24
|
+
/** The colorscheme the copy came from (e.g. "astrodark"), if it says. */
|
|
25
|
+
export function followedColorscheme(editor, dir = themesDir()) {
|
|
26
|
+
try {
|
|
27
|
+
const c = JSON.parse(fs.readFileSync(followFile(editor, dir), "utf8")).colorscheme;
|
|
28
|
+
return typeof c === "string" && c ? c : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The editor's theme: the copy from last time, or (the first time) an import now. Throws if the editor can't say. */
|
|
35
|
+
export function followTheme(editor, dir = themesDir()) {
|
|
36
|
+
const file = followFile(editor, dir);
|
|
37
|
+
if (!fs.existsSync(file))
|
|
38
|
+
importTheme({ editor, as: editor, outDir: dir });
|
|
39
|
+
return loadTheme(file);
|
|
40
|
+
}
|
|
41
|
+
/** The copy's colors in a fixed order (the editor lists its groups in any order). */
|
|
42
|
+
function read(file) {
|
|
43
|
+
try {
|
|
44
|
+
const sorted = (v) => v && typeof v === "object" && !Array.isArray(v)
|
|
45
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sorted(v[k])]))
|
|
46
|
+
: v;
|
|
47
|
+
return JSON.stringify(sorted(JSON.parse(fs.readFileSync(file, "utf8"))));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Import the editor's colorscheme again without blocking (in a child
|
|
55
|
+
* `vigent theme import`); `onChange` gets the theme when it differs from the copy.
|
|
56
|
+
*/
|
|
57
|
+
export function refreshFollow(editor, onChange, cli = process.argv[1]) {
|
|
58
|
+
const file = followFile(editor);
|
|
59
|
+
const before = read(file);
|
|
60
|
+
const child = spawn(process.execPath, [cli, "theme", "import", `--${editor}`, "--as", editor], { stdio: "ignore" });
|
|
61
|
+
child.on("error", () => { });
|
|
62
|
+
child.on("exit", (code) => {
|
|
63
|
+
if (code !== 0)
|
|
64
|
+
return;
|
|
65
|
+
const after = read(file);
|
|
66
|
+
if (after === undefined || after === before)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
onChange(loadTheme(file));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// a copy that doesn't load: keep what's on screen
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nerd Font icons (`icons = "nerd"`), the glyphs AstroNvim, LazyVim and
|
|
3
|
+
* nvim-web-devicons draw. They need a Nerd Font in the terminal
|
|
4
|
+
* (https://www.nerdfonts.com); codepoints are Nerd Fonts v3.
|
|
5
|
+
*/
|
|
6
|
+
export const ICON_SETS = ["unicode", "nerd", "ascii"];
|
|
7
|
+
export const NERD = {
|
|
8
|
+
branch: "", // nf-dev-git_branch
|
|
9
|
+
check: "", // nf-fa-check
|
|
10
|
+
times: "", // nf-fa-times
|
|
11
|
+
warning: "", // nf-fa-warning
|
|
12
|
+
circle: "", // nf-fa-circle
|
|
13
|
+
circleO: "", // nf-fa-circle_o
|
|
14
|
+
pause: "", // nf-fa-pause
|
|
15
|
+
clock: "", // nf-fa-clock_o
|
|
16
|
+
hourglass: "", // nf-fa-hourglass_half
|
|
17
|
+
search: "", // nf-fa-search
|
|
18
|
+
pencil: "", // nf-fa-pencil_square_o
|
|
19
|
+
plus: "", // nf-fa-plus
|
|
20
|
+
palette: "", // nf-fa-palette
|
|
21
|
+
gear: "", // nf-fa-gear
|
|
22
|
+
chip: "", // nf-fa-microchip
|
|
23
|
+
bolt: "", // nf-fa-bolt
|
|
24
|
+
keyboard: "", // nf-fa-keyboard_o
|
|
25
|
+
power: "", // nf-fa-power_off
|
|
26
|
+
terminal: "", // nf-oct-terminal
|
|
27
|
+
file: "", // nf-fa-file
|
|
28
|
+
fileText: "", // nf-fa-file_text
|
|
29
|
+
globe: "", // nf-fa-globe
|
|
30
|
+
wrench: "", // nf-fa-wrench
|
|
31
|
+
list: "", // nf-fa-list
|
|
32
|
+
caretRight: "", // nf-fa-caret_right
|
|
33
|
+
caretDown: "", // nf-fa-caret_down
|
|
34
|
+
};
|
|
35
|
+
/** An icon for a tool call, by the tool's name (Claude Code, Codex, OpenCode and pi names). */
|
|
36
|
+
export function toolIcon(name) {
|
|
37
|
+
const n = name.toLowerCase();
|
|
38
|
+
if (/bash|shell|exec|command|run/.test(n))
|
|
39
|
+
return NERD.terminal;
|
|
40
|
+
if (/edit|write|patch|apply/.test(n))
|
|
41
|
+
return NERD.pencil;
|
|
42
|
+
if (/read|view|cat|open/.test(n))
|
|
43
|
+
return NERD.fileText;
|
|
44
|
+
if (/grep|glob|search|find|^ls$|list/.test(n))
|
|
45
|
+
return NERD.search;
|
|
46
|
+
if (/web|fetch|http|url/.test(n))
|
|
47
|
+
return NERD.globe;
|
|
48
|
+
if (/todo|plan|task/.test(n))
|
|
49
|
+
return NERD.list;
|
|
50
|
+
return NERD.wrench;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A file's icon and the highlight group to color it with, by name or
|
|
54
|
+
* extension (like nvim-web-devicons, colored from the theme).
|
|
55
|
+
*/
|
|
56
|
+
export function fileIcon(file) {
|
|
57
|
+
const base = file.split(/[/\\]/).pop()?.toLowerCase() ?? "";
|
|
58
|
+
const byName = {
|
|
59
|
+
"package.json": ["", "DiagnosticError"], // nf-dev-npm
|
|
60
|
+
"package-lock.json": ["", "DiagnosticError"],
|
|
61
|
+
dockerfile: ["", "DiagnosticInfo"], // nf-linux-docker
|
|
62
|
+
".gitignore": ["", "DiagnosticWarn"], // nf-dev-git
|
|
63
|
+
makefile: ["", "Comment"], // nf-dev-gnu
|
|
64
|
+
license: ["", "DiagnosticWarn"], // nf-seti-license
|
|
65
|
+
};
|
|
66
|
+
if (byName[base])
|
|
67
|
+
return byName[base];
|
|
68
|
+
const ext = base.includes(".") ? base.slice(base.lastIndexOf(".") + 1) : "";
|
|
69
|
+
const byExt = {
|
|
70
|
+
ts: ["", "Function"], // nf-seti-typescript
|
|
71
|
+
tsx: ["", "Function"], // nf-dev-react
|
|
72
|
+
mts: ["", "Function"],
|
|
73
|
+
js: ["", "DiagnosticWarn"], // nf-dev-javascript
|
|
74
|
+
jsx: ["", "Function"],
|
|
75
|
+
mjs: ["", "DiagnosticWarn"],
|
|
76
|
+
cjs: ["", "DiagnosticWarn"],
|
|
77
|
+
json: ["", "DiagnosticWarn"], // nf-seti-json
|
|
78
|
+
md: ["", "Normal"], // nf-dev-markdown
|
|
79
|
+
py: ["", "DiagnosticWarn"], // nf-dev-python
|
|
80
|
+
rs: ["", "Special"], // nf-dev-rust
|
|
81
|
+
go: ["", "Function"], // nf-seti-go
|
|
82
|
+
lua: ["", "Function"], // nf-seti-lua
|
|
83
|
+
vim: ["", "String"], // nf-seti-vim
|
|
84
|
+
html: ["", "Special"], // nf-dev-html5
|
|
85
|
+
css: ["", "Function"], // nf-dev-css3
|
|
86
|
+
scss: ["", "Statement"],
|
|
87
|
+
sh: ["", "String"], // nf-oct-terminal
|
|
88
|
+
bash: ["", "String"],
|
|
89
|
+
zsh: ["", "String"],
|
|
90
|
+
toml: ["", "Comment"], // nf-seti-config
|
|
91
|
+
yml: ["", "Comment"],
|
|
92
|
+
yaml: ["", "Comment"],
|
|
93
|
+
lock: ["", "Comment"], // nf-fa-lock
|
|
94
|
+
svg: ["", "DiagnosticWarn"], // nf-fa-file_image_o
|
|
95
|
+
png: ["", "Constant"],
|
|
96
|
+
jpg: ["", "Constant"],
|
|
97
|
+
java: ["", "DiagnosticError"], // nf-dev-java
|
|
98
|
+
rb: ["", "DiagnosticError"], // nf-dev-ruby
|
|
99
|
+
c: ["", "Function"], // nf-custom-c
|
|
100
|
+
cpp: ["", "Function"], // nf-custom-cpp
|
|
101
|
+
h: ["", "Statement"],
|
|
102
|
+
};
|
|
103
|
+
return byExt[ext] ?? [NERD.file, "Comment"];
|
|
104
|
+
}
|
package/dist/import.js
CHANGED
|
@@ -255,6 +255,7 @@ export function importTheme(opts = {}) {
|
|
|
255
255
|
const dir = opts.outDir ?? themesDir();
|
|
256
256
|
fs.mkdirSync(dir, { recursive: true });
|
|
257
257
|
const file = path.join(dir, `${name}.json`);
|
|
258
|
-
|
|
258
|
+
const colorscheme = dump?.name || undefined;
|
|
259
|
+
fs.writeFileSync(file, `${JSON.stringify({ name, background, source: via, colorscheme, groups }, null, "\t")}\n`);
|
|
259
260
|
return { name, file, groups: Object.keys(groups).length, via };
|
|
260
261
|
}
|
package/dist/picker.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fuzzyFilter, parseKey, truncateToWidth, visibleWidth } from "@mariozech
|
|
|
8
8
|
import { RESET, sgr } from "./theme.js";
|
|
9
9
|
export const PICKER_ICONS = {
|
|
10
10
|
unicode: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", lt: "├", rt: "┤", current: "●", prompt: "❯" },
|
|
11
|
+
nerd: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", lt: "├", rt: "┤", current: "\uf00c", prompt: "\uf002" },
|
|
11
12
|
ascii: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", lt: "+", rt: "+", current: "*", prompt: ">" },
|
|
12
13
|
};
|
|
13
14
|
const VISIBLE = 14;
|
|
@@ -171,9 +172,10 @@ export class ThemePicker {
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
/**
|
|
174
|
-
* A
|
|
175
|
-
* Tab)
|
|
176
|
-
*
|
|
175
|
+
* A list to pick from (`:model`, `:effort`), vim-style. `j`/`k` (↓/↑, Ctrl-n/p,
|
|
176
|
+
* Tab) move, `g`/`G` jump to the top/bottom, Enter picks, `q`/Esc closes.
|
|
177
|
+
* `/` starts a search: type to filter, Esc ends it. With `custom`, Enter on a
|
|
178
|
+
* search that matches nothing picks the text itself (a model the list doesn't know).
|
|
177
179
|
*/
|
|
178
180
|
export class ChoicePicker {
|
|
179
181
|
painter;
|
|
@@ -184,6 +186,8 @@ export class ChoicePicker {
|
|
|
184
186
|
onDone;
|
|
185
187
|
requestRender;
|
|
186
188
|
query = "";
|
|
189
|
+
/** typing goes to the search (after `/`) */
|
|
190
|
+
searching = false;
|
|
187
191
|
index = 0;
|
|
188
192
|
scroll = 0;
|
|
189
193
|
items = [];
|
|
@@ -215,45 +219,70 @@ export class ChoicePicker {
|
|
|
215
219
|
handleInput(data) {
|
|
216
220
|
const key = parseKey(data) ?? "";
|
|
217
221
|
const items = this.filtered();
|
|
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
|
-
this.index = (this.index - 1 + items.length) % items.length;
|
|
243
|
-
break;
|
|
244
|
-
case "backspace":
|
|
245
|
-
this.query = this.query.slice(0, -1);
|
|
222
|
+
const move = (by) => {
|
|
223
|
+
if (items.length)
|
|
224
|
+
this.index = (this.index + by + items.length) % items.length;
|
|
225
|
+
};
|
|
226
|
+
if (key === "ctrl+c")
|
|
227
|
+
return this.onDone(null);
|
|
228
|
+
if (key === "enter" || key === "return") {
|
|
229
|
+
const picked = items[this.index]?.value ?? (this.custom && this.query.trim() ? this.query.trim() : undefined);
|
|
230
|
+
if (picked !== undefined)
|
|
231
|
+
this.onDone(picked);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (["down", "tab", "ctrl+n", "ctrl+j"].includes(key))
|
|
235
|
+
move(1);
|
|
236
|
+
else if (["up", "shift+tab", "ctrl+p", "ctrl+k"].includes(key))
|
|
237
|
+
move(-1);
|
|
238
|
+
else if (this.searching) {
|
|
239
|
+
if (key === "escape")
|
|
240
|
+
this.searching = false; // the filter stays; j/k move through it
|
|
241
|
+
else if (key === "backspace") {
|
|
242
|
+
if (this.query)
|
|
243
|
+
this.query = this.query.slice(0, -1);
|
|
244
|
+
else
|
|
245
|
+
this.searching = false;
|
|
246
246
|
this.index = 0;
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
}
|
|
248
|
+
else if (key === "ctrl+u") {
|
|
249
249
|
this.query = "";
|
|
250
250
|
this.index = 0;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
}
|
|
252
|
+
else if (data.length >= 1 && !data.startsWith("\x1b") && data >= " ") {
|
|
253
|
+
this.query += data;
|
|
254
|
+
this.index = 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
switch (data) {
|
|
259
|
+
case "j":
|
|
260
|
+
move(1);
|
|
261
|
+
break;
|
|
262
|
+
case "k":
|
|
263
|
+
move(-1);
|
|
264
|
+
break;
|
|
265
|
+
case "g":
|
|
255
266
|
this.index = 0;
|
|
256
|
-
|
|
267
|
+
break;
|
|
268
|
+
case "G":
|
|
269
|
+
this.index = Math.max(0, items.length - 1);
|
|
270
|
+
break;
|
|
271
|
+
case "/":
|
|
272
|
+
this.searching = true;
|
|
273
|
+
break;
|
|
274
|
+
case "q":
|
|
275
|
+
return this.onDone(null);
|
|
276
|
+
default:
|
|
277
|
+
if (key === "escape") {
|
|
278
|
+
if (this.query) {
|
|
279
|
+
this.query = ""; // first Esc drops the filter, the next one closes
|
|
280
|
+
this.index = 0;
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
return this.onDone(null);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
257
286
|
}
|
|
258
287
|
if (this.index < this.scroll)
|
|
259
288
|
this.scroll = this.index;
|
|
@@ -272,7 +301,14 @@ export class ChoicePicker {
|
|
|
272
301
|
const items = this.filtered();
|
|
273
302
|
const title = ` ${this.title} `;
|
|
274
303
|
const lines = [border(I.tl + I.h) + P.on("FloatTitle", bg) + title + RESET + border(I.h.repeat(Math.max(0, inner - 1 - visibleWidth(title))) + I.tr)];
|
|
275
|
-
|
|
304
|
+
if (this.searching) {
|
|
305
|
+
lines.push(row(P.on("Title", bg) + ` ${I.prompt} ` + RESET + P.on("NormalFloat", bg) + this.query + P.on("PmenuSel", undefined) + " " + RESET, visibleWidth(this.query) + 4));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const hint = this.query ? `/${this.query}` : `/ to search${this.custom ? " or type any name" : ""}`;
|
|
309
|
+
const text = truncateToWidth(hint, Math.max(0, inner - 4), "…");
|
|
310
|
+
lines.push(row(P.on("Comment", bg) + ` ${I.prompt} ${text}` + RESET, visibleWidth(text) + 3));
|
|
311
|
+
}
|
|
276
312
|
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
277
313
|
const shown = items.slice(this.scroll, this.scroll + VISIBLE);
|
|
278
314
|
for (let i = 0; i < VISIBLE; i++) {
|
|
@@ -294,7 +330,8 @@ export class ChoicePicker {
|
|
|
294
330
|
const note = c.note ? truncateToWidth(` ${c.note}`, Math.max(0, inner - 2 - visibleWidth(name)), "…") : "";
|
|
295
331
|
lines.push(row(P.on(group, rowBg) + ` ${name}` + P.on("Comment", rowBg) + note + RESET, 1 + visibleWidth(name) + visibleWidth(note), rowBg));
|
|
296
332
|
}
|
|
297
|
-
const
|
|
333
|
+
const keys = this.searching ? "type to filter · Esc done" : "j/k move · Enter pick · q close";
|
|
334
|
+
const count = ` ${items.length ? this.index + 1 : 0}/${items.length} · ${keys} `;
|
|
298
335
|
lines.push(border(I.bl + I.h.repeat(Math.max(0, inner - visibleWidth(count) - 1))) + P.on("Comment", bg) + count + RESET + border(I.h + I.br));
|
|
299
336
|
return lines;
|
|
300
337
|
}
|
package/dist/tree.js
CHANGED