|
| 1 | +import { stdin as input, stdout as output } from "node:process"; |
| 2 | +import { |
| 3 | + type DashboardAccountSortMode, |
| 4 | + type DashboardDisplaySettings, |
| 5 | + DEFAULT_DASHBOARD_DISPLAY_SETTINGS, |
| 6 | +} from "../dashboard-settings.js"; |
| 7 | +import type { UI_COPY } from "../ui/copy.js"; |
| 8 | +import { getUiRuntimeOptions } from "../ui/runtime.js"; |
| 9 | +import { type MenuItem, select } from "../ui/select.js"; |
| 10 | + |
| 11 | +export type DashboardDisplaySettingKey = |
| 12 | + | "menuShowStatusBadge" |
| 13 | + | "menuShowCurrentBadge" |
| 14 | + | "menuShowLastUsed" |
| 15 | + | "menuShowQuotaSummary" |
| 16 | + | "menuShowQuotaCooldown" |
| 17 | + | "menuShowDetailsForUnselectedRows" |
| 18 | + | "menuShowFetchStatus" |
| 19 | + | "menuHighlightCurrentRow" |
| 20 | + | "menuSortEnabled" |
| 21 | + | "menuSortPinCurrent" |
| 22 | + | "menuSortQuickSwitchVisibleRow"; |
| 23 | + |
| 24 | +export interface DashboardDisplaySettingOption { |
| 25 | + key: DashboardDisplaySettingKey; |
| 26 | + label: string; |
| 27 | + description: string; |
| 28 | +} |
| 29 | + |
| 30 | +export type DashboardConfigAction = |
| 31 | + | { type: "toggle"; key: DashboardDisplaySettingKey } |
| 32 | + | { type: "cycle-sort-mode" } |
| 33 | + | { type: "cycle-layout-mode" } |
| 34 | + | { type: "reset" } |
| 35 | + | { type: "save" } |
| 36 | + | { type: "cancel" }; |
| 37 | + |
| 38 | +export interface DashboardDisplayPanelDeps { |
| 39 | + cloneDashboardSettings: ( |
| 40 | + settings: DashboardDisplaySettings, |
| 41 | + ) => DashboardDisplaySettings; |
| 42 | + buildAccountListPreview: ( |
| 43 | + settings: DashboardDisplaySettings, |
| 44 | + ui: ReturnType<typeof getUiRuntimeOptions>, |
| 45 | + focusKey: DashboardDisplaySettingKey | "menuSortMode" | "menuLayoutMode", |
| 46 | + ) => { label: string; hint?: string }; |
| 47 | + formatDashboardSettingState: (enabled: boolean) => string; |
| 48 | + formatMenuSortMode: (mode: DashboardAccountSortMode) => string; |
| 49 | + resolveMenuLayoutMode: ( |
| 50 | + settings?: DashboardDisplaySettings, |
| 51 | + ) => "compact-details" | "expanded-rows"; |
| 52 | + formatMenuLayoutMode: (mode: "compact-details" | "expanded-rows") => string; |
| 53 | + applyDashboardDefaultsForKeys: ( |
| 54 | + draft: DashboardDisplaySettings, |
| 55 | + keys: readonly (keyof DashboardDisplaySettings)[], |
| 56 | + ) => DashboardDisplaySettings; |
| 57 | + DASHBOARD_DISPLAY_OPTIONS: readonly DashboardDisplaySettingOption[]; |
| 58 | + ACCOUNT_LIST_PANEL_KEYS: readonly (keyof DashboardDisplaySettings)[]; |
| 59 | + UI_COPY: typeof UI_COPY; |
| 60 | +} |
| 61 | + |
| 62 | +export async function promptDashboardDisplayPanel( |
| 63 | + initial: DashboardDisplaySettings, |
| 64 | + deps: DashboardDisplayPanelDeps, |
| 65 | +): Promise<DashboardDisplaySettings | null> { |
| 66 | + if (!input.isTTY || !output.isTTY) return null; |
| 67 | + |
| 68 | + const ui = getUiRuntimeOptions(); |
| 69 | + let draft = deps.cloneDashboardSettings(initial); |
| 70 | + let focusKey: DashboardDisplaySettingKey | "menuSortMode" | "menuLayoutMode" = |
| 71 | + deps.DASHBOARD_DISPLAY_OPTIONS[0]?.key ?? "menuShowStatusBadge"; |
| 72 | + |
| 73 | + while (true) { |
| 74 | + const preview = deps.buildAccountListPreview(draft, ui, focusKey); |
| 75 | + const optionItems: MenuItem<DashboardConfigAction>[] = |
| 76 | + deps.DASHBOARD_DISPLAY_OPTIONS.map((option, index) => { |
| 77 | + const enabled = draft[option.key] ?? true; |
| 78 | + return { |
| 79 | + label: `${deps.formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}`, |
| 80 | + hint: option.description, |
| 81 | + value: { type: "toggle", key: option.key }, |
| 82 | + color: enabled ? "green" : "yellow", |
| 83 | + }; |
| 84 | + }); |
| 85 | + const sortMode = |
| 86 | + draft.menuSortMode ?? |
| 87 | + DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? |
| 88 | + "ready-first"; |
| 89 | + const sortModeItem: MenuItem<DashboardConfigAction> = { |
| 90 | + label: `Sort mode: ${deps.formatMenuSortMode(sortMode)}`, |
| 91 | + hint: "Applies when smart sort is enabled.", |
| 92 | + value: { type: "cycle-sort-mode" }, |
| 93 | + color: sortMode === "ready-first" ? "green" : "yellow", |
| 94 | + }; |
| 95 | + const layoutMode = deps.resolveMenuLayoutMode(draft); |
| 96 | + const layoutModeItem: MenuItem<DashboardConfigAction> = { |
| 97 | + label: `Layout: ${deps.formatMenuLayoutMode(layoutMode)}`, |
| 98 | + hint: "Compact shows one-line rows with a selected details pane.", |
| 99 | + value: { type: "cycle-layout-mode" }, |
| 100 | + color: layoutMode === "compact-details" ? "green" : "yellow", |
| 101 | + }; |
| 102 | + const items: MenuItem<DashboardConfigAction>[] = [ |
| 103 | + { |
| 104 | + label: deps.UI_COPY.settings.previewHeading, |
| 105 | + value: { type: "cancel" }, |
| 106 | + kind: "heading", |
| 107 | + }, |
| 108 | + { |
| 109 | + label: preview.label, |
| 110 | + hint: preview.hint, |
| 111 | + value: { type: "cancel" }, |
| 112 | + color: "green", |
| 113 | + disabled: true, |
| 114 | + hideUnavailableSuffix: true, |
| 115 | + }, |
| 116 | + { label: "", value: { type: "cancel" }, separator: true }, |
| 117 | + { |
| 118 | + label: deps.UI_COPY.settings.displayHeading, |
| 119 | + value: { type: "cancel" }, |
| 120 | + kind: "heading", |
| 121 | + }, |
| 122 | + ...optionItems, |
| 123 | + sortModeItem, |
| 124 | + layoutModeItem, |
| 125 | + { label: "", value: { type: "cancel" }, separator: true }, |
| 126 | + { |
| 127 | + label: deps.UI_COPY.settings.resetDefault, |
| 128 | + value: { type: "reset" }, |
| 129 | + color: "yellow", |
| 130 | + }, |
| 131 | + { |
| 132 | + label: deps.UI_COPY.settings.saveAndBack, |
| 133 | + value: { type: "save" }, |
| 134 | + color: "green", |
| 135 | + }, |
| 136 | + { |
| 137 | + label: deps.UI_COPY.settings.backNoSave, |
| 138 | + value: { type: "cancel" }, |
| 139 | + color: "red", |
| 140 | + }, |
| 141 | + ]; |
| 142 | + |
| 143 | + const initialCursor = items.findIndex( |
| 144 | + (item) => |
| 145 | + (item.value.type === "toggle" && item.value.key === focusKey) || |
| 146 | + (item.value.type === "cycle-sort-mode" && |
| 147 | + focusKey === "menuSortMode") || |
| 148 | + (item.value.type === "cycle-layout-mode" && |
| 149 | + focusKey === "menuLayoutMode"), |
| 150 | + ); |
| 151 | + |
| 152 | + const updateFocusedPreview = (cursor: number) => { |
| 153 | + const focusedItem = items[cursor]; |
| 154 | + const focused = |
| 155 | + focusedItem?.value.type === "toggle" |
| 156 | + ? focusedItem.value.key |
| 157 | + : focusedItem?.value.type === "cycle-sort-mode" |
| 158 | + ? "menuSortMode" |
| 159 | + : focusedItem?.value.type === "cycle-layout-mode" |
| 160 | + ? "menuLayoutMode" |
| 161 | + : focusKey; |
| 162 | + const nextPreview = deps.buildAccountListPreview(draft, ui, focused); |
| 163 | + const previewItem = items[1]; |
| 164 | + if (!previewItem) return; |
| 165 | + previewItem.label = nextPreview.label; |
| 166 | + previewItem.hint = nextPreview.hint; |
| 167 | + }; |
| 168 | + |
| 169 | + const result = await select<DashboardConfigAction>(items, { |
| 170 | + message: deps.UI_COPY.settings.accountListTitle, |
| 171 | + subtitle: deps.UI_COPY.settings.accountListSubtitle, |
| 172 | + help: deps.UI_COPY.settings.accountListHelp, |
| 173 | + clearScreen: true, |
| 174 | + theme: ui.theme, |
| 175 | + selectedEmphasis: "minimal", |
| 176 | + initialCursor: initialCursor >= 0 ? initialCursor : undefined, |
| 177 | + onCursorChange: ({ cursor }) => { |
| 178 | + const focusedItem = items[cursor]; |
| 179 | + if (focusedItem?.value.type === "toggle") |
| 180 | + focusKey = focusedItem.value.key; |
| 181 | + else if (focusedItem?.value.type === "cycle-sort-mode") |
| 182 | + focusKey = "menuSortMode"; |
| 183 | + else if (focusedItem?.value.type === "cycle-layout-mode") |
| 184 | + focusKey = "menuLayoutMode"; |
| 185 | + updateFocusedPreview(cursor); |
| 186 | + }, |
| 187 | + onInput: (raw) => { |
| 188 | + const lower = raw.toLowerCase(); |
| 189 | + if (lower === "q") return { type: "cancel" }; |
| 190 | + if (lower === "s") return { type: "save" }; |
| 191 | + if (lower === "r") return { type: "reset" }; |
| 192 | + if (lower === "m") return { type: "cycle-sort-mode" }; |
| 193 | + if (lower === "l") return { type: "cycle-layout-mode" }; |
| 194 | + const parsed = Number.parseInt(raw, 10); |
| 195 | + if ( |
| 196 | + Number.isFinite(parsed) && |
| 197 | + parsed >= 1 && |
| 198 | + parsed <= deps.DASHBOARD_DISPLAY_OPTIONS.length |
| 199 | + ) { |
| 200 | + const target = deps.DASHBOARD_DISPLAY_OPTIONS[parsed - 1]; |
| 201 | + if (target) return { type: "toggle", key: target.key }; |
| 202 | + } |
| 203 | + if (parsed === deps.DASHBOARD_DISPLAY_OPTIONS.length + 1) |
| 204 | + return { type: "cycle-sort-mode" }; |
| 205 | + if (parsed === deps.DASHBOARD_DISPLAY_OPTIONS.length + 2) |
| 206 | + return { type: "cycle-layout-mode" }; |
| 207 | + return undefined; |
| 208 | + }, |
| 209 | + }); |
| 210 | + |
| 211 | + if (!result || result.type === "cancel") return null; |
| 212 | + if (result.type === "save") return draft; |
| 213 | + if (result.type === "reset") { |
| 214 | + draft = deps.applyDashboardDefaultsForKeys( |
| 215 | + draft, |
| 216 | + deps.ACCOUNT_LIST_PANEL_KEYS, |
| 217 | + ); |
| 218 | + focusKey = deps.DASHBOARD_DISPLAY_OPTIONS[0]?.key ?? focusKey; |
| 219 | + continue; |
| 220 | + } |
| 221 | + if (result.type === "cycle-sort-mode") { |
| 222 | + const currentMode = |
| 223 | + draft.menuSortMode ?? |
| 224 | + DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? |
| 225 | + "ready-first"; |
| 226 | + const nextMode: DashboardAccountSortMode = |
| 227 | + currentMode === "ready-first" ? "manual" : "ready-first"; |
| 228 | + draft = { |
| 229 | + ...draft, |
| 230 | + menuSortMode: nextMode, |
| 231 | + menuSortEnabled: |
| 232 | + nextMode === "ready-first" |
| 233 | + ? true |
| 234 | + : (draft.menuSortEnabled ?? |
| 235 | + DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? |
| 236 | + true), |
| 237 | + }; |
| 238 | + focusKey = "menuSortMode"; |
| 239 | + continue; |
| 240 | + } |
| 241 | + if (result.type === "cycle-layout-mode") { |
| 242 | + const currentLayout = deps.resolveMenuLayoutMode(draft); |
| 243 | + const nextLayout = |
| 244 | + currentLayout === "compact-details" |
| 245 | + ? "expanded-rows" |
| 246 | + : "compact-details"; |
| 247 | + draft = { |
| 248 | + ...draft, |
| 249 | + menuLayoutMode: nextLayout, |
| 250 | + menuShowDetailsForUnselectedRows: nextLayout === "expanded-rows", |
| 251 | + }; |
| 252 | + focusKey = "menuLayoutMode"; |
| 253 | + continue; |
| 254 | + } |
| 255 | + focusKey = result.key; |
| 256 | + draft = { ...draft, [result.key]: !draft[result.key] }; |
| 257 | + } |
| 258 | +} |
0 commit comments