Skip to content

Commit 8fe9b82

Browse files
committed
refactor: extract settings hub entry wrappers
1 parent 16acf57 commit 8fe9b82

5 files changed

Lines changed: 359 additions & 152 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import type { PluginConfig } from "../types.js";
2+
import type { UiRuntimeOptions } from "../ui/runtime.js";
3+
import type { MenuItem } from "../ui/select.js";
4+
import type {
5+
BackendCategoryKey,
6+
BackendCategoryOption,
7+
BackendSettingFocusKey,
8+
BackendSettingsHubAction,
9+
} from "./backend-settings-schema.js";
10+
11+
export async function promptBackendSettingsMenu(params: {
12+
initial: PluginConfig;
13+
isInteractive: () => boolean;
14+
ui: UiRuntimeOptions;
15+
cloneBackendPluginConfig: (config: PluginConfig) => PluginConfig;
16+
backendCategoryOptions: readonly BackendCategoryOption[];
17+
getBackendCategoryInitialFocus: (
18+
category: BackendCategoryOption,
19+
) => BackendSettingFocusKey;
20+
buildBackendSettingsPreview: (
21+
config: PluginConfig,
22+
ui: UiRuntimeOptions,
23+
focus: BackendSettingFocusKey,
24+
deps: {
25+
highlightPreviewToken: (text: string, ui: UiRuntimeOptions) => string;
26+
},
27+
) => { label: string; hint: string };
28+
highlightPreviewToken: (text: string, ui: UiRuntimeOptions) => string;
29+
select: <T>(
30+
items: MenuItem<T>[],
31+
options: {
32+
message: string;
33+
subtitle: string;
34+
help: string;
35+
clearScreen: boolean;
36+
theme: UiRuntimeOptions["theme"];
37+
selectedEmphasis: "minimal";
38+
initialCursor?: number;
39+
onCursorChange: (event: { cursor: number }) => void;
40+
onInput: (raw: string) => T | undefined;
41+
},
42+
) => Promise<T | null>;
43+
getBackendCategory: (
44+
key: BackendCategoryKey,
45+
categories: readonly BackendCategoryOption[],
46+
) => BackendCategoryOption | null;
47+
promptBackendCategorySettings: (
48+
initial: PluginConfig,
49+
category: BackendCategoryOption,
50+
focus: BackendSettingFocusKey,
51+
) => Promise<{ draft: PluginConfig; focusKey: BackendSettingFocusKey }>;
52+
backendDefaults: PluginConfig;
53+
copy: {
54+
previewHeading: string;
55+
backendCategoriesHeading: string;
56+
resetDefault: string;
57+
saveAndBack: string;
58+
backNoSave: string;
59+
backendTitle: string;
60+
backendSubtitle: string;
61+
backendHelp: string;
62+
back: string;
63+
};
64+
}): Promise<PluginConfig | null> {
65+
if (!params.isInteractive()) return null;
66+
67+
let draft = params.cloneBackendPluginConfig(params.initial);
68+
let activeCategory = params.backendCategoryOptions[0]?.key ?? "session-sync";
69+
const focusByCategory: Partial<
70+
Record<BackendCategoryKey, BackendSettingFocusKey>
71+
> = {};
72+
for (const category of params.backendCategoryOptions) {
73+
focusByCategory[category.key] =
74+
params.getBackendCategoryInitialFocus(category);
75+
}
76+
77+
while (true) {
78+
const previewFocus = focusByCategory[activeCategory] ?? null;
79+
const preview = params.buildBackendSettingsPreview(
80+
draft,
81+
params.ui,
82+
previewFocus,
83+
{
84+
highlightPreviewToken: params.highlightPreviewToken,
85+
},
86+
);
87+
const categoryItems: MenuItem<BackendSettingsHubAction>[] =
88+
params.backendCategoryOptions.map((category, index) => ({
89+
label: `${index + 1}. ${category.label}`,
90+
hint: category.description,
91+
value: { type: "open-category", key: category.key },
92+
color: "green",
93+
}));
94+
95+
const items: MenuItem<BackendSettingsHubAction>[] = [
96+
{
97+
label: params.copy.previewHeading,
98+
value: { type: "cancel" },
99+
kind: "heading",
100+
},
101+
{
102+
label: preview.label,
103+
hint: preview.hint,
104+
value: { type: "cancel" },
105+
disabled: true,
106+
color: "green",
107+
hideUnavailableSuffix: true,
108+
},
109+
{ label: "", value: { type: "cancel" }, separator: true },
110+
{
111+
label: params.copy.backendCategoriesHeading,
112+
value: { type: "cancel" },
113+
kind: "heading",
114+
},
115+
...categoryItems,
116+
{ label: "", value: { type: "cancel" }, separator: true },
117+
{
118+
label: params.copy.resetDefault,
119+
value: { type: "reset" },
120+
color: "yellow",
121+
},
122+
{
123+
label: params.copy.saveAndBack,
124+
value: { type: "save" },
125+
color: "green",
126+
},
127+
{
128+
label: params.copy.backNoSave,
129+
value: { type: "cancel" },
130+
color: "red",
131+
},
132+
];
133+
134+
const initialCursor = items.findIndex((item) => {
135+
if (item.separator || item.disabled || item.kind === "heading")
136+
return false;
137+
return (
138+
item.value.type === "open-category" && item.value.key === activeCategory
139+
);
140+
});
141+
142+
const result = await params.select(items, {
143+
message: params.copy.backendTitle,
144+
subtitle: params.copy.backendSubtitle,
145+
help: params.copy.backendHelp,
146+
clearScreen: true,
147+
theme: params.ui.theme,
148+
selectedEmphasis: "minimal",
149+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
150+
onCursorChange: ({ cursor }) => {
151+
const focusedItem = items[cursor];
152+
if (focusedItem?.value.type === "open-category") {
153+
activeCategory = focusedItem.value.key;
154+
}
155+
},
156+
onInput: (raw) => {
157+
const lower = raw.toLowerCase();
158+
if (lower === "q") return { type: "cancel" as const };
159+
if (lower === "s") return { type: "save" as const };
160+
if (lower === "r") return { type: "reset" as const };
161+
const parsed = Number.parseInt(raw, 10);
162+
if (
163+
Number.isFinite(parsed) &&
164+
parsed >= 1 &&
165+
parsed <= params.backendCategoryOptions.length
166+
) {
167+
const target = params.backendCategoryOptions[parsed - 1];
168+
if (target)
169+
return { type: "open-category" as const, key: target.key };
170+
}
171+
return undefined;
172+
},
173+
});
174+
175+
if (!result || result.type === "cancel") return null;
176+
if (result.type === "save") return draft;
177+
if (result.type === "reset") {
178+
draft = params.cloneBackendPluginConfig(params.backendDefaults);
179+
for (const category of params.backendCategoryOptions) {
180+
focusByCategory[category.key] =
181+
params.getBackendCategoryInitialFocus(category);
182+
}
183+
activeCategory = params.backendCategoryOptions[0]?.key ?? activeCategory;
184+
continue;
185+
}
186+
187+
const category = params.getBackendCategory(
188+
result.key,
189+
params.backendCategoryOptions,
190+
);
191+
if (!category) continue;
192+
activeCategory = category.key;
193+
const categoryResult = await params.promptBackendCategorySettings(
194+
draft,
195+
category,
196+
focusByCategory[category.key] ??
197+
params.getBackendCategoryInitialFocus(category),
198+
);
199+
draft = categoryResult.draft;
200+
focusByCategory[category.key] = categoryResult.focusKey;
201+
}
202+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { DashboardDisplaySettings } from "../dashboard-settings.js";
2+
3+
export async function configureDashboardSettingsEntry(
4+
currentSettings: DashboardDisplaySettings | undefined,
5+
deps: {
6+
configureDashboardSettingsController: (
7+
currentSettings: DashboardDisplaySettings | undefined,
8+
deps: {
9+
loadDashboardDisplaySettings: () => Promise<DashboardDisplaySettings>;
10+
promptSettings: (
11+
settings: DashboardDisplaySettings,
12+
) => Promise<DashboardDisplaySettings | null>;
13+
settingsEqual: (
14+
left: DashboardDisplaySettings,
15+
right: DashboardDisplaySettings,
16+
) => boolean;
17+
persistSelection: (
18+
selected: DashboardDisplaySettings,
19+
) => Promise<DashboardDisplaySettings>;
20+
applyUiThemeFromDashboardSettings: (
21+
settings: DashboardDisplaySettings,
22+
) => void;
23+
isInteractive: () => boolean;
24+
getDashboardSettingsPath: () => string;
25+
writeLine: (message: string) => void;
26+
},
27+
) => Promise<DashboardDisplaySettings>;
28+
loadDashboardDisplaySettings: () => Promise<DashboardDisplaySettings>;
29+
promptSettings: (
30+
settings: DashboardDisplaySettings,
31+
) => Promise<DashboardDisplaySettings | null>;
32+
settingsEqual: (
33+
left: DashboardDisplaySettings,
34+
right: DashboardDisplaySettings,
35+
) => boolean;
36+
persistSelection: (
37+
selected: DashboardDisplaySettings,
38+
) => Promise<DashboardDisplaySettings>;
39+
applyUiThemeFromDashboardSettings: (
40+
settings: DashboardDisplaySettings,
41+
) => void;
42+
isInteractive: () => boolean;
43+
getDashboardSettingsPath: () => string;
44+
writeLine: (message: string) => void;
45+
},
46+
): Promise<DashboardDisplaySettings> {
47+
return deps.configureDashboardSettingsController(currentSettings, {
48+
loadDashboardDisplaySettings: deps.loadDashboardDisplaySettings,
49+
promptSettings: deps.promptSettings,
50+
settingsEqual: deps.settingsEqual,
51+
persistSelection: deps.persistSelection,
52+
applyUiThemeFromDashboardSettings: deps.applyUiThemeFromDashboardSettings,
53+
isInteractive: deps.isInteractive,
54+
getDashboardSettingsPath: deps.getDashboardSettingsPath,
55+
writeLine: deps.writeLine,
56+
});
57+
}

0 commit comments

Comments
 (0)