@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.6
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/CHANGELOG.md +107 -0
- package/dist/cli.js +692 -607
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/usage.d.ts +9 -0
- package/dist/types/config/api-key-resolver.d.ts +9 -3
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +6 -4
- package/dist/types/config/model-registry.d.ts +7 -4
- package/dist/types/config/settings-schema.d.ts +508 -155
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/mnemopi/config.d.ts +3 -1
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-defs.d.ts +9 -2
- package/dist/types/modes/components/settings-selector.d.ts +9 -4
- package/dist/types/modes/components/tool-execution.d.ts +26 -1
- package/dist/types/modes/components/transcript-container.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +9 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +23 -3
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +28 -8
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +129 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/task/render.d.ts +17 -6
- package/dist/types/tools/gh.d.ts +3 -0
- package/dist/types/tools/render-utils.d.ts +8 -16
- package/dist/types/tools/todo.d.ts +0 -11
- package/dist/types/utils/session-color.d.ts +15 -3
- package/dist/types/web/kagi.d.ts +1 -2
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +9 -6
- package/package.json +11 -11
- package/src/auto-thinking/classifier.ts +1 -5
- package/src/cli/usage-cli.ts +187 -16
- package/src/commands/usage.ts +8 -0
- package/src/commit/model-selection.ts +3 -6
- package/src/config/api-key-resolver.ts +10 -3
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-discovery.ts +60 -46
- package/src/config/model-registry.ts +21 -8
- package/src/config/model-resolver.ts +57 -3
- package/src/config/settings-schema.ts +654 -153
- package/src/config/settings.ts +9 -0
- package/src/eval/completion-bridge.ts +1 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +13 -6
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/issue-pr-protocol.ts +10 -4
- package/src/memories/index.ts +2 -10
- package/src/mnemopi/backend.ts +30 -8
- package/src/mnemopi/config.ts +6 -1
- package/src/mnemopi/state.ts +6 -0
- package/src/modes/components/extensions/inspector-panel.ts +6 -2
- package/src/modes/components/plan-review-overlay.ts +15 -17
- package/src/modes/components/plugin-settings.ts +22 -5
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-defs.ts +19 -4
- package/src/modes/components/settings-selector.ts +510 -95
- package/src/modes/components/status-line/component.ts +3 -1
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tool-execution.ts +87 -12
- package/src/modes/components/transcript-container.ts +49 -1
- package/src/modes/components/tree-selector.ts +16 -6
- package/src/modes/controllers/command-controller.ts +61 -8
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +68 -6
- package/src/modes/controllers/selector-controller.ts +149 -61
- package/src/modes/interactive-mode.ts +63 -2
- package/src/modes/rpc/rpc-mode.ts +2 -1
- package/src/modes/session-observer-registry.ts +61 -3
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/theme.ts +102 -9
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +78 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +9 -5
- package/src/prompts/system/personalities/default.md +26 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/system-prompt.md +5 -22
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/task.md +3 -3
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +23 -1
- package/src/session/agent-session.ts +221 -29
- package/src/session/auth-storage.ts +4 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/session-dump-format.ts +8 -1
- package/src/session/session-manager.ts +5 -5
- package/src/session/snapcompact-inline.ts +524 -0
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +36 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +57 -32
- package/src/tool-discovery/tool-index.ts +2 -0
- package/src/tools/bash.ts +10 -3
- package/src/tools/eval-render.ts +13 -8
- package/src/tools/gh.ts +39 -1
- package/src/tools/image-gen.ts +114 -78
- package/src/tools/inspect-image.ts +1 -5
- package/src/tools/job.ts +25 -5
- package/src/tools/read.ts +1 -57
- package/src/tools/render-utils.ts +29 -31
- package/src/tools/ssh.ts +3 -3
- package/src/tools/todo.ts +8 -128
- package/src/tools/tts.ts +40 -20
- package/src/utils/clipboard.ts +56 -4
- package/src/utils/commit-message-generator.ts +1 -5
- package/src/utils/session-color.ts +83 -9
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +26 -27
- package/src/web/search/providers/codex.ts +42 -40
- package/src/web/search/providers/gemini.ts +42 -22
- package/src/web/search/providers/perplexity.ts +22 -10
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Effort } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import {
|
|
4
|
+
type Component,
|
|
4
5
|
Container,
|
|
6
|
+
extractPrintableText,
|
|
7
|
+
fuzzyRank,
|
|
8
|
+
getKeybindings,
|
|
9
|
+
getSettingItemFilterText,
|
|
5
10
|
Input,
|
|
6
11
|
matchesKey,
|
|
12
|
+
parseSgrMouse,
|
|
7
13
|
type SelectItem,
|
|
8
14
|
SelectList,
|
|
9
15
|
type SettingItem,
|
|
10
16
|
SettingsList,
|
|
17
|
+
type SgrMouseEvent,
|
|
11
18
|
Spacer,
|
|
12
19
|
type Tab,
|
|
13
20
|
TabBar,
|
|
14
21
|
Text,
|
|
22
|
+
truncateToWidth,
|
|
23
|
+
visibleWidth,
|
|
15
24
|
} from "@oh-my-pi/pi-tui";
|
|
16
25
|
import { getDefault, type SettingPath, settings } from "../../config/settings";
|
|
17
26
|
import type {
|
|
@@ -22,12 +31,11 @@ import type {
|
|
|
22
31
|
} from "../../config/settings-schema";
|
|
23
32
|
import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
|
|
24
33
|
import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
|
|
25
|
-
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
26
34
|
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
|
27
35
|
import { getTabBarTheme } from "../shared";
|
|
28
|
-
import {
|
|
36
|
+
import { bottomBorder, divider, row, topBorder } from "./overlay-box";
|
|
29
37
|
import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
|
|
30
|
-
import { getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
38
|
+
import { getSettingDef, getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
31
39
|
import { getPreset } from "./status-line/presets";
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -59,8 +67,6 @@ class TextInputSubmenu extends Container {
|
|
|
59
67
|
this.#input = new Input();
|
|
60
68
|
if (currentValue) {
|
|
61
69
|
this.#input.setValue(currentValue);
|
|
62
|
-
// Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
|
|
63
|
-
this.#input.handleInput("\x05");
|
|
64
70
|
}
|
|
65
71
|
this.#input.onSubmit = value => {
|
|
66
72
|
this.onSubmit(value); // empty string clears the setting
|
|
@@ -79,6 +85,8 @@ class SelectSubmenu extends Container {
|
|
|
79
85
|
#selectList: SelectList;
|
|
80
86
|
#previewText: Text | null = null;
|
|
81
87
|
#previewUpdateRequestId: number = 0;
|
|
88
|
+
#selectListLineOffset = 0;
|
|
89
|
+
#selectListLineCount = 0;
|
|
82
90
|
|
|
83
91
|
constructor(
|
|
84
92
|
title: string,
|
|
@@ -158,19 +166,73 @@ class SelectSubmenu extends Container {
|
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Concatenate children like Container.render, recording where the select
|
|
171
|
+
* list lands so routed mouse events can be hit-tested against it.
|
|
172
|
+
*/
|
|
173
|
+
override render(width: number): readonly string[] {
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
for (const child of this.children) {
|
|
176
|
+
const childLines = child.render(Math.max(1, width));
|
|
177
|
+
if (child === this.#selectList) {
|
|
178
|
+
this.#selectListLineOffset = lines.length;
|
|
179
|
+
this.#selectListLineCount = childLines.length;
|
|
180
|
+
}
|
|
181
|
+
lines.push(...childLines);
|
|
182
|
+
}
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Mouse routed from the host: wheel steps, hover lights, click confirms. */
|
|
187
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
188
|
+
if (event.wheel !== null) {
|
|
189
|
+
this.#selectList.handleWheel(event.wheel);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const listLine = line - this.#selectListLineOffset;
|
|
193
|
+
const within = listLine >= 0 && listLine < this.#selectListLineCount;
|
|
194
|
+
const index = within ? this.#selectList.hitTest(listLine) : undefined;
|
|
195
|
+
if (event.motion) {
|
|
196
|
+
this.#selectList.setHoverIndex(index ?? null);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (event.leftClick && index !== undefined) {
|
|
200
|
+
this.#selectList.clickItem(index);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
161
204
|
handleInput(data: string): void {
|
|
162
205
|
this.#selectList.handleInput(data);
|
|
163
206
|
}
|
|
164
207
|
}
|
|
165
208
|
|
|
209
|
+
let cachedSidebarWidth: number | undefined;
|
|
210
|
+
/**
|
|
211
|
+
* Split-sidebar width derived from every group name in the schema (not just
|
|
212
|
+
* the visible tab), so the divider column never moves when switching tabs or
|
|
213
|
+
* when condition-gated groups appear.
|
|
214
|
+
*/
|
|
215
|
+
function settingsSidebarWidth(): number {
|
|
216
|
+
if (cachedSidebarWidth === undefined) {
|
|
217
|
+
let nameWidth = 0;
|
|
218
|
+
for (const tab of SETTING_TABS) {
|
|
219
|
+
for (const def of getSettingsForTab(tab)) {
|
|
220
|
+
if (def.group) nameWidth = Math.max(nameWidth, visibleWidth(def.group));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
cachedSidebarWidth = Math.min(22, nameWidth) + 4;
|
|
224
|
+
}
|
|
225
|
+
return cachedSidebarWidth;
|
|
226
|
+
}
|
|
227
|
+
|
|
166
228
|
function getSettingsTabs(): Tab[] {
|
|
167
229
|
return [
|
|
168
230
|
...SETTING_TABS.map(id => {
|
|
169
231
|
const meta = TAB_METADATA[id];
|
|
170
232
|
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
171
|
-
return { id, label: `${icon} ${meta.label}
|
|
233
|
+
return { id, label: `${icon} ${meta.label}`, short: icon };
|
|
172
234
|
}),
|
|
173
|
-
{ id: "plugins", label: `${theme.icon.package} Plugins
|
|
235
|
+
{ id: "plugins", label: `${theme.icon.package} Plugins`, short: theme.icon.package },
|
|
174
236
|
];
|
|
175
237
|
}
|
|
176
238
|
|
|
@@ -218,72 +280,380 @@ export interface SettingsCallbacks {
|
|
|
218
280
|
* Main tabbed settings selector component.
|
|
219
281
|
* Uses declarative settings definitions from settings-defs.ts.
|
|
220
282
|
*/
|
|
221
|
-
export class SettingsSelectorComponent
|
|
283
|
+
export class SettingsSelectorComponent implements Component {
|
|
222
284
|
#tabBar: TabBar;
|
|
223
285
|
#currentList: SettingsList | null = null;
|
|
224
|
-
#
|
|
286
|
+
#searchList: SettingsList | null = null;
|
|
225
287
|
#pluginComponent: PluginSettingsComponent | null = null;
|
|
226
|
-
#statusPreviewContainer: Container | null = null;
|
|
227
|
-
#statusPreviewText: Text | null = null;
|
|
228
288
|
#currentTabId: SettingTab | "plugins" = "appearance";
|
|
289
|
+
#preSearchTabId: SettingTab | "plugins" = "appearance";
|
|
290
|
+
#searchQuery = "";
|
|
291
|
+
/** Single-line editor backing the search banner (cursor, word ops, paste). */
|
|
292
|
+
#searchInput = new Input();
|
|
293
|
+
#searchMatchCount = 0;
|
|
294
|
+
/** First matching item id per tab id, for Tab-key jumps while searching. */
|
|
295
|
+
#searchFirstMatch = new Map<string, string>();
|
|
229
296
|
#textInputActive = false;
|
|
297
|
+
#hasSectionJump = false;
|
|
298
|
+
// Frame geometry from the last render, for mouse hit-testing (the
|
|
299
|
+
// fullscreen overlay paints from screen row 0, so mouse rows map 1:1).
|
|
300
|
+
#tabRowStart = 0;
|
|
301
|
+
#tabRowCount = 0;
|
|
302
|
+
#contentRowStart = 0;
|
|
303
|
+
#contentRowCount = 0;
|
|
230
304
|
|
|
231
305
|
constructor(
|
|
232
306
|
private readonly context: SettingsRuntimeContext,
|
|
233
307
|
private readonly callbacks: SettingsCallbacks,
|
|
234
308
|
) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.
|
|
239
|
-
|
|
240
|
-
// Tab bar
|
|
241
|
-
this.#tabBar = new TabBar("Settings", getSettingsTabs(), getTabBarTheme());
|
|
309
|
+
// No label prefix (the frame title already says Settings) and no
|
|
310
|
+
// "(tab to cycle)" hint (folded into the footer hint line).
|
|
311
|
+
this.#tabBar = new TabBar("", getSettingsTabs(), getTabBarTheme());
|
|
312
|
+
this.#tabBar.showHint = false;
|
|
242
313
|
this.#tabBar.onTabChange = () => {
|
|
243
|
-
this.#
|
|
314
|
+
const tabId = this.#tabBar.getActiveTab().id as SettingTab | "plugins";
|
|
315
|
+
if (this.#searchList) {
|
|
316
|
+
// While searching, tabs act as jump targets into the result list.
|
|
317
|
+
const firstId = this.#searchFirstMatch.get(tabId);
|
|
318
|
+
if (firstId) this.#searchList.selectItem(firstId);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.#switchToTab(tabId);
|
|
244
322
|
};
|
|
245
|
-
this.addChild(this.#tabBar);
|
|
246
|
-
|
|
247
|
-
// Spacer after tab bar
|
|
248
|
-
this.addChild(new Spacer(1));
|
|
249
323
|
|
|
250
324
|
// Initialize with first tab
|
|
251
325
|
this.#switchToTab("appearance");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
invalidate(): void {
|
|
329
|
+
this.#tabBar.invalidate();
|
|
330
|
+
this.#currentList?.invalidate();
|
|
331
|
+
this.#searchList?.invalidate();
|
|
332
|
+
this.#pluginComponent?.invalidate();
|
|
333
|
+
}
|
|
252
334
|
|
|
253
|
-
|
|
254
|
-
|
|
335
|
+
/** Swap the active content (per-tab list, search list, or plugins). */
|
|
336
|
+
#setContent(build: () => void): void {
|
|
337
|
+
this.#currentList = null;
|
|
338
|
+
this.#searchList = null;
|
|
339
|
+
this.#pluginComponent = null;
|
|
340
|
+
build();
|
|
255
341
|
}
|
|
256
342
|
|
|
257
343
|
#switchToTab(tabId: SettingTab | "plugins"): void {
|
|
258
344
|
this.#currentTabId = tabId;
|
|
345
|
+
this.#setContent(() => {
|
|
346
|
+
if (tabId === "plugins") {
|
|
347
|
+
this.#showPluginsTab();
|
|
348
|
+
} else {
|
|
349
|
+
this.#showSettingsTab(tabId);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
259
353
|
|
|
260
|
-
|
|
261
|
-
if (this.#
|
|
262
|
-
|
|
263
|
-
this.#currentList = null;
|
|
354
|
+
#footerHintText(): string {
|
|
355
|
+
if (this.#searchList) {
|
|
356
|
+
return "Enter to change · Tab to jump tabs · Esc to exit search";
|
|
264
357
|
}
|
|
265
|
-
if (this.#
|
|
266
|
-
|
|
267
|
-
this.#pluginComponent = null;
|
|
358
|
+
if (this.#currentTabId === "plugins") {
|
|
359
|
+
return "Tab to switch tabs · Esc to close";
|
|
268
360
|
}
|
|
269
|
-
if (this.#
|
|
270
|
-
|
|
271
|
-
this.#statusPreviewContainer = null;
|
|
272
|
-
this.#statusPreviewText = null;
|
|
361
|
+
if (this.#currentList?.sectionFocused) {
|
|
362
|
+
return "↑/↓ to jump sections · Tab/Enter to settings · ←/→ to switch tabs · Esc to close";
|
|
273
363
|
}
|
|
364
|
+
const nav = this.#hasSectionJump ? "Tab to jump sections · ←/→ to switch tabs" : "Tab to switch tabs";
|
|
365
|
+
return `Enter/Space to change · ${nav} · Type to search · Esc to close`;
|
|
366
|
+
}
|
|
274
367
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
368
|
+
/** Single-line search banner: accent icon, editable query with live cursor, right-aligned match count. */
|
|
369
|
+
#renderSearchBanner(width: number): string {
|
|
370
|
+
const icon = theme.symbol("icon.search");
|
|
371
|
+
const countText = this.#searchMatchCount === 1 ? "1 match" : `${this.#searchMatchCount} matches`;
|
|
372
|
+
const rightWidth = visibleWidth(countText) + 1; // trailing margin
|
|
373
|
+
const prefix = ` ${theme.fg("accent", icon)} `;
|
|
374
|
+
// The input pads itself to exactly this width and keeps the cursor in view.
|
|
375
|
+
const inputWidth = Math.max(4, width - visibleWidth(prefix) - rightWidth - 1);
|
|
376
|
+
const inputLine = this.#searchInput.render(inputWidth)[0] ?? "";
|
|
377
|
+
const count = theme.fg(this.#searchMatchCount > 0 ? "dim" : "warning", countText);
|
|
378
|
+
return truncateToWidth(`${prefix}${theme.bold(inputLine)} ${count} `, width);
|
|
379
|
+
}
|
|
278
380
|
|
|
279
|
-
|
|
280
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Fullscreen frame: title border, tab row, divider, optional search banner,
|
|
383
|
+
* the active content sized to fill the terminal, the appearance preview,
|
|
384
|
+
* then a footer hint pinned above the bottom border.
|
|
385
|
+
*/
|
|
386
|
+
render(width: number): readonly string[] {
|
|
387
|
+
const height = Math.max(14, process.stdout.rows || 40);
|
|
388
|
+
const innerWidth = Math.max(1, width - 4);
|
|
389
|
+
|
|
390
|
+
const tabLines = this.#tabBar.render(innerWidth);
|
|
391
|
+
const searching = this.#searchList !== null;
|
|
392
|
+
const showPreview = !searching && this.#currentTabId === "appearance";
|
|
393
|
+
const previewLines = showPreview ? ["", theme.fg("muted", "Preview:"), this.#getStatusPreviewString()] : [];
|
|
394
|
+
|
|
395
|
+
// Fixed chrome: top border, tabs, divider, [search row], divider, hint, bottom border.
|
|
396
|
+
const fixedRows = 1 + tabLines.length + 1 + (searching ? 1 : 0) + 1 + 1 + 1;
|
|
397
|
+
const contentRows = Math.max(7, height - fixedRows - previewLines.length);
|
|
398
|
+
|
|
399
|
+
const list = this.#searchList ?? this.#currentList;
|
|
400
|
+
let contentLines: readonly string[];
|
|
401
|
+
if (list) {
|
|
402
|
+
// SettingsList pads itself to viewport + blank + 3 description rows.
|
|
403
|
+
list.setMaxVisible(contentRows - 4);
|
|
404
|
+
contentLines = list.render(innerWidth);
|
|
405
|
+
} else if (this.#pluginComponent) {
|
|
406
|
+
contentLines = this.#pluginComponent.render(innerWidth);
|
|
281
407
|
} else {
|
|
282
|
-
|
|
408
|
+
contentLines = [];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const out: string[] = [];
|
|
412
|
+
out.push(topBorder(width, "Settings"));
|
|
413
|
+
this.#tabRowStart = out.length;
|
|
414
|
+
this.#tabRowCount = tabLines.length;
|
|
415
|
+
for (const line of tabLines) {
|
|
416
|
+
out.push(row(line, width));
|
|
417
|
+
}
|
|
418
|
+
out.push(divider(width));
|
|
419
|
+
if (searching) {
|
|
420
|
+
out.push(row(this.#renderSearchBanner(innerWidth), width));
|
|
421
|
+
}
|
|
422
|
+
this.#contentRowStart = out.length;
|
|
423
|
+
this.#contentRowCount = contentRows;
|
|
424
|
+
for (let i = 0; i < contentRows; i++) {
|
|
425
|
+
out.push(row(contentLines[i] ?? "", width));
|
|
426
|
+
}
|
|
427
|
+
for (const line of previewLines) {
|
|
428
|
+
out.push(row(line, width));
|
|
429
|
+
}
|
|
430
|
+
out.push(divider(width));
|
|
431
|
+
out.push(row(theme.fg("dim", this.#footerHintText()), width));
|
|
432
|
+
out.push(bottomBorder(width));
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Route an SGR mouse report against the frame geometry of the last render.
|
|
438
|
+
* Wheel scrolls the focused list, motion drives the hover highlights (tabs
|
|
439
|
+
* and rows), and a left click activates: tabs switch (or jump, while
|
|
440
|
+
* searching), a row click selects, and a click on the already-selected row
|
|
441
|
+
* activates it (toggle / open submenu).
|
|
442
|
+
*/
|
|
443
|
+
#handleMouse(data: string): boolean {
|
|
444
|
+
const event = parseSgrMouse(data);
|
|
445
|
+
if (!event) return false;
|
|
446
|
+
|
|
447
|
+
const list = this.#searchList ?? this.#currentList;
|
|
448
|
+
// row() insets content by two columns (border + space).
|
|
449
|
+
const innerCol = event.col - 2;
|
|
450
|
+
const contentLine = event.row - this.#contentRowStart;
|
|
451
|
+
|
|
452
|
+
// An open submenu owns the pointer: wheel, hover, and clicks route into
|
|
453
|
+
// it (text-input submenus ignore routed events).
|
|
454
|
+
if (list?.hasOpenSubmenu()) {
|
|
455
|
+
list.routeSubmenuMouse(event, contentLine, innerCol);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (event.wheel !== null) {
|
|
460
|
+
list?.handleWheel(event.wheel);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const tabLine = event.row - this.#tabRowStart;
|
|
465
|
+
const overTabs = tabLine >= 0 && tabLine < this.#tabRowCount;
|
|
466
|
+
const overContent = contentLine >= 0 && contentLine < this.#contentRowCount;
|
|
467
|
+
|
|
468
|
+
if (event.motion) {
|
|
469
|
+
const hovered = overTabs ? this.#tabBar.tabAt(tabLine, innerCol) : undefined;
|
|
470
|
+
this.#tabBar.setHoverTab(hovered && !hovered.muted ? hovered.id : null);
|
|
471
|
+
// hoverTest: never light up pane rows while the pointer is on the
|
|
472
|
+
// sidebar — only rows the pointer is actually on.
|
|
473
|
+
list?.setHoverItem(overContent ? (list.hoverTest(contentLine, innerCol) ?? null) : null);
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
if (!event.leftClick) return true;
|
|
477
|
+
|
|
478
|
+
if (overTabs) {
|
|
479
|
+
const tab = this.#tabBar.tabAt(tabLine, innerCol);
|
|
480
|
+
if (tab) this.#tabBar.selectTab(tab.id);
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (overContent && list) {
|
|
484
|
+
const id = list.hitTest(contentLine, innerCol);
|
|
485
|
+
if (id !== undefined) {
|
|
486
|
+
const wasSelected = list.getSelectedItem()?.id === id;
|
|
487
|
+
list.selectItem(id);
|
|
488
|
+
// Click-again activates: toggle booleans, open submenus.
|
|
489
|
+
if (wasSelected) list.handleInput("\n");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
496
|
+
// Global search (type-to-search across every tab)
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
498
|
+
|
|
499
|
+
/** Swap the tab content for the global search result list. */
|
|
500
|
+
#startSearch(initialQuery: string): void {
|
|
501
|
+
this.#preSearchTabId = this.#currentTabId;
|
|
502
|
+
this.#searchInput = new Input();
|
|
503
|
+
this.#searchInput.prompt = "";
|
|
504
|
+
this.#searchInput.setValue(initialQuery);
|
|
505
|
+
const list = new SettingsList(
|
|
506
|
+
[],
|
|
507
|
+
10,
|
|
508
|
+
getSettingsListTheme(),
|
|
509
|
+
(id, newValue) => this.#onSearchSettingChange(id as SettingPath, newValue),
|
|
510
|
+
() => this.callbacks.onCancel(),
|
|
511
|
+
{
|
|
512
|
+
layout: "flat",
|
|
513
|
+
typeToSearch: false,
|
|
514
|
+
emptyText: "No matching settings",
|
|
515
|
+
hint: "",
|
|
516
|
+
},
|
|
517
|
+
);
|
|
518
|
+
// Keep the footer tab highlight on the tab owning the selected result.
|
|
519
|
+
list.onSelectionChange = item => this.#syncTabBarToSelection(item);
|
|
520
|
+
this.#setContent(() => {
|
|
521
|
+
this.#searchList = list;
|
|
522
|
+
});
|
|
523
|
+
this.#setSearchQuery(initialQuery);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Recompute matches across every settings tab. Results render as one flat
|
|
528
|
+
* list with a heading row per tab; the footer tab bar reorders to show
|
|
529
|
+
* matching tabs (with counts) first and the rest muted at the end.
|
|
530
|
+
*/
|
|
531
|
+
#setSearchQuery(query: string): void {
|
|
532
|
+
if (!this.#searchList) return;
|
|
533
|
+
if (query.length === 0) {
|
|
534
|
+
this.#endSearch(false);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
this.#searchQuery = query;
|
|
538
|
+
|
|
539
|
+
const counts = new Map<SettingTab, number>();
|
|
540
|
+
const items: SettingItem[] = [];
|
|
541
|
+
const tabResults: { tab: SettingTab; matched: SettingItem[]; bestScore: number; order: number }[] = [];
|
|
542
|
+
this.#searchFirstMatch.clear();
|
|
543
|
+
let total = 0;
|
|
544
|
+
for (const tab of SETTING_TABS) {
|
|
545
|
+
const candidates: SettingItem[] = [];
|
|
546
|
+
for (const def of getSettingsForTab(tab)) {
|
|
547
|
+
const item = this.#defToItem(def);
|
|
548
|
+
if (item) candidates.push(item);
|
|
549
|
+
}
|
|
550
|
+
const ranked = fuzzyRank(candidates, query, getSettingItemFilterText);
|
|
551
|
+
const matched = ranked.map(result => result.item);
|
|
552
|
+
counts.set(tab, matched.length);
|
|
553
|
+
if (matched.length === 0) continue;
|
|
554
|
+
total += matched.length;
|
|
555
|
+
tabResults.push({
|
|
556
|
+
tab,
|
|
557
|
+
matched,
|
|
558
|
+
bestScore: ranked[0]?.score ?? 0,
|
|
559
|
+
order: SETTING_TABS.indexOf(tab),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
tabResults.sort((a, b) => a.bestScore - b.bestScore || a.order - b.order);
|
|
564
|
+
for (const result of tabResults) {
|
|
565
|
+
const meta = TAB_METADATA[result.tab];
|
|
566
|
+
items.push({
|
|
567
|
+
id: `__tab:${result.tab}`,
|
|
568
|
+
label: `${theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0])} ${meta.label}`,
|
|
569
|
+
currentValue: "",
|
|
570
|
+
heading: true,
|
|
571
|
+
});
|
|
572
|
+
this.#searchFirstMatch.set(result.tab, result.matched[0]?.id ?? "");
|
|
573
|
+
items.push(...result.matched);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
this.#searchList.setItems(items);
|
|
577
|
+
this.#searchMatchCount = total;
|
|
578
|
+
this.#tabBar.setTabs(
|
|
579
|
+
this.#buildSearchTabs(
|
|
580
|
+
counts,
|
|
581
|
+
tabResults.map(result => result.tab),
|
|
582
|
+
),
|
|
583
|
+
);
|
|
584
|
+
this.#syncTabBarToSelection(this.#searchList.getSelectedItem());
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Leave search mode. With `jumpToSelection`, land on the tab containing
|
|
589
|
+
* the selected result and keep it selected there — search doubles as
|
|
590
|
+
* navigation. Otherwise restore the pre-search tab.
|
|
591
|
+
*/
|
|
592
|
+
#endSearch(jumpToSelection: boolean): void {
|
|
593
|
+
if (!this.#searchList) return;
|
|
594
|
+
const selected = jumpToSelection ? this.#searchList.getSelectedItem() : undefined;
|
|
595
|
+
const selectedDef = selected ? getSettingDef(selected.id as SettingPath) : undefined;
|
|
596
|
+
const targetTab: SettingTab | "plugins" = selectedDef?.tab ?? this.#preSearchTabId;
|
|
597
|
+
|
|
598
|
+
this.#searchQuery = "";
|
|
599
|
+
this.#searchFirstMatch.clear();
|
|
600
|
+
this.#searchMatchCount = 0;
|
|
601
|
+
this.#tabBar.setTabs(getSettingsTabs(), targetTab);
|
|
602
|
+
this.#switchToTab(targetTab);
|
|
603
|
+
if (selectedDef) {
|
|
604
|
+
this.#currentList?.selectItem(selectedDef.path);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Matching tabs first (counts attached), ordered by best result score; the rest stay muted at the end. */
|
|
609
|
+
#buildSearchTabs(counts: Map<SettingTab, number>, matchedTabOrder: readonly SettingTab[]): Tab[] {
|
|
610
|
+
const matched: Tab[] = [];
|
|
611
|
+
const empty: Tab[] = [];
|
|
612
|
+
const matchedIds = new Set<SettingTab>(matchedTabOrder);
|
|
613
|
+
for (const id of matchedTabOrder) {
|
|
614
|
+
const meta = TAB_METADATA[id];
|
|
615
|
+
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
616
|
+
const count = counts.get(id) ?? 0;
|
|
617
|
+
if (count > 0) {
|
|
618
|
+
matched.push({ id, label: `${icon} ${meta.label} (${count})`, short: `${icon} ${count}` });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
for (const id of SETTING_TABS) {
|
|
622
|
+
if (matchedIds.has(id)) continue;
|
|
623
|
+
const meta = TAB_METADATA[id];
|
|
624
|
+
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
625
|
+
empty.push({ id, label: `${icon} ${meta.label}`, short: icon, muted: true });
|
|
283
626
|
}
|
|
627
|
+
// Plugins hosts its own UI; it is not part of the schema-backed search.
|
|
628
|
+
empty.push({ id: "plugins", label: `${theme.icon.package} Plugins`, short: theme.icon.package, muted: true });
|
|
629
|
+
return [...matched, ...empty];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
#syncTabBarToSelection(item: SettingItem | undefined): void {
|
|
633
|
+
if (!this.#searchList || !item) return;
|
|
634
|
+
const def = getSettingDef(item.id as SettingPath);
|
|
635
|
+
if (def) this.#tabBar.setActiveById(def.tab);
|
|
636
|
+
}
|
|
284
637
|
|
|
285
|
-
|
|
286
|
-
|
|
638
|
+
/** Value-change dispatch for the search result list (any tab's setting). */
|
|
639
|
+
#onSearchSettingChange(path: SettingPath, newValue: string): void {
|
|
640
|
+
const def = getSettingDef(path);
|
|
641
|
+
if (!def) return;
|
|
642
|
+
if (def.type === "boolean") {
|
|
643
|
+
const boolValue = newValue === "true";
|
|
644
|
+
settings.set(path, boolValue as never);
|
|
645
|
+
this.callbacks.onChange(path, boolValue);
|
|
646
|
+
} else if (def.type === "enum") {
|
|
647
|
+
settings.set(path, newValue as never);
|
|
648
|
+
this.callbacks.onChange(path, newValue);
|
|
649
|
+
}
|
|
650
|
+
// Submenu/text types already persisted inside their own done callbacks.
|
|
651
|
+
if (def.tab === "appearance") {
|
|
652
|
+
this.#triggerStatusLinePreview();
|
|
653
|
+
}
|
|
654
|
+
// Values feed the searchable text and condition gates may have flipped:
|
|
655
|
+
// recompute results in place (selection is preserved by item id).
|
|
656
|
+
this.#setSearchQuery(this.#searchQuery);
|
|
287
657
|
}
|
|
288
658
|
|
|
289
659
|
/**
|
|
@@ -408,7 +778,6 @@ export class SettingsSelectorComponent extends Container {
|
|
|
408
778
|
rightSegments: presetDef.rightSegments,
|
|
409
779
|
separator: presetDef.separator,
|
|
410
780
|
});
|
|
411
|
-
this.#updateStatusPreview();
|
|
412
781
|
};
|
|
413
782
|
onPreviewCancel = () => {
|
|
414
783
|
const currentPreset = settings.get("statusLine.preset");
|
|
@@ -419,17 +788,14 @@ export class SettingsSelectorComponent extends Container {
|
|
|
419
788
|
rightSegments: presetDef.rightSegments,
|
|
420
789
|
separator: presetDef.separator,
|
|
421
790
|
});
|
|
422
|
-
this.#updateStatusPreview();
|
|
423
791
|
};
|
|
424
792
|
} else if (def.path === "statusLine.separator") {
|
|
425
793
|
onPreview = value => {
|
|
426
794
|
this.callbacks.onStatusLinePreview?.({ separator: value as StatusLineSeparatorStyle });
|
|
427
|
-
this.#updateStatusPreview();
|
|
428
795
|
};
|
|
429
796
|
onPreviewCancel = () => {
|
|
430
797
|
const separator = settings.get("statusLine.separator");
|
|
431
798
|
this.callbacks.onStatusLinePreview?.({ separator });
|
|
432
|
-
this.#updateStatusPreview();
|
|
433
799
|
};
|
|
434
800
|
}
|
|
435
801
|
|
|
@@ -509,19 +875,15 @@ export class SettingsSelectorComponent extends Container {
|
|
|
509
875
|
#showSettingsTab(tabId: SettingTab): void {
|
|
510
876
|
const defs = getSettingsForTab(tabId);
|
|
511
877
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
this.#statusPreviewContainer.addChild(this.#statusPreviewText);
|
|
519
|
-
this.#statusPreviewContainer.addChild(new Spacer(1));
|
|
520
|
-
this.addChild(this.#statusPreviewContainer);
|
|
521
|
-
}
|
|
878
|
+
const items = this.#buildItemsForDefs(defs);
|
|
879
|
+
// Mirror SettingsList's section detection (leading ungrouped items form
|
|
880
|
+
// an implicit section) so the footer hint only advertises PgUp/PgDn
|
|
881
|
+
// when the jump actually changes sections.
|
|
882
|
+
const sectionCount = items.filter(item => item.heading).length + (items.length > 0 && !items[0].heading ? 1 : 0);
|
|
883
|
+
this.#hasSectionJump = sectionCount >= 2;
|
|
522
884
|
|
|
523
885
|
this.#currentList = new SettingsList(
|
|
524
|
-
|
|
886
|
+
items,
|
|
525
887
|
10,
|
|
526
888
|
getSettingsListTheme(),
|
|
527
889
|
(id, newValue) => {
|
|
@@ -550,17 +912,28 @@ export class SettingsSelectorComponent extends Container {
|
|
|
550
912
|
this.#refreshCurrentTabItems(defs);
|
|
551
913
|
},
|
|
552
914
|
() => this.callbacks.onCancel(),
|
|
915
|
+
// The selector owns type-to-search and the footer hint; pin the
|
|
916
|
+
// split sidebar width so the divider never jumps between tabs.
|
|
917
|
+
{ typeToSearch: false, hint: "", sidebarWidth: settingsSidebarWidth() },
|
|
553
918
|
);
|
|
554
|
-
|
|
555
|
-
this.addChild(this.#currentList);
|
|
556
919
|
}
|
|
557
920
|
|
|
558
|
-
/**
|
|
921
|
+
/**
|
|
922
|
+
* Map a definition list to UI items, dropping any whose condition is false.
|
|
923
|
+
* Inserts a heading row whenever the (group-sorted) definition list crosses
|
|
924
|
+
* into a new group; groups whose items are all condition-hidden emit none.
|
|
925
|
+
*/
|
|
559
926
|
#buildItemsForDefs(defs: SettingDef[]): SettingItem[] {
|
|
560
927
|
const items: SettingItem[] = [];
|
|
928
|
+
let lastGroup: string | undefined;
|
|
561
929
|
for (const def of defs) {
|
|
562
930
|
const item = this.#defToItem(def);
|
|
563
|
-
if (item)
|
|
931
|
+
if (!item) continue;
|
|
932
|
+
if (def.group && def.group !== lastGroup) {
|
|
933
|
+
items.push({ id: `__heading:${def.group}`, label: def.group, currentValue: "", heading: true });
|
|
934
|
+
lastGroup = def.group;
|
|
935
|
+
}
|
|
936
|
+
items.push(item);
|
|
564
937
|
}
|
|
565
938
|
return items;
|
|
566
939
|
}
|
|
@@ -594,16 +967,6 @@ export class SettingsSelectorComponent extends Container {
|
|
|
594
967
|
transparent: settings.get("statusLine.transparent"),
|
|
595
968
|
};
|
|
596
969
|
this.callbacks.onStatusLinePreview?.(statusLineSettings);
|
|
597
|
-
this.#updateStatusPreview();
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Update the inline status preview text.
|
|
602
|
-
*/
|
|
603
|
-
#updateStatusPreview(): void {
|
|
604
|
-
if (this.#statusPreviewText && this.#currentTabId === "appearance") {
|
|
605
|
-
this.#statusPreviewText.setText(this.#getStatusPreviewString());
|
|
606
|
-
}
|
|
607
970
|
}
|
|
608
971
|
|
|
609
972
|
#showPluginsTab(): void {
|
|
@@ -611,43 +974,95 @@ export class SettingsSelectorComponent extends Container {
|
|
|
611
974
|
onClose: () => this.callbacks.onCancel(),
|
|
612
975
|
onPluginChanged: () => this.callbacks.onPluginsChanged?.(),
|
|
613
976
|
});
|
|
614
|
-
this.addChild(this.#pluginComponent);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
getFocusComponent(): SettingsList | PluginSettingsComponent {
|
|
618
|
-
// Return the current focusable component - one of these will always be set
|
|
619
|
-
return (this.#currentList || this.#pluginComponent)!;
|
|
620
977
|
}
|
|
621
978
|
|
|
622
979
|
handleInput(data: string): void {
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
) {
|
|
632
|
-
this.#
|
|
980
|
+
// SGR mouse reports (the fullscreen overlay enables tracking).
|
|
981
|
+
if (data.startsWith("\x1b[<")) {
|
|
982
|
+
this.#handleMouse(data);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Text-input submenus take every byte: arrow keys must reach the
|
|
987
|
+
// cursor and Tab must not switch tabs.
|
|
988
|
+
if (this.#textInputActive) {
|
|
989
|
+
(this.#searchList ?? this.#currentList)?.handleInput(data);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const activeList = this.#searchList ?? this.#currentList;
|
|
994
|
+
|
|
995
|
+
// An open submenu owns input entirely — Tab/arrows/typing belong to it.
|
|
996
|
+
if (activeList?.hasOpenSubmenu()) {
|
|
997
|
+
activeList.handleInput(data);
|
|
633
998
|
return;
|
|
634
999
|
}
|
|
635
1000
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1001
|
+
if (this.#searchList) {
|
|
1002
|
+
this.#handleSearchModeInput(data, this.#searchList);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Tab toggles keyboard focus between section headings and setting rows
|
|
1007
|
+
// (fast section hopping); tabs without sections keep Tab switching tabs.
|
|
1008
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
1009
|
+
if (this.#currentList?.hasSectionFocusTargets()) {
|
|
1010
|
+
this.#currentList.toggleSectionFocus();
|
|
640
1011
|
return;
|
|
641
1012
|
}
|
|
642
|
-
this.
|
|
1013
|
+
this.#tabBar.handleInput(data);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (matchesKey(data, "left") || matchesKey(data, "right")) {
|
|
1017
|
+
this.#tabBar.handleInput(data);
|
|
643
1018
|
return;
|
|
644
1019
|
}
|
|
645
1020
|
|
|
646
|
-
//
|
|
1021
|
+
// Printable characters start a search across every settings tab. The
|
|
1022
|
+
// plugins tab keeps its own local filtering instead.
|
|
1023
|
+
if (this.#currentTabId !== "plugins") {
|
|
1024
|
+
const printable = extractPrintableText(data);
|
|
1025
|
+
if (printable !== undefined && printable.trim().length > 0) {
|
|
1026
|
+
this.#startSearch(printable);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
647
1031
|
if (this.#currentList) {
|
|
648
1032
|
this.#currentList.handleInput(data);
|
|
649
1033
|
} else if (this.#pluginComponent) {
|
|
650
1034
|
this.#pluginComponent.handleInput(data);
|
|
651
1035
|
}
|
|
652
1036
|
}
|
|
1037
|
+
|
|
1038
|
+
#handleSearchModeInput(data: string, list: SettingsList): void {
|
|
1039
|
+
const kb = getKeybindings();
|
|
1040
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
1041
|
+
// Exit search, landing on the tab of the selected result.
|
|
1042
|
+
this.#endSearch(true);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
1046
|
+
// Jump between tabs that have matches (muted tabs are skipped).
|
|
1047
|
+
this.#tabBar.handleInput(data);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
// Selection, paging, and activation stay with the result list.
|
|
1051
|
+
if (
|
|
1052
|
+
kb.matches(data, "tui.select.up") ||
|
|
1053
|
+
kb.matches(data, "tui.select.down") ||
|
|
1054
|
+
kb.matches(data, "tui.select.pageUp") ||
|
|
1055
|
+
kb.matches(data, "tui.select.pageDown") ||
|
|
1056
|
+
kb.matches(data, "tui.select.confirm") ||
|
|
1057
|
+
data === "\n"
|
|
1058
|
+
) {
|
|
1059
|
+
list.handleInput(data);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
// Everything else edits the query like a regular single-line editor:
|
|
1063
|
+
// cursor movement, word ops, kill ring, undo, paste.
|
|
1064
|
+
this.#searchInput.handleInput(data);
|
|
1065
|
+
const value = this.#searchInput.getValue();
|
|
1066
|
+
if (value !== this.#searchQuery) this.#setSearchQuery(value);
|
|
1067
|
+
}
|
|
653
1068
|
}
|