@flyingrobots/bijou-tui 4.0.0 → 4.2.0
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/README.md +24 -7
- package/dist/app-frame-actions.d.ts +5 -5
- package/dist/app-frame-actions.d.ts.map +1 -1
- package/dist/app-frame-actions.js +71 -5
- package/dist/app-frame-actions.js.map +1 -1
- package/dist/app-frame-i18n.d.ts +12 -0
- package/dist/app-frame-i18n.d.ts.map +1 -0
- package/dist/app-frame-i18n.js +92 -0
- package/dist/app-frame-i18n.js.map +1 -0
- package/dist/app-frame-layers.d.ts +33 -0
- package/dist/app-frame-layers.d.ts.map +1 -0
- package/dist/app-frame-layers.js +136 -0
- package/dist/app-frame-layers.js.map +1 -0
- package/dist/app-frame-palette.d.ts +6 -2
- package/dist/app-frame-palette.d.ts.map +1 -1
- package/dist/app-frame-palette.js +42 -10
- package/dist/app-frame-palette.js.map +1 -1
- package/dist/app-frame-render.d.ts +13 -5
- package/dist/app-frame-render.d.ts.map +1 -1
- package/dist/app-frame-render.js +216 -70
- package/dist/app-frame-render.js.map +1 -1
- package/dist/app-frame-types.d.ts +120 -20
- package/dist/app-frame-types.d.ts.map +1 -1
- package/dist/app-frame-types.js +8 -6
- package/dist/app-frame-types.js.map +1 -1
- package/dist/app-frame-utils.d.ts +8 -3
- package/dist/app-frame-utils.d.ts.map +1 -1
- package/dist/app-frame-utils.js +42 -27
- package/dist/app-frame-utils.js.map +1 -1
- package/dist/app-frame.d.ts +125 -11
- package/dist/app-frame.d.ts.map +1 -1
- package/dist/app-frame.js +1406 -158
- package/dist/app-frame.js.map +1 -1
- package/dist/browsable-list.d.ts.map +1 -1
- package/dist/browsable-list.js +15 -5
- package/dist/browsable-list.js.map +1 -1
- package/dist/collection-surface.d.ts +8 -0
- package/dist/collection-surface.d.ts.map +1 -0
- package/dist/collection-surface.js +41 -0
- package/dist/collection-surface.js.map +1 -0
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -3
- package/dist/command-palette.js.map +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/eventbus.d.ts.map +1 -1
- package/dist/eventbus.js +21 -1
- package/dist/eventbus.js.map +1 -1
- package/dist/focus-area.d.ts +7 -0
- package/dist/focus-area.d.ts.map +1 -1
- package/dist/focus-area.js +33 -15
- package/dist/focus-area.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/inspector-drawer.d.ts +36 -0
- package/dist/inspector-drawer.d.ts.map +1 -0
- package/dist/inspector-drawer.js +68 -0
- package/dist/inspector-drawer.js.map +1 -0
- package/dist/layout-node-surface.d.ts +8 -0
- package/dist/layout-node-surface.d.ts.map +1 -1
- package/dist/layout-node-surface.js +26 -7
- package/dist/layout-node-surface.js.map +1 -1
- package/dist/notification.d.ts +10 -1
- package/dist/notification.d.ts.map +1 -1
- package/dist/notification.js +142 -17
- package/dist/notification.js.map +1 -1
- package/dist/overlay.d.ts +8 -0
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +23 -10
- package/dist/overlay.js.map +1 -1
- package/dist/runtime-engine.d.ts +223 -0
- package/dist/runtime-engine.d.ts.map +1 -0
- package/dist/runtime-engine.js +457 -0
- package/dist/runtime-engine.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +27 -7
- package/dist/runtime.js.map +1 -1
- package/dist/shell-quit.d.ts +12 -0
- package/dist/shell-quit.d.ts.map +1 -0
- package/dist/shell-quit.js +36 -0
- package/dist/shell-quit.js.map +1 -0
- package/dist/subapp/mount.d.ts.map +1 -1
- package/dist/subapp/mount.js +3 -0
- package/dist/subapp/mount.js.map +1 -1
- package/dist/types.d.ts +19 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -1
- package/dist/view-output.d.ts +1 -0
- package/dist/view-output.d.ts.map +1 -1
- package/dist/view-output.js +25 -15
- package/dist/view-output.js.map +1 -1
- package/package.json +5 -2
package/dist/app-frame.js
CHANGED
|
@@ -4,23 +4,101 @@
|
|
|
4
4
|
* Provides tabs, pane focus/scroll isolation, shell key handling, help,
|
|
5
5
|
* panel-scoped overlay context, and optional frame-level command palette.
|
|
6
6
|
*/
|
|
7
|
-
import { createSurface, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
|
|
8
|
-
import {
|
|
7
|
+
import { createSurface, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
|
|
8
|
+
import { helpViewSurface } from './help.js';
|
|
9
|
+
import { createKeyMap } from './keybindings.js';
|
|
9
10
|
import { isKeyMsg, isMouseMsg, isResizeMsg } from './types.js';
|
|
10
|
-
import {
|
|
11
|
+
import { quit } from './commands.js';
|
|
12
|
+
import { compositeSurfaceInto, drawer, modal } from './overlay.js';
|
|
13
|
+
import { isShellQuitConfirmAccept, isShellQuitConfirmDismiss, isShellQuitRequest, renderShellQuitOverlay, shouldUseShellQuitConfirm, } from './shell-quit.js';
|
|
11
14
|
import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
|
|
15
|
+
import { createPagerStateForSurface, pagerSurface, } from './pager.js';
|
|
12
16
|
import { restoreLayoutState } from './layout-preset.js';
|
|
13
|
-
import { createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
|
|
17
|
+
import { countNotificationHistory, createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationHistorySurface, renderNotificationReviewEntrySurface, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
|
|
18
|
+
import { insetLineSurface } from './collection-surface.js';
|
|
19
|
+
import { vstackSurface } from './surface-layout.js';
|
|
14
20
|
import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
|
|
15
21
|
import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
22
|
+
import { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, } from './app-frame-layers.js';
|
|
23
|
+
import { applyRuntimeCommandBuffer, bufferRuntimeRouteResult, createRuntimeBuffers, createRuntimeRetainedLayouts, retainRuntimeLayout, routeRuntimeInput, } from './runtime-engine.js';
|
|
24
|
+
import { frameEndAnchor, frameMessage, frameNotificationCue, frameNotificationFilterLabel, frameStartAnchor, } from './app-frame-i18n.js';
|
|
25
|
+
import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, } from './app-frame-render.js';
|
|
26
|
+
import { applyFrameAction, scrollFocusedPane, switchTab, syncPageFrameState, } from './app-frame-actions.js';
|
|
27
|
+
import { handlePaletteKey, openCommandPalette, openSearchPalette, } from './app-frame-palette.js';
|
|
28
|
+
export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, underlyingFrameLayer, } from './app-frame-layers.js';
|
|
19
29
|
// ---------------------------------------------------------------------------
|
|
20
30
|
// Frame Notification Helpers
|
|
21
31
|
// ---------------------------------------------------------------------------
|
|
22
32
|
const FRAME_NOTIFICATION_TICK_MS = 40;
|
|
23
33
|
const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
|
|
34
|
+
const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
|
|
35
|
+
const EMPTY_RUNTIME_LAYOUTS = createRuntimeRetainedLayouts();
|
|
36
|
+
const DEFAULT_NOTIFICATION_CENTER_FILTERS = [
|
|
37
|
+
'ALL',
|
|
38
|
+
'ACTIONABLE',
|
|
39
|
+
'ERROR',
|
|
40
|
+
'WARNING',
|
|
41
|
+
'SUCCESS',
|
|
42
|
+
'INFO',
|
|
43
|
+
];
|
|
44
|
+
const quitHelpKeys = createKeyMap()
|
|
45
|
+
.group('Exit', (g) => g
|
|
46
|
+
.bind('q', 'Quit', { type: 'toggle-help' })
|
|
47
|
+
.bind('escape', 'Quit', { type: 'toggle-help' })
|
|
48
|
+
.bind('ctrl+c', 'Quit', { type: 'toggle-help' }));
|
|
49
|
+
const helpLayerHelpKeys = createKeyMap()
|
|
50
|
+
.group('Help', (g) => g
|
|
51
|
+
.bind('escape', 'Close help', { type: 'noop' })
|
|
52
|
+
.bind('?', 'Close help', { type: 'noop' })
|
|
53
|
+
.bind('up', 'Scroll up', { type: 'noop' })
|
|
54
|
+
.bind('down', 'Scroll down', { type: 'noop' })
|
|
55
|
+
.bind('j', 'Scroll down', { type: 'noop' })
|
|
56
|
+
.bind('k', 'Scroll up', { type: 'noop' })
|
|
57
|
+
.bind('d', 'Page down', { type: 'noop' })
|
|
58
|
+
.bind('u', 'Page up', { type: 'noop' })
|
|
59
|
+
.bind('g', 'Top', { type: 'noop' })
|
|
60
|
+
.bind('shift+g', 'Bottom', { type: 'noop' }));
|
|
61
|
+
const settingsHelpKeys = createKeyMap()
|
|
62
|
+
.group('Settings', (g) => g
|
|
63
|
+
.bind('escape', 'Close settings', { type: 'toggle-settings' })
|
|
64
|
+
.bind('f2', 'Close settings', { type: 'toggle-settings' })
|
|
65
|
+
.bind('up', 'Previous row', { type: 'scroll-up' })
|
|
66
|
+
.bind('down', 'Next row', { type: 'scroll-down' })
|
|
67
|
+
.bind('enter', 'Activate setting', { type: 'toggle-settings' })
|
|
68
|
+
.bind('space', 'Activate setting', { type: 'toggle-settings' })
|
|
69
|
+
.bind('j', 'Scroll down', { type: 'scroll-down' })
|
|
70
|
+
.bind('k', 'Scroll up', { type: 'scroll-up' })
|
|
71
|
+
.bind('d', 'Page down', { type: 'page-down' })
|
|
72
|
+
.bind('u', 'Page up', { type: 'page-up' })
|
|
73
|
+
.bind('g', 'Top', { type: 'top' })
|
|
74
|
+
.bind('shift+g', 'Bottom', { type: 'bottom' })
|
|
75
|
+
.bind('/', 'Search', { type: 'open-search' })
|
|
76
|
+
.bind('ctrl+p', 'Open command palette', { type: 'open-palette' })
|
|
77
|
+
.bind(':', 'Open command palette', { type: 'open-palette' })
|
|
78
|
+
.bind('?', 'Toggle help', { type: 'toggle-help' }));
|
|
79
|
+
const notificationCenterHelpKeys = createKeyMap()
|
|
80
|
+
.group('Notifications', (g) => g
|
|
81
|
+
.bind('shift+n', 'Close notification center', { type: 'noop' })
|
|
82
|
+
.bind('up', 'Scroll up', { type: 'noop' })
|
|
83
|
+
.bind('down', 'Scroll down', { type: 'noop' })
|
|
84
|
+
.bind('j', 'Scroll down', { type: 'noop' })
|
|
85
|
+
.bind('k', 'Scroll up', { type: 'noop' })
|
|
86
|
+
.bind('d', 'Page down', { type: 'noop' })
|
|
87
|
+
.bind('u', 'Page up', { type: 'noop' })
|
|
88
|
+
.bind('g', 'Top', { type: 'noop' })
|
|
89
|
+
.bind('shift+g', 'Bottom', { type: 'noop' })
|
|
90
|
+
.bind('f', 'Cycle filter', { type: 'noop' })
|
|
91
|
+
.bind('/', 'Search', { type: 'noop' })
|
|
92
|
+
.bind('ctrl+p', 'Open command palette', { type: 'noop' })
|
|
93
|
+
.bind(':', 'Open command palette', { type: 'noop' })
|
|
94
|
+
.bind('?', 'Toggle help', { type: 'noop' }));
|
|
95
|
+
const quitConfirmHelpKeys = createKeyMap()
|
|
96
|
+
.group('Quit', (g) => g
|
|
97
|
+
.bind('y', 'Quit', { type: 'noop' })
|
|
98
|
+
.bind('enter', 'Quit', { type: 'noop' })
|
|
99
|
+
.bind('n', 'Stay', { type: 'noop' })
|
|
100
|
+
.bind('escape', 'Stay', { type: 'noop' })
|
|
101
|
+
.bind('q', 'Stay', { type: 'noop' }));
|
|
24
102
|
function resolveFrameNotificationOptions(options) {
|
|
25
103
|
if (options.runtimeNotifications === false) {
|
|
26
104
|
return {
|
|
@@ -77,8 +155,13 @@ export function createFramedApp(options) {
|
|
|
77
155
|
if (!pagesById.has(defaultPageId)) {
|
|
78
156
|
throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
|
|
79
157
|
}
|
|
80
|
-
const frameKeys = createFrameKeyMap(
|
|
158
|
+
const frameKeys = createFrameKeyMap({
|
|
159
|
+
enableSettings: options.settings != null,
|
|
160
|
+
enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
|
|
161
|
+
i18n: options.i18n,
|
|
162
|
+
});
|
|
81
163
|
const frameNotificationOptions = resolveFrameNotificationOptions(options);
|
|
164
|
+
let composedFrameScratch = null;
|
|
82
165
|
const paletteKeys = commandPaletteKeyMap({
|
|
83
166
|
focusNext: { type: 'cp-next' },
|
|
84
167
|
focusPrev: { type: 'cp-prev' },
|
|
@@ -87,11 +170,783 @@ export function createFramedApp(options) {
|
|
|
87
170
|
select: { type: 'cp-select' },
|
|
88
171
|
close: { type: 'cp-close' },
|
|
89
172
|
});
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
173
|
+
function getComposedFrameScratch(width, height) {
|
|
174
|
+
if (composedFrameScratch == null
|
|
175
|
+
|| composedFrameScratch.width !== width
|
|
176
|
+
|| composedFrameScratch.height !== height) {
|
|
177
|
+
composedFrameScratch = createSurface(width, height);
|
|
178
|
+
}
|
|
179
|
+
return composedFrameScratch;
|
|
180
|
+
}
|
|
181
|
+
function closeCommandPalette(model) {
|
|
182
|
+
return {
|
|
183
|
+
...model,
|
|
184
|
+
commandPalette: undefined,
|
|
185
|
+
commandPaletteEntries: undefined,
|
|
186
|
+
commandPaletteTitle: undefined,
|
|
187
|
+
commandPaletteKind: undefined,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const shellCommandHandlers = {
|
|
191
|
+
// --- overlay lifecycle ---
|
|
192
|
+
'close-help': (model) => ({ ...model, helpOpen: false, helpScrollY: 0 }),
|
|
193
|
+
'close-settings': (model) => ({ ...model, settingsOpen: false }),
|
|
194
|
+
'close-notification-center': (model) => ({ ...model, notificationCenterOpen: false, notificationCenterScrollY: 0 }),
|
|
195
|
+
'close-palette': (model) => closeCommandPalette(model),
|
|
196
|
+
'close-quit-confirm': (model) => ({ ...model, quitConfirmOpen: false }),
|
|
197
|
+
'open-help': (model) => ({ ...model, helpOpen: true }),
|
|
198
|
+
'open-quit-confirm': (model) => {
|
|
199
|
+
if (!shouldUseShellQuitConfirm())
|
|
200
|
+
return model;
|
|
201
|
+
if (model.quitConfirmOpen)
|
|
202
|
+
return model;
|
|
203
|
+
return {
|
|
204
|
+
...model,
|
|
205
|
+
quitConfirmOpen: true,
|
|
206
|
+
helpOpen: false,
|
|
207
|
+
helpScrollY: 0,
|
|
208
|
+
settingsOpen: false,
|
|
209
|
+
notificationCenterOpen: false,
|
|
210
|
+
commandPalette: undefined,
|
|
211
|
+
commandPaletteEntries: undefined,
|
|
212
|
+
commandPaletteTitle: undefined,
|
|
213
|
+
commandPaletteKind: undefined,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
'open-search-palette': (model) => openSearchPalette(model, frameKeys, options, pagesById),
|
|
217
|
+
'open-command-palette': (model) => openCommandPalette(model, frameKeys, options, pagesById),
|
|
218
|
+
// --- settings ---
|
|
219
|
+
'settings-focus-move': (model, cmd) => {
|
|
220
|
+
const c = cmd;
|
|
221
|
+
const layout = resolveSettingsLayout(model, options, pagesById);
|
|
222
|
+
return layout != null ? moveSettingsFocus(model, layout, c.delta) : model;
|
|
223
|
+
},
|
|
224
|
+
'settings-scroll': (model, cmd) => {
|
|
225
|
+
const c = cmd;
|
|
226
|
+
const layout = resolveSettingsLayout(model, options, pagesById);
|
|
227
|
+
return layout != null ? scrollSettingsBy(model, layout, c.delta) : model;
|
|
228
|
+
},
|
|
229
|
+
'settings-scroll-to': (model, cmd) => {
|
|
230
|
+
const c = cmd;
|
|
231
|
+
const layout = resolveSettingsLayout(model, options, pagesById);
|
|
232
|
+
if (layout == null)
|
|
233
|
+
return model;
|
|
234
|
+
return { ...model, settingsScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
|
|
235
|
+
},
|
|
236
|
+
'activate-settings-row': (model, cmd, teaCmds) => {
|
|
237
|
+
const c = cmd;
|
|
238
|
+
const layout = resolveSettingsLayout(model, options, pagesById);
|
|
239
|
+
if (layout == null)
|
|
240
|
+
return model;
|
|
241
|
+
const hitRow = layout.rows.find((r) => r.index === c.rowIndex);
|
|
242
|
+
if (hitRow == null)
|
|
243
|
+
return model;
|
|
244
|
+
const focusedModel = { ...model, settingsFocusIndex: hitRow.index };
|
|
245
|
+
if (hitRow.row.action === undefined || hitRow.row.enabled === false || hitRow.row.kind === 'info') {
|
|
246
|
+
return focusedModel;
|
|
247
|
+
}
|
|
248
|
+
const [nextModel, cmds] = activateSettingsRow(focusedModel, hitRow.row);
|
|
249
|
+
teaCmds.push(...cmds);
|
|
250
|
+
return nextModel;
|
|
251
|
+
},
|
|
252
|
+
// --- notification center ---
|
|
253
|
+
'notification-center-scroll': (model, cmd) => {
|
|
254
|
+
const c = cmd;
|
|
255
|
+
const layout = resolveNotificationCenterLayout(model, options, pagesById);
|
|
256
|
+
return layout != null ? scrollNotificationCenterBy(model, layout, c.delta) : model;
|
|
257
|
+
},
|
|
258
|
+
'notification-center-scroll-to': (model, cmd) => {
|
|
259
|
+
const c = cmd;
|
|
260
|
+
const layout = resolveNotificationCenterLayout(model, options, pagesById);
|
|
261
|
+
if (layout == null)
|
|
262
|
+
return model;
|
|
263
|
+
return { ...model, notificationCenterScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
|
|
264
|
+
},
|
|
265
|
+
'cycle-notification-filter': (model, _cmd, teaCmds) => {
|
|
266
|
+
const layout = resolveNotificationCenterLayout(model, options, pagesById);
|
|
267
|
+
if (layout == null)
|
|
268
|
+
return model;
|
|
269
|
+
const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
|
|
270
|
+
teaCmds.push(...cmds);
|
|
271
|
+
return nextModel;
|
|
272
|
+
},
|
|
273
|
+
// --- help ---
|
|
274
|
+
'help-scroll': (model, cmd) => {
|
|
275
|
+
const c = cmd;
|
|
276
|
+
const activePage = pagesById.get(model.activePageId);
|
|
277
|
+
const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
|
|
278
|
+
const viewportHeight = Math.max(1, overlay.body.height - 1);
|
|
279
|
+
const delta = c.action === 'down' ? 3
|
|
280
|
+
: c.action === 'up' ? -3
|
|
281
|
+
: c.action === 'page-down' ? viewportHeight
|
|
282
|
+
: c.action === 'page-up' ? -viewportHeight
|
|
283
|
+
: c.action === 'bottom' ? Infinity
|
|
284
|
+
: /* top */ -Infinity;
|
|
285
|
+
return {
|
|
286
|
+
...model,
|
|
287
|
+
helpScrollY: Math.max(0, Math.min(overlay.maxScrollY, overlay.scrollY + delta)),
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
// --- workspace ---
|
|
291
|
+
'focus-pane': (model, cmd) => {
|
|
292
|
+
const c = cmd;
|
|
293
|
+
return focusPane(model, c.paneId);
|
|
294
|
+
},
|
|
295
|
+
'scroll-focused-pane': (model, cmd) => {
|
|
296
|
+
const c = cmd;
|
|
297
|
+
return scrollFocusedPane(model, { type: c.direction === 'down' ? 'scroll-down' : 'scroll-up' }, pagesById, options);
|
|
298
|
+
},
|
|
299
|
+
'switch-tab': (model, cmd, teaCmds) => {
|
|
300
|
+
const c = cmd;
|
|
301
|
+
const [nextModel, cmds] = switchTab(model, c.delta, pagesById, options);
|
|
302
|
+
teaCmds.push(...cmds);
|
|
303
|
+
return nextModel;
|
|
304
|
+
},
|
|
305
|
+
// --- delegation ---
|
|
306
|
+
'apply-frame-action': (model, cmd, teaCmds) => {
|
|
307
|
+
const c = cmd;
|
|
308
|
+
const [nextModel, cmds] = applyFrameAction(c.action, model, options, pagesById);
|
|
309
|
+
teaCmds.push(...cmds);
|
|
310
|
+
return nextModel;
|
|
311
|
+
},
|
|
312
|
+
'palette-key': (model, cmd, teaCmds) => {
|
|
313
|
+
const c = cmd;
|
|
314
|
+
const [nextModel, cmds] = handlePaletteKey(c.msg, model, paletteKeys, options, pagesById);
|
|
315
|
+
teaCmds.push(...cmds);
|
|
316
|
+
return nextModel;
|
|
317
|
+
},
|
|
318
|
+
// --- TEA command emissions ---
|
|
319
|
+
'emit-page-msg': (model, cmd, teaCmds) => {
|
|
320
|
+
const c = cmd;
|
|
321
|
+
teaCmds.push(emitMsgForPage(c.pageId, c.msg));
|
|
322
|
+
return model;
|
|
323
|
+
},
|
|
324
|
+
'emit-global-msg': (model, cmd, teaCmds) => {
|
|
325
|
+
const c = cmd;
|
|
326
|
+
teaCmds.push(emitMsg(c.msg));
|
|
327
|
+
return model;
|
|
328
|
+
},
|
|
329
|
+
'quit': (_model, _cmd, teaCmds) => {
|
|
330
|
+
teaCmds.push(quit());
|
|
331
|
+
return _model;
|
|
332
|
+
},
|
|
333
|
+
'dismiss-notification': (model, cmd, teaCmds) => {
|
|
334
|
+
const c = cmd;
|
|
335
|
+
if (!frameNotificationOptions.enabled)
|
|
336
|
+
return model;
|
|
337
|
+
const nowMs = resolveClock(resolveSafeCtx()).now();
|
|
338
|
+
const [nextModel, cmds] = applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, c.notificationId, nowMs), nowMs);
|
|
339
|
+
teaCmds.push(...cmds);
|
|
340
|
+
return nextModel;
|
|
341
|
+
},
|
|
342
|
+
// --- observation ---
|
|
343
|
+
'observed-key': (model, cmd, teaCmds) => {
|
|
344
|
+
const c = cmd;
|
|
345
|
+
const observed = options.observeKey?.(c.msg, c.route);
|
|
346
|
+
if (observed !== undefined) {
|
|
347
|
+
teaCmds.push(emitMsgForPage(model.activePageId, observed));
|
|
348
|
+
}
|
|
349
|
+
return model;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
function drainShellCommandBuffer(model, routeResult) {
|
|
353
|
+
const buffers = bufferRuntimeRouteResult(createRuntimeBuffers(), routeResult);
|
|
354
|
+
const teaCmds = [];
|
|
355
|
+
const { state } = applyRuntimeCommandBuffer(model, buffers.commands, (s, cmd) => shellCommandHandlers[cmd.type](s, cmd, teaCmds));
|
|
356
|
+
return [state, teaCmds];
|
|
357
|
+
}
|
|
358
|
+
function resolveLayerContext(model) {
|
|
359
|
+
const activePage = pagesById.get(model.activePageId);
|
|
360
|
+
const activePageModel = model.pageModels[model.activePageId];
|
|
361
|
+
const inputAreas = resolveInputAreas(activePage, activePageModel);
|
|
362
|
+
const activeInputArea = findInputAreaByPaneId(inputAreas, model.focusedPaneByPage[model.activePageId]);
|
|
363
|
+
const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
|
|
364
|
+
const pageModalOpen = modalKeyMap != null;
|
|
365
|
+
const activeLayer = activeFrameLayer(model, { pageModalOpen });
|
|
366
|
+
return {
|
|
367
|
+
activePage,
|
|
368
|
+
activePageModel,
|
|
369
|
+
inputAreas,
|
|
370
|
+
activeInputArea,
|
|
371
|
+
modalKeyMap,
|
|
372
|
+
pageModalOpen,
|
|
373
|
+
activeLayer,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function quitRequestCommands(msg, route) {
|
|
377
|
+
if (!shouldUseShellQuitConfirm()) {
|
|
378
|
+
return [{ type: 'observed-key', msg, route }, { type: 'quit' }];
|
|
379
|
+
}
|
|
380
|
+
return [{ type: 'observed-key', msg, route }, { type: 'open-quit-confirm' }];
|
|
381
|
+
}
|
|
382
|
+
function resolveFrameActionCommands(msg, action, route) {
|
|
383
|
+
if (action.type === 'open-search' && options.enableCommandPalette) {
|
|
384
|
+
return [{ type: 'observed-key', msg, route }, { type: 'open-search-palette' }];
|
|
385
|
+
}
|
|
386
|
+
if (action.type === 'open-palette' && options.enableCommandPalette) {
|
|
387
|
+
return [{ type: 'observed-key', msg, route }, { type: 'open-command-palette' }];
|
|
388
|
+
}
|
|
389
|
+
return [{ type: 'observed-key', msg, route }, { type: 'apply-frame-action', action }];
|
|
390
|
+
}
|
|
391
|
+
function handlePaletteLayerKeyCommands(msg, routedLayerKind) {
|
|
392
|
+
const obs = { type: 'observed-key', msg, route: 'palette' };
|
|
393
|
+
if (msg.ctrl && !msg.alt && msg.key === 'c') {
|
|
394
|
+
return quitRequestCommands(msg, 'palette');
|
|
395
|
+
}
|
|
396
|
+
if (!msg.ctrl && !msg.alt && !msg.shift && msg.key === 'escape') {
|
|
397
|
+
return [obs, { type: 'close-palette' }];
|
|
398
|
+
}
|
|
399
|
+
const frameAction = frameKeys.handle(msg);
|
|
400
|
+
if (frameAction?.type === 'open-search') {
|
|
401
|
+
return routedLayerKind === 'search'
|
|
402
|
+
? [obs, { type: 'close-palette' }]
|
|
403
|
+
: [obs, { type: 'open-search-palette' }];
|
|
404
|
+
}
|
|
405
|
+
if (frameAction?.type === 'open-palette') {
|
|
406
|
+
return routedLayerKind === 'command-palette'
|
|
407
|
+
? [obs, { type: 'close-palette' }]
|
|
408
|
+
: [obs, { type: 'open-command-palette' }];
|
|
409
|
+
}
|
|
410
|
+
if (frameAction?.type === 'toggle-notifications') {
|
|
411
|
+
return [obs, { type: 'close-palette' }, { type: 'apply-frame-action', action: frameAction }];
|
|
412
|
+
}
|
|
413
|
+
return [obs, { type: 'palette-key', msg }];
|
|
414
|
+
}
|
|
415
|
+
function handleHelpLayerKeyCommands(msg) {
|
|
416
|
+
const obs = { type: 'observed-key', msg, route: 'help' };
|
|
417
|
+
if (!msg.ctrl && !msg.alt && (msg.key === '?' || msg.key === 'escape')) {
|
|
418
|
+
return [obs, { type: 'close-help' }];
|
|
419
|
+
}
|
|
420
|
+
if (isShellQuitRequest(msg)) {
|
|
421
|
+
return quitRequestCommands(msg, 'help');
|
|
422
|
+
}
|
|
423
|
+
const helpAction = frameKeys.handle(msg);
|
|
424
|
+
if (helpAction && isHelpScrollAction(helpAction)) {
|
|
425
|
+
const action = helpAction.type === 'scroll-down' ? 'down'
|
|
426
|
+
: helpAction.type === 'scroll-up' ? 'up'
|
|
427
|
+
: helpAction.type === 'page-down' ? 'page-down'
|
|
428
|
+
: helpAction.type === 'page-up' ? 'page-up'
|
|
429
|
+
: helpAction.type === 'bottom' ? 'bottom'
|
|
430
|
+
: 'top';
|
|
431
|
+
return [obs, { type: 'help-scroll', action }];
|
|
432
|
+
}
|
|
433
|
+
return [obs];
|
|
434
|
+
}
|
|
435
|
+
function handleSettingsLayerKeyCommands(msg, model) {
|
|
436
|
+
const layout = resolveSettingsLayout(model, options, pagesById);
|
|
437
|
+
if (layout == null)
|
|
438
|
+
return undefined;
|
|
439
|
+
const obs = { type: 'observed-key', msg, route: 'frame' };
|
|
440
|
+
if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === 'f2')) {
|
|
441
|
+
return [obs, { type: 'close-settings' }];
|
|
442
|
+
}
|
|
443
|
+
if (msg.ctrl && !msg.alt && msg.key === ',') {
|
|
444
|
+
return [obs, { type: 'close-settings' }];
|
|
445
|
+
}
|
|
446
|
+
if (!msg.ctrl && !msg.alt && msg.key === '?') {
|
|
447
|
+
return [obs, { type: 'open-help' }];
|
|
448
|
+
}
|
|
449
|
+
if (isShellQuitRequest(msg)) {
|
|
450
|
+
return quitRequestCommands(msg, 'frame');
|
|
451
|
+
}
|
|
452
|
+
if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
|
|
453
|
+
return [obs, { type: 'open-search-palette' }];
|
|
454
|
+
}
|
|
455
|
+
if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
|
|
456
|
+
return [obs, { type: 'open-command-palette' }];
|
|
457
|
+
}
|
|
458
|
+
const settingsFrameAction = frameKeys.handle(msg);
|
|
459
|
+
if (settingsFrameAction?.type === 'toggle-notifications') {
|
|
460
|
+
return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
|
|
461
|
+
}
|
|
462
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'up') {
|
|
463
|
+
return [obs, { type: 'settings-focus-move', delta: -1 }];
|
|
464
|
+
}
|
|
465
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'down') {
|
|
466
|
+
return [obs, { type: 'settings-focus-move', delta: 1 }];
|
|
467
|
+
}
|
|
468
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'j') {
|
|
469
|
+
return [obs, { type: 'settings-scroll', delta: 1 }];
|
|
470
|
+
}
|
|
471
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'k') {
|
|
472
|
+
return [obs, { type: 'settings-scroll', delta: -1 }];
|
|
473
|
+
}
|
|
474
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'd') {
|
|
475
|
+
return [obs, { type: 'settings-scroll', delta: Math.max(1, layout.contentHeight - 1) }];
|
|
476
|
+
}
|
|
477
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'u') {
|
|
478
|
+
return [obs, { type: 'settings-scroll', delta: -Math.max(1, layout.contentHeight - 1) }];
|
|
479
|
+
}
|
|
480
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'g') {
|
|
481
|
+
return [obs, { type: 'settings-scroll-to', position: 'top' }];
|
|
482
|
+
}
|
|
483
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'G') {
|
|
484
|
+
return [obs, { type: 'settings-scroll-to', position: 'bottom' }];
|
|
485
|
+
}
|
|
486
|
+
if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
|
|
487
|
+
const rowIndex = clampSettingsFocus(model, layout);
|
|
488
|
+
const row = layout.rows[rowIndex];
|
|
489
|
+
if (row?.row.action !== undefined && row.row.enabled !== false && row.row.kind !== 'info') {
|
|
490
|
+
return [obs, { type: 'activate-settings-row', rowIndex: row.index }];
|
|
491
|
+
}
|
|
492
|
+
return [obs];
|
|
493
|
+
}
|
|
494
|
+
return [obs];
|
|
495
|
+
}
|
|
496
|
+
function handleNotificationCenterLayerKeyCommands(msg, model) {
|
|
497
|
+
const layout = resolveNotificationCenterLayout(model, options, pagesById);
|
|
498
|
+
if (layout == null)
|
|
499
|
+
return undefined;
|
|
500
|
+
const obs = { type: 'observed-key', msg, route: 'frame' };
|
|
501
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
|
|
502
|
+
return [obs, { type: 'close-notification-center' }];
|
|
503
|
+
}
|
|
504
|
+
if (isShellQuitRequest(msg)) {
|
|
505
|
+
return quitRequestCommands(msg, 'frame');
|
|
506
|
+
}
|
|
507
|
+
const centerFrameAction = frameKeys.handle(msg);
|
|
508
|
+
if (centerFrameAction?.type === 'toggle-notifications') {
|
|
509
|
+
return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
|
|
510
|
+
}
|
|
511
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
|
|
512
|
+
return [obs, { type: 'close-notification-center' }, { type: 'apply-frame-action', action: { type: 'toggle-settings' } }];
|
|
513
|
+
}
|
|
514
|
+
if (!msg.ctrl && !msg.alt && msg.key === '?') {
|
|
515
|
+
return [obs, { type: 'close-notification-center' }, { type: 'open-help' }];
|
|
516
|
+
}
|
|
517
|
+
if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
|
|
518
|
+
return [obs, { type: 'close-notification-center' }, { type: 'open-search-palette' }];
|
|
519
|
+
}
|
|
520
|
+
if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
|
|
521
|
+
return [obs, { type: 'close-notification-center' }, { type: 'open-command-palette' }];
|
|
522
|
+
}
|
|
523
|
+
if (!msg.ctrl && !msg.alt && (msg.key === 'up' || msg.key === 'k')) {
|
|
524
|
+
return [obs, { type: 'notification-center-scroll', delta: -1 }];
|
|
525
|
+
}
|
|
526
|
+
if (!msg.ctrl && !msg.alt && (msg.key === 'down' || msg.key === 'j')) {
|
|
527
|
+
return [obs, { type: 'notification-center-scroll', delta: 1 }];
|
|
528
|
+
}
|
|
529
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'd') {
|
|
530
|
+
return [obs, { type: 'notification-center-scroll', delta: Math.max(1, layout.contentHeight - 2) }];
|
|
531
|
+
}
|
|
532
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'u') {
|
|
533
|
+
return [obs, { type: 'notification-center-scroll', delta: -Math.max(1, layout.contentHeight - 2) }];
|
|
534
|
+
}
|
|
535
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'g') {
|
|
536
|
+
return [obs, { type: 'notification-center-scroll-to', position: 'top' }];
|
|
537
|
+
}
|
|
538
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'G') {
|
|
539
|
+
return [obs, { type: 'notification-center-scroll-to', position: 'bottom' }];
|
|
540
|
+
}
|
|
541
|
+
if (!msg.ctrl && !msg.alt && msg.key === 'f') {
|
|
542
|
+
return [obs, { type: 'cycle-notification-filter' }];
|
|
543
|
+
}
|
|
544
|
+
return [obs];
|
|
545
|
+
}
|
|
546
|
+
function handleWorkspaceLayerKeyCommands(msg, model) {
|
|
547
|
+
if (isShellQuitRequest(msg)) {
|
|
548
|
+
return quitRequestCommands(msg, 'frame');
|
|
549
|
+
}
|
|
550
|
+
const context = resolveLayerContext(model);
|
|
551
|
+
const { activePage, activeInputArea } = context;
|
|
552
|
+
const paneAction = activeInputArea?.keyMap?.handle(msg);
|
|
553
|
+
const pageAction = activePage.keyMap?.handle(msg);
|
|
554
|
+
const globalAction = options.globalKeys?.handle(msg);
|
|
555
|
+
const frameAction = frameKeys.handle(msg);
|
|
556
|
+
const keyPriority = options.keyPriority ?? 'frame-first';
|
|
557
|
+
if (keyPriority === 'page-first') {
|
|
558
|
+
if (paneAction !== undefined) {
|
|
559
|
+
return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
|
|
560
|
+
}
|
|
561
|
+
if (pageAction !== undefined) {
|
|
562
|
+
return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: pageAction }];
|
|
563
|
+
}
|
|
564
|
+
if (globalAction !== undefined) {
|
|
565
|
+
return [{ type: 'observed-key', msg, route: 'global' }, { type: 'emit-global-msg', msg: globalAction }];
|
|
566
|
+
}
|
|
567
|
+
if (frameAction !== undefined) {
|
|
568
|
+
return resolveFrameActionCommands(msg, frameAction, 'frame');
|
|
569
|
+
}
|
|
570
|
+
return [{ type: 'observed-key', msg, route: 'unhandled' }];
|
|
571
|
+
}
|
|
572
|
+
// frame-first (default)
|
|
573
|
+
if (frameAction !== undefined) {
|
|
574
|
+
return resolveFrameActionCommands(msg, frameAction, 'frame');
|
|
575
|
+
}
|
|
576
|
+
if (paneAction !== undefined) {
|
|
577
|
+
return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
|
|
578
|
+
}
|
|
579
|
+
if (globalAction !== undefined) {
|
|
580
|
+
return [{ type: 'observed-key', msg, route: 'global' }, { type: 'emit-global-msg', msg: globalAction }];
|
|
581
|
+
}
|
|
582
|
+
if (pageAction !== undefined) {
|
|
583
|
+
return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: pageAction }];
|
|
584
|
+
}
|
|
585
|
+
return [{ type: 'observed-key', msg, route: 'unhandled' }];
|
|
586
|
+
}
|
|
587
|
+
function resolveRoutedKeyLayer(msg, model) {
|
|
588
|
+
const context = resolveLayerContext(model);
|
|
589
|
+
const runtimeStack = describeFrameRuntimeViewStack(model, {
|
|
590
|
+
pageModalOpen: context.pageModalOpen,
|
|
591
|
+
});
|
|
592
|
+
return routeRuntimeInput(runtimeStack, EMPTY_RUNTIME_LAYOUTS, { kind: 'key', key: msg.key }, ({ layer }) => {
|
|
593
|
+
const frameLayer = layer.model;
|
|
594
|
+
if (frameLayer == null)
|
|
595
|
+
return undefined;
|
|
596
|
+
if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette') {
|
|
597
|
+
return { handled: true, commands: handlePaletteLayerKeyCommands(msg, frameLayer.kind) };
|
|
598
|
+
}
|
|
599
|
+
if (frameLayer.kind === 'help') {
|
|
600
|
+
return { handled: true, commands: handleHelpLayerKeyCommands(msg) };
|
|
601
|
+
}
|
|
602
|
+
if (frameLayer.kind === 'settings') {
|
|
603
|
+
const cmds = handleSettingsLayerKeyCommands(msg, model);
|
|
604
|
+
return cmds != null ? { handled: true, commands: cmds } : { bubble: true };
|
|
605
|
+
}
|
|
606
|
+
if (frameLayer.kind === 'notification-center') {
|
|
607
|
+
const cmds = handleNotificationCenterLayerKeyCommands(msg, model);
|
|
608
|
+
return cmds != null ? { handled: true, commands: cmds } : { bubble: true };
|
|
609
|
+
}
|
|
610
|
+
if (frameLayer.kind === 'quit-confirm') {
|
|
611
|
+
const obs = { type: 'observed-key', msg, route: 'frame' };
|
|
612
|
+
if (isShellQuitConfirmAccept(msg)) {
|
|
613
|
+
return { handled: true, commands: [obs, { type: 'close-quit-confirm' }, { type: 'quit' }] };
|
|
614
|
+
}
|
|
615
|
+
if (isShellQuitConfirmDismiss(msg)) {
|
|
616
|
+
return { handled: true, commands: [obs, { type: 'close-quit-confirm' }] };
|
|
617
|
+
}
|
|
618
|
+
return { handled: true, commands: [obs] };
|
|
619
|
+
}
|
|
620
|
+
if (frameLayer.kind === 'page-modal') {
|
|
621
|
+
const { modalKeyMap } = context;
|
|
622
|
+
const obs = { type: 'observed-key', msg, route: 'page' };
|
|
623
|
+
if (modalKeyMap != null) {
|
|
624
|
+
const modalAction = modalKeyMap.handle(msg);
|
|
625
|
+
if (modalAction !== undefined) {
|
|
626
|
+
return { handled: true, commands: [obs, { type: 'emit-page-msg', pageId: model.activePageId, msg: modalAction }] };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return { handled: true, commands: [obs] };
|
|
630
|
+
}
|
|
631
|
+
// workspace (root layer)
|
|
632
|
+
return { handled: true, commands: handleWorkspaceLayerKeyCommands(msg, model) };
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function createShellRetainedLayoutNode(id, rect, children) {
|
|
636
|
+
return {
|
|
637
|
+
id,
|
|
638
|
+
rect: {
|
|
639
|
+
x: rect.col,
|
|
640
|
+
y: rect.row,
|
|
641
|
+
width: rect.width,
|
|
642
|
+
height: rect.height,
|
|
643
|
+
},
|
|
644
|
+
children: children ?? [],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function resolveWorkspacePaneRects(model) {
|
|
648
|
+
const bodyRect = resolveBodyRect(model, options);
|
|
649
|
+
const maxState = model.maximizedPaneByPage[model.activePageId];
|
|
650
|
+
const maximizedPaneId = maxState?.maximizedPaneId;
|
|
651
|
+
const renderResult = maximizedPaneId
|
|
652
|
+
? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
|
|
653
|
+
: renderPageContent(model.activePageId, model, bodyRect, pagesById);
|
|
654
|
+
return renderResult.paneRects;
|
|
655
|
+
}
|
|
656
|
+
function buildWorkspaceLayoutTree(model) {
|
|
657
|
+
const header = resolveHeaderLine(model, options, pagesById);
|
|
658
|
+
const tabChildren = header.tabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
|
|
659
|
+
row: 0,
|
|
660
|
+
col: target.startCol,
|
|
661
|
+
width: target.endCol - target.startCol + 1,
|
|
662
|
+
height: 1,
|
|
663
|
+
}));
|
|
664
|
+
const bodyRect = resolveBodyRect(model, options);
|
|
665
|
+
const paneRects = resolveWorkspacePaneRects(model);
|
|
666
|
+
const paneChildren = [];
|
|
667
|
+
for (const [paneId, rect] of paneRects.entries()) {
|
|
668
|
+
paneChildren.push(createShellRetainedLayoutNode(`pane:${paneId}`, rect));
|
|
669
|
+
}
|
|
670
|
+
return createShellRetainedLayoutNode('workspace', { row: 0, col: 0, width: model.columns, height: model.rows }, [
|
|
671
|
+
createShellRetainedLayoutNode('header-bar', { row: 0, col: 0, width: model.columns, height: 1 }, tabChildren),
|
|
672
|
+
createShellRetainedLayoutNode('workspace-body', bodyRect, paneChildren),
|
|
673
|
+
]);
|
|
674
|
+
}
|
|
675
|
+
function buildSettingsRowChildren(model, layout) {
|
|
676
|
+
const scrollY = clampSettingsScroll(model, layout);
|
|
677
|
+
const viewportTop = 1;
|
|
678
|
+
const viewportBottom = model.rows - 1;
|
|
679
|
+
const children = [];
|
|
680
|
+
for (const flatRow of layout.rows) {
|
|
681
|
+
const screenRow = flatRow.line - scrollY + viewportTop;
|
|
682
|
+
const clippedTop = Math.max(viewportTop, screenRow);
|
|
683
|
+
const clippedBottom = Math.min(viewportBottom, screenRow + flatRow.height);
|
|
684
|
+
if (clippedTop >= clippedBottom)
|
|
685
|
+
continue;
|
|
686
|
+
children.push(createShellRetainedLayoutNode(`settings-row:${flatRow.index}`, {
|
|
687
|
+
row: clippedTop,
|
|
688
|
+
col: layout.startCol,
|
|
689
|
+
width: layout.drawerWidth,
|
|
690
|
+
height: clippedBottom - clippedTop,
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
return children;
|
|
694
|
+
}
|
|
695
|
+
function resolveFrameMouseRuntimeLayouts(model) {
|
|
696
|
+
let layouts = EMPTY_RUNTIME_LAYOUTS;
|
|
697
|
+
const settingsLayout = model.settingsOpen ? resolveSettingsLayout(model, options, pagesById) : undefined;
|
|
698
|
+
if (settingsLayout != null) {
|
|
699
|
+
layouts = retainRuntimeLayout(layouts, {
|
|
700
|
+
viewId: 'settings',
|
|
701
|
+
tree: createShellRetainedLayoutNode('settings-drawer', {
|
|
702
|
+
row: 0,
|
|
703
|
+
col: settingsLayout.startCol,
|
|
704
|
+
width: settingsLayout.drawerWidth,
|
|
705
|
+
height: model.rows,
|
|
706
|
+
}, buildSettingsRowChildren(model, settingsLayout)),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const notificationCenterLayout = model.notificationCenterOpen ? resolveNotificationCenterLayout(model, options, pagesById) : undefined;
|
|
710
|
+
if (notificationCenterLayout != null) {
|
|
711
|
+
layouts = retainRuntimeLayout(layouts, {
|
|
712
|
+
viewId: 'notification-center',
|
|
713
|
+
tree: createShellRetainedLayoutNode('notification-center-drawer', {
|
|
714
|
+
row: 0,
|
|
715
|
+
col: notificationCenterLayout.startCol,
|
|
716
|
+
width: notificationCenterLayout.drawerWidth,
|
|
717
|
+
height: model.rows,
|
|
718
|
+
}),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
layouts = retainRuntimeLayout(layouts, {
|
|
722
|
+
viewId: 'workspace',
|
|
723
|
+
tree: buildWorkspaceLayoutTree(model),
|
|
724
|
+
});
|
|
725
|
+
return layouts;
|
|
726
|
+
}
|
|
727
|
+
function resolveRoutedMouseLayer(msg, model) {
|
|
728
|
+
const context = resolveLayerContext(model);
|
|
729
|
+
const { activePageModel, inputAreas } = context;
|
|
730
|
+
const runtimeStack = describeFrameRuntimeViewStack(model, {
|
|
731
|
+
pageModalOpen: context.pageModalOpen,
|
|
732
|
+
});
|
|
733
|
+
return routeRuntimeInput(runtimeStack, resolveFrameMouseRuntimeLayouts(model), {
|
|
734
|
+
kind: 'pointer',
|
|
735
|
+
action: msg.action,
|
|
736
|
+
x: msg.col,
|
|
737
|
+
y: msg.row,
|
|
738
|
+
button: msg.button === 'none' ? undefined : msg.button,
|
|
739
|
+
}, ({ layer, hit }) => {
|
|
740
|
+
const frameLayer = layer.model;
|
|
741
|
+
if (frameLayer == null)
|
|
742
|
+
return undefined;
|
|
743
|
+
const cmds = [];
|
|
744
|
+
if (frameLayer.kind === 'help') {
|
|
745
|
+
if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
|
|
746
|
+
cmds.push({ type: 'help-scroll', action: msg.action === 'scroll-down' ? 'down' : 'up' });
|
|
747
|
+
}
|
|
748
|
+
return { handled: true, commands: cmds };
|
|
749
|
+
}
|
|
750
|
+
if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette'
|
|
751
|
+
|| frameLayer.kind === 'quit-confirm' || frameLayer.kind === 'page-modal') {
|
|
752
|
+
return { handled: true };
|
|
753
|
+
}
|
|
754
|
+
if (frameLayer.kind === 'settings') {
|
|
755
|
+
if (hit == null)
|
|
756
|
+
return { handled: true };
|
|
757
|
+
if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
|
|
758
|
+
cmds.push({ type: 'settings-scroll', delta: msg.action === 'scroll-down' ? 3 : -3 });
|
|
759
|
+
return { handled: true, commands: cmds };
|
|
760
|
+
}
|
|
761
|
+
if (msg.action === 'press' && msg.button === 'left') {
|
|
762
|
+
const rowNode = hit.path.find((n) => n.id?.startsWith('settings-row:'));
|
|
763
|
+
if (rowNode != null) {
|
|
764
|
+
const rowIndex = parseInt(rowNode.id.slice('settings-row:'.length), 10);
|
|
765
|
+
cmds.push({ type: 'activate-settings-row', rowIndex });
|
|
766
|
+
}
|
|
767
|
+
return { handled: true, commands: cmds };
|
|
768
|
+
}
|
|
769
|
+
return { handled: true };
|
|
770
|
+
}
|
|
771
|
+
if (frameLayer.kind === 'notification-center') {
|
|
772
|
+
if (hit == null)
|
|
773
|
+
return { handled: true };
|
|
774
|
+
if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
|
|
775
|
+
cmds.push({ type: 'notification-center-scroll', delta: msg.action === 'scroll-down' ? 3 : -3 });
|
|
776
|
+
return { handled: true, commands: cmds };
|
|
777
|
+
}
|
|
778
|
+
return { handled: true };
|
|
779
|
+
}
|
|
780
|
+
// workspace layer
|
|
781
|
+
if (msg.action === 'press' && msg.button === 'left') {
|
|
782
|
+
// notification toast hit-testing (outside retained layouts)
|
|
783
|
+
if (frameNotificationOptions.enabled) {
|
|
784
|
+
const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
|
|
785
|
+
screenWidth: model.columns,
|
|
786
|
+
screenHeight: model.rows,
|
|
787
|
+
margin: frameNotificationOptions.margin,
|
|
788
|
+
gap: frameNotificationOptions.gap,
|
|
789
|
+
ctx: resolveSafeCtx() ?? undefined,
|
|
790
|
+
}, msg.col, msg.row);
|
|
791
|
+
if (notificationTarget?.kind === 'dismiss') {
|
|
792
|
+
cmds.push({ type: 'dismiss-notification', notificationId: notificationTarget.item.id });
|
|
793
|
+
return { handled: true, commands: cmds };
|
|
794
|
+
}
|
|
795
|
+
if (notificationTarget != null) {
|
|
796
|
+
return { handled: true };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// tab click
|
|
800
|
+
const tabNode = hit?.path.find((n) => n.id?.startsWith('tab:'));
|
|
801
|
+
if (tabNode != null) {
|
|
802
|
+
const pageId = tabNode.id.slice('tab:'.length);
|
|
803
|
+
const currentIndex = model.pageOrder.indexOf(model.activePageId);
|
|
804
|
+
const nextIndex = model.pageOrder.indexOf(pageId);
|
|
805
|
+
if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
|
|
806
|
+
cmds.push({ type: 'switch-tab', delta: nextIndex - currentIndex });
|
|
807
|
+
}
|
|
808
|
+
return { handled: true, commands: cmds };
|
|
809
|
+
}
|
|
810
|
+
if (msg.row === 0) {
|
|
811
|
+
return { handled: true };
|
|
812
|
+
}
|
|
813
|
+
// pane click
|
|
814
|
+
const clickedPaneNode = hit?.path.find((n) => n.id?.startsWith('pane:'));
|
|
815
|
+
if (clickedPaneNode != null) {
|
|
816
|
+
const paneId = clickedPaneNode.id.slice('pane:'.length);
|
|
817
|
+
const paneRects = resolveWorkspacePaneRects(model);
|
|
818
|
+
const paneRect = paneRects.get(paneId);
|
|
819
|
+
if (paneRect != null) {
|
|
820
|
+
cmds.push({ type: 'focus-pane', paneId });
|
|
821
|
+
const inputArea = findInputAreaByPaneId(inputAreas, paneId);
|
|
822
|
+
const areaMsg = inputArea?.mouse?.({ msg, model: activePageModel, rect: paneRect });
|
|
823
|
+
cmds.push({
|
|
824
|
+
type: 'emit-page-msg',
|
|
825
|
+
pageId: model.activePageId,
|
|
826
|
+
msg: areaMsg !== undefined ? areaMsg : msg,
|
|
827
|
+
});
|
|
828
|
+
return { handled: true, commands: cmds };
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
|
|
833
|
+
const scrollPaneNode = hit?.path.find((n) => n.id?.startsWith('pane:'));
|
|
834
|
+
if (scrollPaneNode != null) {
|
|
835
|
+
const paneId = scrollPaneNode.id.slice('pane:'.length);
|
|
836
|
+
const paneRects = resolveWorkspacePaneRects(model);
|
|
837
|
+
const paneRect = paneRects.get(paneId);
|
|
838
|
+
if (paneRect != null) {
|
|
839
|
+
cmds.push({ type: 'focus-pane', paneId });
|
|
840
|
+
const inputArea = findInputAreaByPaneId(inputAreas, paneId);
|
|
841
|
+
const areaMsg = inputArea?.mouse?.({ msg, model: activePageModel, rect: paneRect });
|
|
842
|
+
if (areaMsg !== undefined) {
|
|
843
|
+
cmds.push({ type: 'emit-page-msg', pageId: model.activePageId, msg: areaMsg });
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
cmds.push({ type: 'scroll-focused-pane', direction: msg.action === 'scroll-down' ? 'down' : 'up' });
|
|
847
|
+
}
|
|
848
|
+
return { handled: true, commands: cmds };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return { handled: true };
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
function resolveWorkspaceHelpSource(activePage, activeInputArea) {
|
|
856
|
+
return mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
|
|
857
|
+
}
|
|
858
|
+
function resolveWorkspaceHintSource(model, activePage, activeInputArea) {
|
|
859
|
+
const helpLineOverride = options.helpLineSource?.({
|
|
860
|
+
model,
|
|
861
|
+
activePage,
|
|
862
|
+
frameKeys,
|
|
863
|
+
globalKeys: options.globalKeys,
|
|
864
|
+
});
|
|
865
|
+
if (typeof helpLineOverride === 'string') {
|
|
866
|
+
return helpLineOverride;
|
|
867
|
+
}
|
|
868
|
+
return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
|
|
869
|
+
}
|
|
870
|
+
function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
|
|
871
|
+
const settings = resolveFrameSettings(model, options, pagesById);
|
|
872
|
+
const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
|
|
873
|
+
const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
|
|
874
|
+
const workspaceHelpSource = resolveWorkspaceHelpSource(activePage, activeInputArea);
|
|
875
|
+
const paletteHint = frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close');
|
|
876
|
+
const helpHint = frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close');
|
|
877
|
+
const settingsHint = frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit');
|
|
878
|
+
const notificationsHint = frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit');
|
|
879
|
+
const quitHint = frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay');
|
|
880
|
+
const paletteTitle = model.commandPaletteTitle
|
|
881
|
+
?? frameMessage(options.i18n, 'palette.title', 'Command Palette');
|
|
882
|
+
const searchTitle = model.commandPaletteTitle
|
|
883
|
+
?? activePage.searchTitle
|
|
884
|
+
?? frameMessage(options.i18n, 'search.title', 'Search');
|
|
885
|
+
const notificationsTitle = notificationCenter == null
|
|
886
|
+
? frameMessage(options.i18n, 'notifications.title', 'Notifications')
|
|
887
|
+
: `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`;
|
|
888
|
+
return {
|
|
889
|
+
workspace: {
|
|
890
|
+
title: activePage.title,
|
|
891
|
+
hintSource: workspaceHintSource,
|
|
892
|
+
helpSource: workspaceHelpSource,
|
|
893
|
+
},
|
|
894
|
+
'page-modal': {
|
|
895
|
+
title: activePage.title,
|
|
896
|
+
hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
|
|
897
|
+
helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
|
|
898
|
+
},
|
|
899
|
+
settings: {
|
|
900
|
+
title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
|
|
901
|
+
hintSource: settingsHint,
|
|
902
|
+
helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
|
|
903
|
+
},
|
|
904
|
+
help: {
|
|
905
|
+
title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
|
|
906
|
+
hintSource: helpHint,
|
|
907
|
+
helpSource: helpLayerHelpKeys,
|
|
908
|
+
},
|
|
909
|
+
'notification-center': {
|
|
910
|
+
title: notificationsTitle,
|
|
911
|
+
hintSource: notificationsHint,
|
|
912
|
+
helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
|
|
913
|
+
},
|
|
914
|
+
search: {
|
|
915
|
+
title: searchTitle,
|
|
916
|
+
hintSource: paletteHint,
|
|
917
|
+
helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
|
|
918
|
+
},
|
|
919
|
+
'command-palette': {
|
|
920
|
+
title: paletteTitle,
|
|
921
|
+
hintSource: paletteHint,
|
|
922
|
+
helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
|
|
923
|
+
},
|
|
924
|
+
'quit-confirm': {
|
|
925
|
+
title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
|
|
926
|
+
hintSource: quitHint,
|
|
927
|
+
helpSource: quitConfirmHelpKeys,
|
|
928
|
+
},
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function resolvePresentedLayerContext(model) {
|
|
932
|
+
const { activePage, activePageModel, inputAreas, activeInputArea, modalKeyMap, pageModalOpen, } = resolveLayerContext(model);
|
|
933
|
+
const layerStack = describeFrameLayerStack(model, {
|
|
934
|
+
pageModalOpen,
|
|
935
|
+
layers: resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap),
|
|
936
|
+
});
|
|
937
|
+
const activeLayer = layerStack[layerStack.length - 1];
|
|
938
|
+
const underlyingLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
|
|
939
|
+
return {
|
|
940
|
+
activePage,
|
|
941
|
+
activePageModel,
|
|
942
|
+
inputAreas,
|
|
943
|
+
activeInputArea,
|
|
944
|
+
modalKeyMap,
|
|
945
|
+
pageModalOpen,
|
|
946
|
+
layerStack,
|
|
947
|
+
activeLayer,
|
|
948
|
+
underlyingLayer,
|
|
949
|
+
};
|
|
95
950
|
}
|
|
96
951
|
function updateTargetPage(model, targetPageId, targetMsg) {
|
|
97
952
|
const targetPage = pagesById.get(targetPageId);
|
|
@@ -99,60 +954,12 @@ export function createFramedApp(options) {
|
|
|
99
954
|
return [model, []];
|
|
100
955
|
const pageModel = model.pageModels[targetPageId];
|
|
101
956
|
const updateResult = targetPage.update(targetMsg, pageModel);
|
|
102
|
-
|
|
103
|
-
let cmds = [];
|
|
104
|
-
if (updateResult !== undefined && updateResult !== null) {
|
|
105
|
-
if (Array.isArray(updateResult)) {
|
|
106
|
-
nextPageModel = (updateResult[0] ?? pageModel);
|
|
107
|
-
cmds = (updateResult[1] ?? []);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
nextPageModel = updateResult;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
957
|
+
const [nextPageModel, cmds = []] = updateResult;
|
|
113
958
|
const nextModels = { ...model.pageModels, [targetPageId]: nextPageModel };
|
|
114
959
|
const synced = syncPageFrameState({ ...model, pageModels: nextModels }, targetPageId, pagesById);
|
|
115
|
-
const wrappedCmds =
|
|
116
|
-
? cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd))
|
|
117
|
-
: [];
|
|
960
|
+
const wrappedCmds = cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd));
|
|
118
961
|
return [synced, wrappedCmds];
|
|
119
962
|
}
|
|
120
|
-
function handleFrameMouse(msg, model) {
|
|
121
|
-
if (model.helpOpen || model.commandPalette != null)
|
|
122
|
-
return [model, []];
|
|
123
|
-
if (msg.action !== 'press' || msg.button !== 'left')
|
|
124
|
-
return undefined;
|
|
125
|
-
if (frameNotificationOptions.enabled) {
|
|
126
|
-
const nowMs = resolveClock(resolveSafeCtx()).now();
|
|
127
|
-
const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
|
|
128
|
-
screenWidth: model.columns,
|
|
129
|
-
screenHeight: model.rows,
|
|
130
|
-
margin: frameNotificationOptions.margin,
|
|
131
|
-
gap: frameNotificationOptions.gap,
|
|
132
|
-
ctx: resolveSafeCtx() ?? undefined,
|
|
133
|
-
}, msg.col, msg.row);
|
|
134
|
-
if (notificationTarget?.kind === 'dismiss') {
|
|
135
|
-
return applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, notificationTarget.item.id, nowMs), nowMs);
|
|
136
|
-
}
|
|
137
|
-
if (notificationTarget != null) {
|
|
138
|
-
return [model, []];
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (msg.row === 0) {
|
|
142
|
-
const header = resolveHeaderLine(model, options, pagesById);
|
|
143
|
-
const tab = header.tabTargets.find((target) => msg.col >= target.startCol && msg.col <= target.endCol);
|
|
144
|
-
if (tab != null) {
|
|
145
|
-
const currentIndex = model.pageOrder.indexOf(model.activePageId);
|
|
146
|
-
const nextIndex = model.pageOrder.indexOf(tab.pageId);
|
|
147
|
-
if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
|
|
148
|
-
return switchTab(model, nextIndex - currentIndex, pagesById, options);
|
|
149
|
-
}
|
|
150
|
-
return [model, []];
|
|
151
|
-
}
|
|
152
|
-
return [model, []];
|
|
153
|
-
}
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
963
|
function applyFrameNotificationState(model, notifications, nowMs, forceTick = false) {
|
|
157
964
|
const trimmed = trimNotificationsToViewport(notifications, {
|
|
158
965
|
screenWidth: model.columns,
|
|
@@ -171,6 +978,32 @@ export function createFramedApp(options) {
|
|
|
171
978
|
}
|
|
172
979
|
return [nextModel, []];
|
|
173
980
|
}
|
|
981
|
+
function activateSettingsRow(model, row) {
|
|
982
|
+
if (row.action === undefined || row.enabled === false || row.kind === 'info') {
|
|
983
|
+
return [model, []];
|
|
984
|
+
}
|
|
985
|
+
const cmds = [emitMsgForPage(model.activePageId, row.action)];
|
|
986
|
+
if (!frameNotificationOptions.enabled) {
|
|
987
|
+
return [model, cmds];
|
|
988
|
+
}
|
|
989
|
+
const feedback = row.feedback ?? {
|
|
990
|
+
title: 'Setting updated',
|
|
991
|
+
message: `${row.label} updated.`,
|
|
992
|
+
};
|
|
993
|
+
const nowMs = resolveClock(resolveSafeCtx()).now();
|
|
994
|
+
const notifications = pushNotification(model.runtimeNotifications, {
|
|
995
|
+
title: feedback.title ?? 'Setting updated',
|
|
996
|
+
message: feedback.message,
|
|
997
|
+
variant: 'TOAST',
|
|
998
|
+
tone: feedback.tone ?? 'INFO',
|
|
999
|
+
width: SETTINGS_FEEDBACK_TOAST_WIDTH,
|
|
1000
|
+
placement: frameNotificationOptions.placement,
|
|
1001
|
+
durationMs: feedback.durationMs ?? 2_500,
|
|
1002
|
+
overflow: frameNotificationOptions.overflow,
|
|
1003
|
+
}, nowMs);
|
|
1004
|
+
const [nextModel, notificationCmds] = applyFrameNotificationState(model, notifications, nowMs);
|
|
1005
|
+
return [nextModel, [...cmds, ...notificationCmds]];
|
|
1006
|
+
}
|
|
174
1007
|
const app = {
|
|
175
1008
|
init() {
|
|
176
1009
|
const pageModels = {};
|
|
@@ -189,6 +1022,14 @@ export function createFramedApp(options) {
|
|
|
189
1022
|
columns: Math.max(1, options.initialColumns ?? 80),
|
|
190
1023
|
rows: Math.max(1, options.initialRows ?? 24),
|
|
191
1024
|
helpOpen: false,
|
|
1025
|
+
helpScrollY: 0,
|
|
1026
|
+
commandPaletteKind: undefined,
|
|
1027
|
+
settingsOpen: false,
|
|
1028
|
+
notificationCenterOpen: false,
|
|
1029
|
+
quitConfirmOpen: false,
|
|
1030
|
+
settingsFocusIndex: 0,
|
|
1031
|
+
settingsScrollY: 0,
|
|
1032
|
+
notificationCenterScrollY: 0,
|
|
192
1033
|
transitionProgress: 1,
|
|
193
1034
|
transitionGeneration: 0,
|
|
194
1035
|
transitionFrame: 0,
|
|
@@ -197,6 +1038,7 @@ export function createFramedApp(options) {
|
|
|
197
1038
|
dockStateByPage: {},
|
|
198
1039
|
splitRatioOverrides: {},
|
|
199
1040
|
runtimeNotifications: createNotificationState(),
|
|
1041
|
+
runtimeNotificationHistoryFilter: 'ALL',
|
|
200
1042
|
runtimeNotificationLoopActive: false,
|
|
201
1043
|
};
|
|
202
1044
|
for (const pageId of pageOrder) {
|
|
@@ -297,95 +1139,55 @@ export function createFramedApp(options) {
|
|
|
297
1139
|
}, []];
|
|
298
1140
|
}
|
|
299
1141
|
if (isKeyMsg(msg)) {
|
|
300
|
-
|
|
301
|
-
const [nextModel, cmds] = handlePaletteKey(msg, model, paletteKeys, options, pagesById);
|
|
302
|
-
return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
|
|
303
|
-
}
|
|
304
|
-
// Help acts as a modal layer when open: only close keys are handled.
|
|
305
|
-
if (model.helpOpen) {
|
|
306
|
-
if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === '?')) {
|
|
307
|
-
return [{ ...model, helpOpen: false }, withObservedKey(model, [], msg, 'help')];
|
|
308
|
-
}
|
|
309
|
-
return [model, withObservedKey(model, [], msg, 'help')];
|
|
310
|
-
}
|
|
311
|
-
const activePage = pagesById.get(model.activePageId);
|
|
312
|
-
const activePageModel = model.pageModels[model.activePageId];
|
|
313
|
-
const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
|
|
314
|
-
if (modalKeyMap != null) {
|
|
315
|
-
const modalAction = modalKeyMap.handle(msg);
|
|
316
|
-
if (modalAction !== undefined) {
|
|
317
|
-
return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, modalAction)], msg, 'page')];
|
|
318
|
-
}
|
|
319
|
-
return [model, withObservedKey(model, [], msg, 'page')];
|
|
320
|
-
}
|
|
321
|
-
const pageAction = activePage.keyMap?.handle(msg);
|
|
322
|
-
const globalAction = options.globalKeys?.handle(msg);
|
|
323
|
-
const frameAction = frameKeys.handle(msg);
|
|
324
|
-
const keyPriority = options.keyPriority ?? 'frame-first';
|
|
325
|
-
if (keyPriority === 'page-first') {
|
|
326
|
-
if (pageAction !== undefined) {
|
|
327
|
-
return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
|
|
328
|
-
}
|
|
329
|
-
if (globalAction !== undefined) {
|
|
330
|
-
return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
|
|
331
|
-
}
|
|
332
|
-
if (frameAction !== undefined) {
|
|
333
|
-
if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
|
|
334
|
-
return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
|
|
335
|
-
}
|
|
336
|
-
const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
|
|
337
|
-
return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
|
|
338
|
-
}
|
|
339
|
-
return [model, withObservedKey(model, [], msg, 'unhandled')];
|
|
340
|
-
}
|
|
341
|
-
if (frameAction !== undefined) {
|
|
342
|
-
// Handle palette opening here since applyFrameAction doesn't have access to palette deps
|
|
343
|
-
if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
|
|
344
|
-
return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
|
|
345
|
-
}
|
|
346
|
-
const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
|
|
347
|
-
return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
|
|
348
|
-
}
|
|
349
|
-
if (globalAction !== undefined) {
|
|
350
|
-
return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
|
|
351
|
-
}
|
|
352
|
-
if (pageAction !== undefined) {
|
|
353
|
-
return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
|
|
354
|
-
}
|
|
355
|
-
return [model, withObservedKey(model, [], msg, 'unhandled')];
|
|
1142
|
+
return drainShellCommandBuffer(model, resolveRoutedKeyLayer(msg, model));
|
|
356
1143
|
}
|
|
357
1144
|
if (isMouseMsg(msg)) {
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
return
|
|
1145
|
+
const mouseRouteResult = resolveRoutedMouseLayer(msg, model);
|
|
1146
|
+
if (mouseRouteResult.handled) {
|
|
1147
|
+
return drainShellCommandBuffer(model, mouseRouteResult);
|
|
1148
|
+
}
|
|
361
1149
|
return updateTargetPage(model, model.activePageId, msg);
|
|
362
1150
|
}
|
|
363
1151
|
// Custom message path: route to originating page when command messages are scoped.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return updateTargetPage(model,
|
|
1152
|
+
if (isPageScopedMsg(msg)) {
|
|
1153
|
+
return updateTargetPage(model, msg.pageId, msg.msg);
|
|
1154
|
+
}
|
|
1155
|
+
return updateTargetPage(model, model.activePageId, msg);
|
|
368
1156
|
},
|
|
369
1157
|
view(model) {
|
|
370
|
-
const activePage =
|
|
1158
|
+
const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
|
|
371
1159
|
const header = resolveHeaderLine(model, options, pagesById).surface;
|
|
372
|
-
const helpLine = renderHelpLine(model,
|
|
373
|
-
const bodyRect =
|
|
1160
|
+
const helpLine = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById));
|
|
1161
|
+
const bodyRect = resolveBodyRect(model, options);
|
|
374
1162
|
// Check for maximized pane — if set, render only that pane at full body rect
|
|
375
1163
|
const maxState = model.maximizedPaneByPage[model.activePageId];
|
|
376
1164
|
const maximizedPaneId = maxState?.maximizedPaneId;
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
1165
|
+
const frameSurface = getComposedFrameScratch(model.columns, model.rows);
|
|
1166
|
+
frameSurface.clear();
|
|
1167
|
+
frameSurface.blit(header, 0, 0);
|
|
1168
|
+
if (model.rows > 1) {
|
|
1169
|
+
frameSurface.blit(helpLine, 0, model.rows - 1);
|
|
1170
|
+
}
|
|
1171
|
+
let activeResult;
|
|
1172
|
+
let bodySurface;
|
|
381
1173
|
const activeTransition = model.activeTransition ?? options.transition;
|
|
382
1174
|
if (model.previousPageId != null && model.transitionProgress < 1 && activeTransition && activeTransition !== 'none') {
|
|
1175
|
+
const activeBodyResult = maximizedPaneId
|
|
1176
|
+
? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
|
|
1177
|
+
: renderPageContent(model.activePageId, model, bodyRect, pagesById);
|
|
1178
|
+
activeResult = activeBodyResult;
|
|
1179
|
+
bodySurface = activeBodyResult.surface;
|
|
383
1180
|
const ctx = resolveSafeCtx();
|
|
384
1181
|
if (ctx) {
|
|
385
1182
|
const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById);
|
|
386
|
-
bodySurface = renderTransition(prevResult.surface,
|
|
1183
|
+
bodySurface = renderTransition(prevResult.surface, activeBodyResult.surface, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
|
|
387
1184
|
}
|
|
388
1185
|
}
|
|
1186
|
+
else {
|
|
1187
|
+
activeResult = maximizedPaneId
|
|
1188
|
+
? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface)
|
|
1189
|
+
: renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface);
|
|
1190
|
+
}
|
|
389
1191
|
const overlays = [];
|
|
390
1192
|
if (options.overlayFactory != null) {
|
|
391
1193
|
overlays.push(...options.overlayFactory({
|
|
@@ -405,12 +1207,31 @@ export function createFramedApp(options) {
|
|
|
405
1207
|
ctx: ctx ?? undefined,
|
|
406
1208
|
}));
|
|
407
1209
|
}
|
|
1210
|
+
if (model.settingsOpen) {
|
|
1211
|
+
const settingsLayer = layerStack.find((layer) => layer.kind === 'settings');
|
|
1212
|
+
const settingsOverlay = renderSettingsDrawer(model, options, pagesById, settingsLayer?.title);
|
|
1213
|
+
if (settingsOverlay != null) {
|
|
1214
|
+
overlays.push(settingsOverlay);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (model.notificationCenterOpen) {
|
|
1218
|
+
const notificationLayer = layerStack.find((layer) => layer.kind === 'notification-center');
|
|
1219
|
+
const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title);
|
|
1220
|
+
if (notificationCenterOverlay != null) {
|
|
1221
|
+
overlays.push(notificationCenterOverlay);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
408
1224
|
if (model.helpOpen) {
|
|
409
|
-
const
|
|
1225
|
+
const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
|
|
410
1226
|
overlays.push(modal({
|
|
411
|
-
title:
|
|
412
|
-
|
|
413
|
-
|
|
1227
|
+
title: activeLayer.kind === 'help'
|
|
1228
|
+
? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
|
|
1229
|
+
: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
|
|
1230
|
+
body: helpOverlay.body,
|
|
1231
|
+
hint: typeof activeLayer.hintSource === 'string'
|
|
1232
|
+
? activeLayer.hintSource
|
|
1233
|
+
: frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
|
|
1234
|
+
width: helpOverlay.body.width + 4,
|
|
414
1235
|
screenWidth: model.columns,
|
|
415
1236
|
screenHeight: model.rows,
|
|
416
1237
|
}));
|
|
@@ -418,25 +1239,27 @@ export function createFramedApp(options) {
|
|
|
418
1239
|
if (model.commandPalette != null) {
|
|
419
1240
|
const paletteWidth = Math.max(20, Math.min(80, model.columns - 4));
|
|
420
1241
|
const paletteBody = commandPalette(model.commandPalette, { width: Math.max(16, paletteWidth - 4) });
|
|
1242
|
+
const paletteLayer = activeLayer.kind === 'search' || activeLayer.kind === 'command-palette'
|
|
1243
|
+
? activeLayer
|
|
1244
|
+
: undefined;
|
|
421
1245
|
overlays.push(modal({
|
|
422
|
-
title: 'Command Palette',
|
|
1246
|
+
title: paletteLayer?.title ?? model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
|
|
423
1247
|
body: paletteBody,
|
|
424
|
-
hint:
|
|
1248
|
+
hint: typeof paletteLayer?.hintSource === 'string'
|
|
1249
|
+
? paletteLayer.hintSource
|
|
1250
|
+
: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
|
|
425
1251
|
width: paletteWidth,
|
|
426
1252
|
screenWidth: model.columns,
|
|
427
1253
|
screenHeight: model.rows,
|
|
428
1254
|
}));
|
|
429
1255
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
overlays,
|
|
438
|
-
dimBackground: overlays.length > 0,
|
|
439
|
-
});
|
|
1256
|
+
if (model.quitConfirmOpen) {
|
|
1257
|
+
overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n));
|
|
1258
|
+
}
|
|
1259
|
+
if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
|
|
1260
|
+
frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
|
|
1261
|
+
}
|
|
1262
|
+
return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: overlays.length > 0 });
|
|
440
1263
|
},
|
|
441
1264
|
routeRuntimeIssue(issue) {
|
|
442
1265
|
if (!frameNotificationOptions.enabled)
|
|
@@ -446,15 +1269,440 @@ export function createFramedApp(options) {
|
|
|
446
1269
|
};
|
|
447
1270
|
return app;
|
|
448
1271
|
}
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
1272
|
+
function focusPane(model, paneId) {
|
|
1273
|
+
if (model.focusedPaneByPage[model.activePageId] === paneId)
|
|
1274
|
+
return model;
|
|
1275
|
+
return {
|
|
1276
|
+
...model,
|
|
1277
|
+
focusedPaneByPage: {
|
|
1278
|
+
...model.focusedPaneByPage,
|
|
1279
|
+
[model.activePageId]: paneId,
|
|
1280
|
+
},
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function resolveBodyRect(model, options) {
|
|
1284
|
+
return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
|
|
1285
|
+
}
|
|
1286
|
+
function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById) {
|
|
1287
|
+
const activePageModel = model.pageModels[model.activePageId];
|
|
1288
|
+
const activeInputArea = findInputAreaByPaneId(resolveInputAreas(activePage, activePageModel), model.focusedPaneByPage[model.activePageId]);
|
|
1289
|
+
const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
|
|
1290
|
+
const settings = resolveFrameSettings(model, options, pagesById);
|
|
1291
|
+
const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
|
|
1292
|
+
const workspaceHintSource = options.helpLineSource?.({
|
|
1293
|
+
model,
|
|
1294
|
+
activePage,
|
|
1295
|
+
frameKeys,
|
|
1296
|
+
globalKeys: options.globalKeys,
|
|
1297
|
+
});
|
|
1298
|
+
const workspaceHelpSource = mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
|
|
1299
|
+
const layerStack = describeFrameLayerStack(model, {
|
|
1300
|
+
pageModalOpen: modalKeyMap != null,
|
|
1301
|
+
layers: {
|
|
1302
|
+
workspace: {
|
|
1303
|
+
title: activePage.title,
|
|
1304
|
+
hintSource: typeof workspaceHintSource === 'string'
|
|
1305
|
+
? workspaceHintSource
|
|
1306
|
+
: workspaceHintSource ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap),
|
|
1307
|
+
helpSource: workspaceHelpSource,
|
|
1308
|
+
},
|
|
1309
|
+
'page-modal': {
|
|
1310
|
+
title: activePage.title,
|
|
1311
|
+
hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
|
|
1312
|
+
helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
|
|
1313
|
+
},
|
|
1314
|
+
settings: {
|
|
1315
|
+
title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
|
|
1316
|
+
hintSource: frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit'),
|
|
1317
|
+
helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
|
|
1318
|
+
},
|
|
1319
|
+
help: {
|
|
1320
|
+
title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
|
|
1321
|
+
hintSource: frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
|
|
1322
|
+
helpSource: helpLayerHelpKeys,
|
|
1323
|
+
},
|
|
1324
|
+
'notification-center': {
|
|
1325
|
+
title: notificationCenter == null
|
|
1326
|
+
? frameMessage(options.i18n, 'notifications.title', 'Notifications')
|
|
1327
|
+
: `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`,
|
|
1328
|
+
hintSource: frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit'),
|
|
1329
|
+
helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
|
|
1330
|
+
},
|
|
1331
|
+
search: {
|
|
1332
|
+
title: model.commandPaletteTitle ?? activePage.searchTitle ?? frameMessage(options.i18n, 'search.title', 'Search'),
|
|
1333
|
+
hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
|
|
1334
|
+
helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
|
|
1335
|
+
},
|
|
1336
|
+
'command-palette': {
|
|
1337
|
+
title: model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
|
|
1338
|
+
hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
|
|
1339
|
+
helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
|
|
1340
|
+
},
|
|
1341
|
+
'quit-confirm': {
|
|
1342
|
+
title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
|
|
1343
|
+
hintSource: frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay'),
|
|
1344
|
+
helpSource: quitConfirmHelpKeys,
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
});
|
|
1348
|
+
const beneathHelpLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
|
|
1349
|
+
const source = beneathHelpLayer?.helpSource ?? workspaceHelpSource;
|
|
1350
|
+
const maxDialogWidth = Math.max(28, Math.min(model.columns - 4, 88));
|
|
1351
|
+
const bodyWidth = Math.max(20, maxDialogWidth - 4);
|
|
1352
|
+
const helpSurface = helpViewSurface(source, {
|
|
1353
|
+
title: undefined,
|
|
1354
|
+
width: bodyWidth,
|
|
1355
|
+
});
|
|
1356
|
+
const pagerHeight = Math.max(4, Math.min(helpSurface.height + 1, Math.max(4, model.rows - 8)));
|
|
1357
|
+
const pagerState = createPagerStateForSurface(helpSurface, {
|
|
1358
|
+
width: bodyWidth,
|
|
1359
|
+
height: pagerHeight,
|
|
1360
|
+
});
|
|
1361
|
+
const scrollY = Math.max(0, Math.min(model.helpScrollY, pagerState.scroll.maxY));
|
|
1362
|
+
const scrolledState = {
|
|
1363
|
+
...pagerState,
|
|
1364
|
+
scroll: {
|
|
1365
|
+
...pagerState.scroll,
|
|
1366
|
+
y: scrollY,
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
return {
|
|
1370
|
+
body: pagerSurface(helpSurface, scrolledState, { showScrollbar: true, showStatus: true }),
|
|
1371
|
+
maxScrollY: pagerState.scroll.maxY,
|
|
1372
|
+
scrollY,
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function isHelpScrollAction(action) {
|
|
1376
|
+
return action.type === 'scroll-up'
|
|
1377
|
+
|| action.type === 'scroll-down'
|
|
1378
|
+
|| action.type === 'page-up'
|
|
1379
|
+
|| action.type === 'page-down'
|
|
1380
|
+
|| action.type === 'top'
|
|
1381
|
+
|| action.type === 'bottom';
|
|
1382
|
+
}
|
|
1383
|
+
function resolveFrameSettings(model, options, pagesById) {
|
|
1384
|
+
const activePage = pagesById.get(model.activePageId);
|
|
1385
|
+
return options.settings?.({
|
|
1386
|
+
model,
|
|
1387
|
+
activePage,
|
|
1388
|
+
pageModel: model.pageModels[model.activePageId],
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
function resolveFrameNotificationCenter(model, options, pagesById) {
|
|
1392
|
+
const activePage = pagesById.get(model.activePageId);
|
|
1393
|
+
const pageModel = model.pageModels[model.activePageId];
|
|
1394
|
+
const provided = options.notificationCenter?.({
|
|
1395
|
+
model,
|
|
1396
|
+
activePage,
|
|
1397
|
+
pageModel,
|
|
1398
|
+
runtimeNotifications: model.runtimeNotifications,
|
|
1399
|
+
});
|
|
1400
|
+
if (provided != null) {
|
|
1401
|
+
const filters = provided.filters != null && provided.filters.length > 0
|
|
1402
|
+
? provided.filters
|
|
1403
|
+
: DEFAULT_NOTIFICATION_CENTER_FILTERS;
|
|
1404
|
+
const activeFilter = filters.includes(provided.activeFilter ?? 'ALL')
|
|
1405
|
+
? (provided.activeFilter ?? 'ALL')
|
|
1406
|
+
: filters[0];
|
|
1407
|
+
return {
|
|
1408
|
+
title: provided.title ?? frameMessage(options.i18n, 'notifications.title', 'Notifications'),
|
|
1409
|
+
state: provided.state,
|
|
1410
|
+
filters,
|
|
1411
|
+
activeFilter,
|
|
1412
|
+
onFilterChange: provided.onFilterChange,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
if (options.runtimeNotifications === false)
|
|
1416
|
+
return undefined;
|
|
1417
|
+
return {
|
|
1418
|
+
title: frameMessage(options.i18n, 'notifications.title', 'Notifications'),
|
|
1419
|
+
state: model.runtimeNotifications,
|
|
1420
|
+
filters: DEFAULT_NOTIFICATION_CENTER_FILTERS,
|
|
1421
|
+
activeFilter: model.runtimeNotificationHistoryFilter,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function resolveSettingsLayout(model, options, pagesById) {
|
|
1425
|
+
const settings = resolveFrameSettings(model, options, pagesById);
|
|
1426
|
+
if (settings == null)
|
|
1427
|
+
return undefined;
|
|
1428
|
+
const sections = settings.sections.filter((section) => section.rows.length > 0);
|
|
1429
|
+
if (sections.length === 0)
|
|
1430
|
+
return undefined;
|
|
1431
|
+
const drawerWidth = resolveSettingsDrawerWidth(model.columns);
|
|
1432
|
+
const anchor = frameStartAnchor(options.i18n);
|
|
1433
|
+
const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
|
|
1434
|
+
const contentWidth = Math.max(16, drawerWidth - 4);
|
|
1435
|
+
const preferenceSections = preparePreferenceSections(toPreferenceSections(sections));
|
|
1436
|
+
const rows = [];
|
|
1437
|
+
let line = 0;
|
|
1438
|
+
for (let sectionIndex = 0; sectionIndex < preferenceSections.length; sectionIndex++) {
|
|
1439
|
+
const section = preferenceSections[sectionIndex];
|
|
1440
|
+
if (sectionIndex > 0) {
|
|
1441
|
+
line += 1;
|
|
1442
|
+
}
|
|
1443
|
+
line += 1;
|
|
1444
|
+
line += 1;
|
|
1445
|
+
for (let rowIndex = 0; rowIndex < section.rows.length; rowIndex++) {
|
|
1446
|
+
const preparedRow = section.rows[rowIndex];
|
|
1447
|
+
const row = sections[sectionIndex].rows[rowIndex];
|
|
1448
|
+
const rowLayout = resolvePreferenceRowLayout(preparedRow, contentWidth);
|
|
1449
|
+
rows.push({
|
|
1450
|
+
index: rows.length,
|
|
1451
|
+
line,
|
|
1452
|
+
height: rowLayout.height,
|
|
1453
|
+
row,
|
|
1454
|
+
});
|
|
1455
|
+
line += rowLayout.height;
|
|
1456
|
+
if (rowIndex < section.rows.length - 1) {
|
|
1457
|
+
line += 1;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const contentHeight = Math.max(1, model.rows - 2);
|
|
1462
|
+
const totalLines = Math.max(1, line);
|
|
1463
|
+
const maxScrollY = Math.max(0, totalLines - contentHeight);
|
|
1464
|
+
return {
|
|
1465
|
+
settings: {
|
|
1466
|
+
...settings,
|
|
1467
|
+
sections,
|
|
1468
|
+
},
|
|
1469
|
+
preferenceSections,
|
|
1470
|
+
rows,
|
|
1471
|
+
anchor,
|
|
1472
|
+
startCol,
|
|
1473
|
+
drawerWidth,
|
|
1474
|
+
contentWidth,
|
|
1475
|
+
contentHeight,
|
|
1476
|
+
totalLines,
|
|
1477
|
+
maxScrollY,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function resolveNotificationCenterDrawerWidth(columns) {
|
|
1481
|
+
const boundedColumns = Math.max(28, columns);
|
|
1482
|
+
return Math.min(Math.max(32, Math.floor(boundedColumns * 0.34)), Math.max(32, boundedColumns - 4), 52);
|
|
1483
|
+
}
|
|
1484
|
+
function resolveNotificationCenterLayout(model, options, pagesById) {
|
|
1485
|
+
const center = resolveFrameNotificationCenter(model, options, pagesById);
|
|
1486
|
+
if (center == null)
|
|
1487
|
+
return undefined;
|
|
1488
|
+
const drawerWidth = resolveNotificationCenterDrawerWidth(model.columns);
|
|
1489
|
+
const anchor = frameEndAnchor(options.i18n);
|
|
1490
|
+
const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
|
|
1491
|
+
const contentWidth = Math.max(18, drawerWidth - 4);
|
|
1492
|
+
const content = renderNotificationCenterSurface(center, contentWidth, options.i18n);
|
|
1493
|
+
const contentHeight = Math.max(1, model.rows - 2);
|
|
1494
|
+
const pagerState = createPagerStateForSurface(content, {
|
|
1495
|
+
width: contentWidth,
|
|
1496
|
+
height: contentHeight,
|
|
1497
|
+
});
|
|
1498
|
+
return {
|
|
1499
|
+
center,
|
|
1500
|
+
anchor,
|
|
1501
|
+
startCol,
|
|
1502
|
+
drawerWidth,
|
|
1503
|
+
contentWidth,
|
|
1504
|
+
contentHeight,
|
|
1505
|
+
content,
|
|
1506
|
+
maxScrollY: pagerState.scroll.maxY,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function resolveSettingsDrawerWidth(columns) {
|
|
1510
|
+
const boundedColumns = Math.max(24, columns);
|
|
1511
|
+
return Math.min(Math.max(28, Math.floor(boundedColumns * 0.3)), Math.max(28, boundedColumns - 4), 42);
|
|
1512
|
+
}
|
|
1513
|
+
function clampSettingsFocus(model, layout) {
|
|
1514
|
+
if (layout.rows.length === 0)
|
|
1515
|
+
return 0;
|
|
1516
|
+
return Math.max(0, Math.min(model.settingsFocusIndex, layout.rows.length - 1));
|
|
1517
|
+
}
|
|
1518
|
+
function clampSettingsScroll(model, layout) {
|
|
1519
|
+
return Math.max(0, Math.min(model.settingsScrollY, layout.maxScrollY));
|
|
1520
|
+
}
|
|
1521
|
+
function resolveInputAreas(page, pageModel) {
|
|
1522
|
+
return page.inputAreas?.(pageModel) ?? [];
|
|
1523
|
+
}
|
|
1524
|
+
function findInputAreaByPaneId(inputAreas, paneId) {
|
|
1525
|
+
if (paneId == null)
|
|
1526
|
+
return undefined;
|
|
1527
|
+
return inputAreas.find((area) => area.paneId === paneId);
|
|
1528
|
+
}
|
|
1529
|
+
function ensureSettingsRangeVisible(startLine, height, scrollY, visibleLines, maxScrollY) {
|
|
1530
|
+
let next = scrollY;
|
|
1531
|
+
const endLine = startLine + Math.max(1, height) - 1;
|
|
1532
|
+
if (startLine < next) {
|
|
1533
|
+
next = startLine;
|
|
454
1534
|
}
|
|
455
|
-
if (
|
|
456
|
-
|
|
1535
|
+
else if (endLine >= next + visibleLines) {
|
|
1536
|
+
next = endLine - visibleLines + 1;
|
|
1537
|
+
}
|
|
1538
|
+
return Math.max(0, Math.min(next, maxScrollY));
|
|
1539
|
+
}
|
|
1540
|
+
function moveSettingsFocus(model, layout, delta) {
|
|
1541
|
+
if (layout.rows.length === 0)
|
|
1542
|
+
return model;
|
|
1543
|
+
const nextFocus = Math.max(0, Math.min(clampSettingsFocus(model, layout) + delta, layout.rows.length - 1));
|
|
1544
|
+
const focusedRow = layout.rows[nextFocus];
|
|
1545
|
+
return {
|
|
1546
|
+
...model,
|
|
1547
|
+
settingsFocusIndex: nextFocus,
|
|
1548
|
+
settingsScrollY: ensureSettingsRangeVisible(focusedRow.line, focusedRow.height, clampSettingsScroll(model, layout), layout.contentHeight, layout.maxScrollY),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function scrollSettingsBy(model, layout, delta) {
|
|
1552
|
+
return {
|
|
1553
|
+
...model,
|
|
1554
|
+
settingsScrollY: Math.max(0, Math.min(clampSettingsScroll(model, layout) + delta, layout.maxScrollY)),
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function scrollNotificationCenterBy(model, layout, delta) {
|
|
1558
|
+
return {
|
|
1559
|
+
...model,
|
|
1560
|
+
notificationCenterScrollY: Math.max(0, Math.min(model.notificationCenterScrollY + delta, layout.maxScrollY)),
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
function cycleNotificationCenterFilter(model, layout) {
|
|
1564
|
+
const filters = layout.center.filters;
|
|
1565
|
+
if (filters.length < 2)
|
|
1566
|
+
return [model, []];
|
|
1567
|
+
const currentIndex = Math.max(0, filters.indexOf(layout.center.activeFilter));
|
|
1568
|
+
const nextFilter = filters[(currentIndex + 1) % filters.length];
|
|
1569
|
+
if (layout.center.onFilterChange != null) {
|
|
1570
|
+
const action = layout.center.onFilterChange(nextFilter);
|
|
1571
|
+
return [{
|
|
1572
|
+
...model,
|
|
1573
|
+
notificationCenterScrollY: 0,
|
|
1574
|
+
}, action === undefined ? [] : [emitMsgForPage(model.activePageId, action)]];
|
|
1575
|
+
}
|
|
1576
|
+
return [{
|
|
1577
|
+
...model,
|
|
1578
|
+
runtimeNotificationHistoryFilter: nextFilter,
|
|
1579
|
+
notificationCenterScrollY: 0,
|
|
1580
|
+
}, []];
|
|
1581
|
+
}
|
|
1582
|
+
function renderSettingsDrawer(model, options, pagesById, titleOverride) {
|
|
1583
|
+
const layout = resolveSettingsLayout(model, options, pagesById);
|
|
1584
|
+
if (layout == null)
|
|
1585
|
+
return undefined;
|
|
1586
|
+
const scrollY = clampSettingsScroll(model, layout);
|
|
1587
|
+
const content = renderSettingsSurface(layout, model);
|
|
1588
|
+
const pagerState = createPagerStateForSurface(content, {
|
|
1589
|
+
width: layout.contentWidth,
|
|
1590
|
+
height: layout.contentHeight,
|
|
1591
|
+
});
|
|
1592
|
+
const scrolledState = {
|
|
1593
|
+
...pagerState,
|
|
1594
|
+
scroll: {
|
|
1595
|
+
...pagerState.scroll,
|
|
1596
|
+
y: scrollY,
|
|
1597
|
+
},
|
|
1598
|
+
};
|
|
1599
|
+
const body = pagerSurface(content, scrolledState, {
|
|
1600
|
+
showScrollbar: layout.maxScrollY > 0,
|
|
1601
|
+
showStatus: false,
|
|
1602
|
+
});
|
|
1603
|
+
return drawer({
|
|
1604
|
+
anchor: layout.anchor,
|
|
1605
|
+
title: titleOverride ?? layout.settings.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
|
|
1606
|
+
content: body,
|
|
1607
|
+
borderToken: layout.settings.borderToken,
|
|
1608
|
+
bgToken: layout.settings.bgToken,
|
|
1609
|
+
ctx: resolveSafeCtx() ?? undefined,
|
|
1610
|
+
width: layout.drawerWidth,
|
|
1611
|
+
screenWidth: model.columns,
|
|
1612
|
+
screenHeight: model.rows,
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
function renderNotificationCenterDrawer(model, options, pagesById, titleOverride) {
|
|
1616
|
+
const layout = resolveNotificationCenterLayout(model, options, pagesById);
|
|
1617
|
+
if (layout == null)
|
|
1618
|
+
return undefined;
|
|
1619
|
+
const pagerState = createPagerStateForSurface(layout.content, {
|
|
1620
|
+
width: layout.contentWidth,
|
|
1621
|
+
height: layout.contentHeight,
|
|
1622
|
+
});
|
|
1623
|
+
const scrolledState = {
|
|
1624
|
+
...pagerState,
|
|
1625
|
+
scroll: {
|
|
1626
|
+
...pagerState.scroll,
|
|
1627
|
+
y: Math.max(0, Math.min(model.notificationCenterScrollY, layout.maxScrollY)),
|
|
1628
|
+
},
|
|
1629
|
+
};
|
|
1630
|
+
const body = pagerSurface(layout.content, scrolledState, {
|
|
1631
|
+
showScrollbar: layout.maxScrollY > 0,
|
|
1632
|
+
showStatus: false,
|
|
1633
|
+
});
|
|
1634
|
+
return drawer({
|
|
1635
|
+
anchor: layout.anchor,
|
|
1636
|
+
title: titleOverride ?? `${layout.center.title} • ${frameNotificationFilterLabel(options.i18n, layout.center.activeFilter)}`,
|
|
1637
|
+
content: body,
|
|
1638
|
+
width: layout.drawerWidth,
|
|
1639
|
+
screenWidth: model.columns,
|
|
1640
|
+
screenHeight: model.rows,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
function renderSettingsSurface(layout, model) {
|
|
1644
|
+
const focusedIndex = clampSettingsFocus(model, layout);
|
|
1645
|
+
return preferenceListSurface(layout.preferenceSections, {
|
|
1646
|
+
width: layout.contentWidth,
|
|
1647
|
+
selectedRowId: layout.rows[focusedIndex]?.row.id,
|
|
1648
|
+
ctx: resolveSafeCtx() ?? undefined,
|
|
1649
|
+
theme: layout.settings.listTheme,
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
function toPreferenceSections(sections) {
|
|
1653
|
+
return sections.map((section) => ({
|
|
1654
|
+
id: section.id,
|
|
1655
|
+
title: section.title,
|
|
1656
|
+
rows: section.rows.map((row) => toPreferenceRow(row)),
|
|
1657
|
+
}));
|
|
1658
|
+
}
|
|
1659
|
+
function toPreferenceRow(row) {
|
|
1660
|
+
return {
|
|
1661
|
+
id: row.id,
|
|
1662
|
+
label: row.label,
|
|
1663
|
+
description: row.description,
|
|
1664
|
+
valueLabel: row.valueLabel,
|
|
1665
|
+
kind: row.kind,
|
|
1666
|
+
checked: row.checked,
|
|
1667
|
+
enabled: row.enabled,
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function resolveNotificationFooterCue(model, options, pagesById) {
|
|
1671
|
+
const center = resolveFrameNotificationCenter(model, options, pagesById);
|
|
1672
|
+
if (center == null)
|
|
1673
|
+
return undefined;
|
|
1674
|
+
const liveCount = center.state.items.length;
|
|
1675
|
+
const archivedCount = countNotificationHistory(center.state, center.activeFilter);
|
|
1676
|
+
return frameNotificationCue(options.i18n, liveCount, archivedCount);
|
|
1677
|
+
}
|
|
1678
|
+
function renderNotificationCenterSurface(center, width, i18n) {
|
|
1679
|
+
const ctx = resolveSafeCtx() ?? undefined;
|
|
1680
|
+
const rows = [
|
|
1681
|
+
insetLineSurface(`Live: ${center.state.items.length} • Archived: ${center.state.history.length}`, width),
|
|
1682
|
+
insetLineSurface(`Filter: ${frameNotificationFilterLabel(i18n, center.activeFilter)}`, width),
|
|
1683
|
+
];
|
|
1684
|
+
const liveItems = [...center.state.items].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
|
|
1685
|
+
if (liveItems.length > 0) {
|
|
1686
|
+
rows.push(createSurface(width, 1));
|
|
1687
|
+
rows.push(insetLineSurface(ctx == null ? 'Current stack' : ctx.style.bold('Current stack'), width));
|
|
1688
|
+
rows.push(createSurface(width, 1));
|
|
1689
|
+
for (let index = 0; index < liveItems.length; index++) {
|
|
1690
|
+
rows.push(renderNotificationReviewEntrySurface(liveItems[index], {
|
|
1691
|
+
width,
|
|
1692
|
+
ctx,
|
|
1693
|
+
metaLabel: `${liveItems[index].variant} • live`,
|
|
1694
|
+
}));
|
|
1695
|
+
if (index < liveItems.length - 1)
|
|
1696
|
+
rows.push(createSurface(width, 1));
|
|
1697
|
+
}
|
|
457
1698
|
}
|
|
458
|
-
|
|
1699
|
+
rows.push(createSurface(width, 1));
|
|
1700
|
+
rows.push(renderNotificationHistorySurface(center.state, {
|
|
1701
|
+
width,
|
|
1702
|
+
height: Number.MAX_SAFE_INTEGER,
|
|
1703
|
+
filter: center.activeFilter,
|
|
1704
|
+
ctx,
|
|
1705
|
+
}));
|
|
1706
|
+
return vstackSurface(...rows);
|
|
459
1707
|
}
|
|
460
1708
|
//# sourceMappingURL=app-frame.js.map
|