Skip to content

Commit 0c8f45c

Browse files
committed
refactor: extract behavior settings panel
1 parent 45a7452 commit 0c8f45c

2 files changed

Lines changed: 228 additions & 195 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { stdin as input, stdout as output } from "node:process";
2+
import type { DashboardDisplaySettings } from "../dashboard-settings.js";
3+
import type { UI_COPY } from "../ui/copy.js";
4+
import { getUiRuntimeOptions } from "../ui/runtime.js";
5+
import { type MenuItem, select } from "../ui/select.js";
6+
7+
export type BehaviorConfigAction =
8+
| { type: "set-delay"; delayMs: number }
9+
| { type: "toggle-pause" }
10+
| { type: "toggle-menu-limit-fetch" }
11+
| { type: "toggle-menu-fetch-status" }
12+
| { type: "set-menu-quota-ttl"; ttlMs: number }
13+
| { type: "reset" }
14+
| { type: "save" }
15+
| { type: "cancel" };
16+
17+
export interface BehaviorSettingsPanelDeps {
18+
cloneDashboardSettings: (
19+
settings: DashboardDisplaySettings,
20+
) => DashboardDisplaySettings;
21+
applyDashboardDefaultsForKeys: (
22+
draft: DashboardDisplaySettings,
23+
keys: readonly (keyof DashboardDisplaySettings)[],
24+
) => DashboardDisplaySettings;
25+
formatDelayLabel: (delayMs: number) => string;
26+
formatMenuQuotaTtl: (ttlMs: number) => string;
27+
AUTO_RETURN_OPTIONS_MS: readonly number[];
28+
MENU_QUOTA_TTL_OPTIONS_MS: readonly number[];
29+
BEHAVIOR_PANEL_KEYS: readonly (keyof DashboardDisplaySettings)[];
30+
UI_COPY: typeof UI_COPY;
31+
}
32+
33+
export async function promptBehaviorSettingsPanel(
34+
initial: DashboardDisplaySettings,
35+
deps: BehaviorSettingsPanelDeps,
36+
): Promise<DashboardDisplaySettings | null> {
37+
if (!input.isTTY || !output.isTTY) return null;
38+
const ui = getUiRuntimeOptions();
39+
let draft = deps.cloneDashboardSettings(initial);
40+
let focus: BehaviorConfigAction = {
41+
type: "set-delay",
42+
delayMs: draft.actionAutoReturnMs ?? 2_000,
43+
};
44+
45+
while (true) {
46+
const currentDelay = draft.actionAutoReturnMs ?? 2_000;
47+
const pauseOnKey = draft.actionPauseOnKey ?? true;
48+
const autoFetchLimits = draft.menuAutoFetchLimits ?? true;
49+
const fetchStatusVisible = draft.menuShowFetchStatus ?? true;
50+
const menuQuotaTtlMs = draft.menuQuotaTtlMs ?? 5 * 60_000;
51+
const delayItems: MenuItem<BehaviorConfigAction>[] =
52+
deps.AUTO_RETURN_OPTIONS_MS.map((delayMs) => {
53+
const color: MenuItem<BehaviorConfigAction>["color"] =
54+
currentDelay === delayMs ? "green" : "yellow";
55+
return {
56+
label: `${currentDelay === delayMs ? "[x]" : "[ ]"} ${deps.formatDelayLabel(delayMs)}`,
57+
hint:
58+
delayMs === 1_000
59+
? "Fastest loop for frequent actions."
60+
: delayMs === 2_000
61+
? "Balanced default for most users."
62+
: "More time to read action output.",
63+
value: { type: "set-delay", delayMs },
64+
color,
65+
};
66+
});
67+
const pauseColor: MenuItem<BehaviorConfigAction>["color"] = pauseOnKey
68+
? "green"
69+
: "yellow";
70+
const items: MenuItem<BehaviorConfigAction>[] = [
71+
{
72+
label: deps.UI_COPY.settings.actionTiming,
73+
value: { type: "cancel" },
74+
kind: "heading",
75+
},
76+
...delayItems,
77+
{ label: "", value: { type: "cancel" }, separator: true },
78+
{
79+
label: `${pauseOnKey ? "[x]" : "[ ]"} Pause on key press`,
80+
hint: "Press any key to stop auto-return.",
81+
value: { type: "toggle-pause" },
82+
color: pauseColor,
83+
},
84+
{
85+
label: `${autoFetchLimits ? "[x]" : "[ ]"} Auto-fetch limits on menu open (5m cache)`,
86+
hint: "Refreshes account limits automatically when opening the menu.",
87+
value: { type: "toggle-menu-limit-fetch" },
88+
color: autoFetchLimits ? "green" : "yellow",
89+
},
90+
{
91+
label: `${fetchStatusVisible ? "[x]" : "[ ]"} Show limit refresh status`,
92+
hint: "Shows background fetch progress like [2/7] in menu subtitle.",
93+
value: { type: "toggle-menu-fetch-status" },
94+
color: fetchStatusVisible ? "green" : "yellow",
95+
},
96+
{
97+
label: `Limit cache TTL: ${deps.formatMenuQuotaTtl(menuQuotaTtlMs)}`,
98+
hint: "How fresh cached quota data must be before refresh runs.",
99+
value: { type: "set-menu-quota-ttl", ttlMs: menuQuotaTtlMs },
100+
color: "yellow",
101+
},
102+
{ label: "", value: { type: "cancel" }, separator: true },
103+
{
104+
label: deps.UI_COPY.settings.resetDefault,
105+
value: { type: "reset" },
106+
color: "yellow",
107+
},
108+
{
109+
label: deps.UI_COPY.settings.saveAndBack,
110+
value: { type: "save" },
111+
color: "green",
112+
},
113+
{
114+
label: deps.UI_COPY.settings.backNoSave,
115+
value: { type: "cancel" },
116+
color: "red",
117+
},
118+
];
119+
120+
const initialCursor = items.findIndex((item) => {
121+
const value = item.value;
122+
if (value.type !== focus.type) return false;
123+
if (value.type === "set-delay" && focus.type === "set-delay") {
124+
return value.delayMs === focus.delayMs;
125+
}
126+
return true;
127+
});
128+
129+
const result = await select<BehaviorConfigAction>(items, {
130+
message: deps.UI_COPY.settings.behaviorTitle,
131+
subtitle: deps.UI_COPY.settings.behaviorSubtitle,
132+
help: deps.UI_COPY.settings.behaviorHelp,
133+
clearScreen: true,
134+
theme: ui.theme,
135+
selectedEmphasis: "minimal",
136+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
137+
onCursorChange: ({ cursor }) => {
138+
const item = items[cursor];
139+
if (item && !item.separator && item.kind !== "heading") {
140+
focus = item.value;
141+
}
142+
},
143+
onInput: (raw) => {
144+
const lower = raw.toLowerCase();
145+
if (lower === "q") return { type: "cancel" };
146+
if (lower === "s") return { type: "save" };
147+
if (lower === "r") return { type: "reset" };
148+
if (lower === "p") return { type: "toggle-pause" };
149+
if (lower === "l") return { type: "toggle-menu-limit-fetch" };
150+
if (lower === "f") return { type: "toggle-menu-fetch-status" };
151+
if (lower === "t")
152+
return { type: "set-menu-quota-ttl", ttlMs: menuQuotaTtlMs };
153+
const parsed = Number.parseInt(raw, 10);
154+
if (
155+
Number.isFinite(parsed) &&
156+
parsed >= 1 &&
157+
parsed <= deps.AUTO_RETURN_OPTIONS_MS.length
158+
) {
159+
const delayMs = deps.AUTO_RETURN_OPTIONS_MS[parsed - 1];
160+
if (typeof delayMs === "number")
161+
return { type: "set-delay", delayMs };
162+
}
163+
return undefined;
164+
},
165+
});
166+
167+
if (!result || result.type === "cancel") return null;
168+
if (result.type === "save") return draft;
169+
if (result.type === "reset") {
170+
draft = deps.applyDashboardDefaultsForKeys(
171+
draft,
172+
deps.BEHAVIOR_PANEL_KEYS,
173+
);
174+
focus = { type: "set-delay", delayMs: draft.actionAutoReturnMs ?? 2_000 };
175+
continue;
176+
}
177+
if (result.type === "toggle-pause") {
178+
draft = { ...draft, actionPauseOnKey: !(draft.actionPauseOnKey ?? true) };
179+
focus = result;
180+
continue;
181+
}
182+
if (result.type === "toggle-menu-limit-fetch") {
183+
draft = {
184+
...draft,
185+
menuAutoFetchLimits: !(draft.menuAutoFetchLimits ?? true),
186+
};
187+
focus = result;
188+
continue;
189+
}
190+
if (result.type === "toggle-menu-fetch-status") {
191+
draft = {
192+
...draft,
193+
menuShowFetchStatus: !(draft.menuShowFetchStatus ?? true),
194+
};
195+
focus = result;
196+
continue;
197+
}
198+
if (result.type === "set-menu-quota-ttl") {
199+
const currentIndex = deps.MENU_QUOTA_TTL_OPTIONS_MS.findIndex(
200+
(value) => value === menuQuotaTtlMs,
201+
);
202+
const nextIndex =
203+
currentIndex < 0
204+
? 0
205+
: (currentIndex + 1) % deps.MENU_QUOTA_TTL_OPTIONS_MS.length;
206+
const nextTtl =
207+
deps.MENU_QUOTA_TTL_OPTIONS_MS[nextIndex] ??
208+
deps.MENU_QUOTA_TTL_OPTIONS_MS[0] ??
209+
menuQuotaTtlMs;
210+
draft = { ...draft, menuQuotaTtlMs: nextTtl };
211+
focus = { type: "set-menu-quota-ttl", ttlMs: nextTtl };
212+
continue;
213+
}
214+
draft = { ...draft, actionAutoReturnMs: result.delayMs };
215+
focus = result;
216+
}
217+
}

0 commit comments

Comments
 (0)