mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 10:05:14 +00:00
Add fuzzy search to settings list
This commit is contained in:
parent
0138eee6f7
commit
ec2b7b5a00
2 changed files with 67 additions and 11 deletions
|
|
@ -309,6 +309,7 @@ export class SettingsSelectorComponent extends Container {
|
|||
}
|
||||
},
|
||||
callbacks.onCancel,
|
||||
{ enableSearch: true },
|
||||
);
|
||||
|
||||
this.addChild(this.settingsList);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { fuzzyFilter } from "../fuzzy.js";
|
||||
import { getEditorKeybindings } from "../keybindings.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
||||
import { Input } from "./input.js";
|
||||
|
||||
export interface SettingItem {
|
||||
/** Unique identifier for this setting */
|
||||
|
|
@ -25,13 +27,20 @@ export interface SettingsListTheme {
|
|||
hint: (text: string) => string;
|
||||
}
|
||||
|
||||
export interface SettingsListOptions {
|
||||
enableSearch?: boolean;
|
||||
}
|
||||
|
||||
export class SettingsList implements Component {
|
||||
private items: SettingItem[];
|
||||
private filteredItems: SettingItem[];
|
||||
private theme: SettingsListTheme;
|
||||
private selectedIndex = 0;
|
||||
private maxVisible: number;
|
||||
private onChange: (id: string, newValue: string) => void;
|
||||
private onCancel: () => void;
|
||||
private searchInput?: Input;
|
||||
private searchEnabled: boolean;
|
||||
|
||||
// Submenu state
|
||||
private submenuComponent: Component | null = null;
|
||||
|
|
@ -43,12 +52,18 @@ export class SettingsList implements Component {
|
|||
theme: SettingsListTheme,
|
||||
onChange: (id: string, newValue: string) => void,
|
||||
onCancel: () => void,
|
||||
options: SettingsListOptions = {},
|
||||
) {
|
||||
this.items = items;
|
||||
this.filteredItems = items;
|
||||
this.maxVisible = maxVisible;
|
||||
this.theme = theme;
|
||||
this.onChange = onChange;
|
||||
this.onCancel = onCancel;
|
||||
this.searchEnabled = options.enableSearch ?? false;
|
||||
if (this.searchEnabled) {
|
||||
this.searchInput = new Input();
|
||||
}
|
||||
}
|
||||
|
||||
/** Update an item's currentValue */
|
||||
|
|
@ -75,24 +90,39 @@ export class SettingsList implements Component {
|
|||
private renderMainList(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (this.searchEnabled && this.searchInput) {
|
||||
lines.push(...this.searchInput.render(width));
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (this.items.length === 0) {
|
||||
lines.push(this.theme.hint(" No settings available"));
|
||||
if (this.searchEnabled) {
|
||||
this.addHintLine(lines);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
|
||||
if (displayItems.length === 0) {
|
||||
lines.push(this.theme.hint(" No matching settings"));
|
||||
this.addHintLine(lines);
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Calculate visible range with scrolling
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.items.length - this.maxVisible),
|
||||
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), displayItems.length - this.maxVisible),
|
||||
);
|
||||
const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
|
||||
const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
|
||||
|
||||
// Calculate max label width for alignment
|
||||
const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
|
||||
|
||||
// Render visible items
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const item = this.items[i];
|
||||
const item = displayItems[i];
|
||||
if (!item) continue;
|
||||
|
||||
const isSelected = i === this.selectedIndex;
|
||||
|
|
@ -114,13 +144,13 @@ export class SettingsList implements Component {
|
|||
}
|
||||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.items.length) {
|
||||
const scrollText = ` (${this.selectedIndex + 1}/${this.items.length})`;
|
||||
if (startIndex > 0 || endIndex < displayItems.length) {
|
||||
const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
|
||||
lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
|
||||
}
|
||||
|
||||
// Add description for selected item
|
||||
const selectedItem = this.items[this.selectedIndex];
|
||||
const selectedItem = displayItems[this.selectedIndex];
|
||||
if (selectedItem?.description) {
|
||||
lines.push("");
|
||||
const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4);
|
||||
|
|
@ -130,8 +160,7 @@ export class SettingsList implements Component {
|
|||
}
|
||||
|
||||
// Add hint
|
||||
lines.push("");
|
||||
lines.push(this.theme.hint(" Enter/Space to change · Esc to cancel"));
|
||||
this.addHintLine(lines);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
|
@ -146,19 +175,29 @@ export class SettingsList implements Component {
|
|||
|
||||
// Main list input handling
|
||||
const kb = getEditorKeybindings();
|
||||
const displayItems = this.searchEnabled ? this.filteredItems : this.items;
|
||||
if (kb.matches(data, "selectUp")) {
|
||||
this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
|
||||
if (displayItems.length === 0) return;
|
||||
this.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
|
||||
} else if (kb.matches(data, "selectDown")) {
|
||||
this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
if (displayItems.length === 0) return;
|
||||
this.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
} else if (kb.matches(data, "selectConfirm") || data === " ") {
|
||||
this.activateItem();
|
||||
} else if (kb.matches(data, "selectCancel")) {
|
||||
this.onCancel();
|
||||
} else if (this.searchEnabled && this.searchInput) {
|
||||
const sanitized = data.replace(/ /g, "");
|
||||
if (!sanitized) {
|
||||
return;
|
||||
}
|
||||
this.searchInput.handleInput(sanitized);
|
||||
this.applyFilter(this.searchInput.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private activateItem(): void {
|
||||
const item = this.items[this.selectedIndex];
|
||||
const item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];
|
||||
if (!item) return;
|
||||
|
||||
if (item.submenu) {
|
||||
|
|
@ -189,4 +228,20 @@ export class SettingsList implements Component {
|
|||
this.submenuItemIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
private applyFilter(query: string): void {
|
||||
this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
|
||||
private addHintLine(lines: string[]): void {
|
||||
lines.push("");
|
||||
lines.push(
|
||||
this.theme.hint(
|
||||
this.searchEnabled
|
||||
? " Type to search · Enter/Space to change · Esc to cancel"
|
||||
: " Enter/Space to change · Esc to cancel",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue