gloomberb 0.14.0 → 0.14.1
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/package.json +1 -1
- package/src/api-client/types.ts +17 -0
- package/src/app/global-shortcuts.test.tsx +85 -1
- package/src/app/global-shortcuts.ts +83 -36
- package/src/app/keybindings/actions.ts +223 -0
- package/src/app/keybindings/capture-request.ts +36 -0
- package/src/app/keybindings/chord.test.ts +131 -0
- package/src/app/keybindings/chord.ts +302 -0
- package/src/app/keybindings/config.ts +83 -0
- package/src/app/keybindings/index.ts +62 -0
- package/src/app/keybindings/labels.ts +87 -0
- package/src/app/keybindings/react.tsx +32 -0
- package/src/app/keybindings/resolve.test.ts +103 -0
- package/src/app/keybindings/resolve.ts +280 -0
- package/src/app.tsx +25 -17
- package/src/cli/commands/keybindings.test.ts +48 -0
- package/src/cli/commands/keybindings.ts +102 -0
- package/src/cli/commands/system.ts +33 -1
- package/src/cli/context.ts +1 -0
- package/src/cli/pane-functions/intraday-shot.test.ts +15 -0
- package/src/cli/restore-plugins.ts +2 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +19 -2
- package/src/components/command-bar/routes/root/results.ts +33 -0
- package/src/components/command-bar/routes/root/runtime.ts +4 -0
- package/src/components/command-bar/routing/launch-request.ts +3 -0
- package/src/components/command-bar/surface/index.test.tsx +58 -0
- package/src/components/command-bar/surface/index.tsx +22 -0
- package/src/components/data-table/sections.ts +55 -0
- package/src/components/index.ts +11 -0
- package/src/components/layout/header.tsx +11 -4
- package/src/components/layout/pane/sidebar.test.tsx +87 -0
- package/src/components/layout/pane/sidebar.tsx +118 -6
- package/src/components/layout/shell/index.tsx +8 -3
- package/src/components/layout/shell/menu.ts +12 -11
- package/src/components/layout/shell/pane/management-shortcuts.ts +4 -2
- package/src/components/layout/shell/shortcuts.ts +55 -35
- package/src/components/ui/action-row.tsx +20 -3
- package/src/components/ui/button.tsx +8 -0
- package/src/core/app-runtime.test.ts +31 -0
- package/src/core/app-runtime.ts +32 -2
- package/src/core/app-service-ports.ts +8 -0
- package/src/core/app-services.ts +2 -2
- package/src/core/state/app/state.ts +4 -0
- package/src/core/state/app/types.ts +23 -3
- package/src/data/config/home.test.ts +70 -0
- package/src/data/config/home.ts +41 -0
- package/src/data/config/store/index.test.ts +25 -0
- package/src/data/config/store/node.ts +21 -19
- package/src/data/config/store/normalize.ts +41 -0
- package/src/i18n/es.ts +1 -1
- package/src/i18n/ja.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/i18n/zh-cn.ts +1 -1
- package/src/i18n/zh-tw.ts +1 -1
- package/src/plugins/builtin/chart-composer/headless.ts +12 -3
- package/src/plugins/builtin/chat/channels.ts +4 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +24 -16
- package/src/plugins/builtin/chat/content/composer-runtime.ts +5 -5
- package/src/plugins/builtin/chat/content/index.tsx +13 -10
- package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +0 -3
- package/src/plugins/builtin/chat/sidebar-store.ts +100 -0
- package/src/plugins/builtin/chat/sidebar.test.tsx +49 -19
- package/src/plugins/builtin/chat/sidebar.tsx +111 -78
- package/src/plugins/builtin/chat/test-harness.tsx +3 -0
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/help/functions-table.tsx +158 -0
- package/src/plugins/builtin/help/index.tsx +142 -301
- package/src/plugins/builtin/help/keybindings-editor.test.tsx +147 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +427 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -35
- package/src/plugins/builtin/help/shortcut-table.tsx +90 -0
- package/src/plugins/builtin/help/table-cells.tsx +66 -0
- package/src/plugins/builtin/jobs/headless.ts +13 -0
- package/src/plugins/builtin/jobs/pane.tsx +11 -1
- package/src/plugins/builtin/research/analyst-model.test.ts +68 -2
- package/src/plugins/builtin/research/analyst-model.ts +180 -17
- package/src/plugins/builtin/research/analyst-pane.test.tsx +56 -13
- package/src/plugins/builtin/research/analyst-pane.tsx +123 -29
- package/src/plugins/builtin/research/index.test.ts +0 -35
- package/src/plugins/catalog-backend.ts +12 -2
- package/src/plugins/loader.ts +3 -3
- package/src/renderers/browser/app-services.ts +2 -2
- package/src/renderers/electrobun/bun/application-menu/index.ts +14 -7
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +1 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -4
- package/src/renderers/electrobun/bun/index.ts +12 -1
- package/src/renderers/electrobun/view/app-services.ts +2 -2
- package/src/renderers/electrobun/view/desktop/controls.tsx +3 -2
- package/src/renderers/electrobun/view/host/media-surface.tsx +3 -0
- package/src/renderers/electrobun/view/test-utils.tsx +17 -8
- package/src/renderers/opentui/host.tsx +6 -15
- package/src/renderers/opentui/start.tsx +2 -5
- package/src/renderers/opentui/terminal-media.test.ts +78 -7
- package/src/renderers/opentui/terminal-media.ts +123 -31
- package/src/time-series/resolution.ts +28 -0
- package/src/time-series/resolve.test.ts +173 -0
- package/src/time-series/resolve.ts +84 -29
- package/src/time-series/session-history.ts +5 -2
- package/src/time-series/use-chart-resolution.ts +7 -0
- package/src/types/config.ts +22 -0
- package/src/ui/host.tsx +6 -4
- package/src/updater.test.ts +46 -0
- package/src/updater.ts +42 -5
- package/src/utils/shortcut-labels.ts +0 -14
package/package.json
CHANGED
package/src/api-client/types.ts
CHANGED
|
@@ -535,6 +535,10 @@ export interface CloudJobsSeriesPoint {
|
|
|
535
535
|
open: number;
|
|
536
536
|
new: number;
|
|
537
537
|
closed: number;
|
|
538
|
+
/** Open standing roles the company hires for continuously. */
|
|
539
|
+
evergreen?: number;
|
|
540
|
+
/** Open contract and temporary roles. */
|
|
541
|
+
contract?: number;
|
|
538
542
|
}
|
|
539
543
|
|
|
540
544
|
export interface CloudJobsBucket {
|
|
@@ -554,6 +558,9 @@ export interface CloudJobsPosting {
|
|
|
554
558
|
location: string | null;
|
|
555
559
|
country: string | null;
|
|
556
560
|
remote: boolean | null;
|
|
561
|
+
employmentKind?: string | null;
|
|
562
|
+
/** A standing posting for a continuous-hiring role; null when unjudged. */
|
|
563
|
+
evergreen?: boolean | null;
|
|
557
564
|
postedAt: string | null;
|
|
558
565
|
postedPrecision: "exact" | "approximate" | "floor" | null;
|
|
559
566
|
firstSeenAt: string;
|
|
@@ -595,6 +602,16 @@ export interface CloudJobsSummaryPayload {
|
|
|
595
602
|
/** Open postings the system dates within the last 30 days. */
|
|
596
603
|
posted30d?: number | null;
|
|
597
604
|
remoteShare: number | null;
|
|
605
|
+
/**
|
|
606
|
+
* Open roles that are standing postings for continuous-hiring positions
|
|
607
|
+
* (baristas, servers, store associates, nurses). One more of those says
|
|
608
|
+
* nothing about the company's plans; open minus evergreen is the count of
|
|
609
|
+
* specific roles.
|
|
610
|
+
*/
|
|
611
|
+
evergreenCount?: number;
|
|
612
|
+
evergreenShare?: number | null;
|
|
613
|
+
/** Share of open roles that are contract or temporary, among those known. */
|
|
614
|
+
contractShare?: number | null;
|
|
598
615
|
medianAgeDays: number | null;
|
|
599
616
|
series: CloudJobsSeriesPoint[];
|
|
600
617
|
/** The current backlog by how long each role has been posted. */
|
|
@@ -4,8 +4,9 @@ import { act } from "react";
|
|
|
4
4
|
import type { PluginRegistry } from "../plugins/registry";
|
|
5
5
|
import { TestDialogProvider, createOpenTuiTestRoot as createRoot, emitKeypress as emitTuiKeypress, type TestKeyEvent } from "../renderers/opentui/test-utils";
|
|
6
6
|
import { createInitialState, type AppAction, type AppState } from "../state/app/context";
|
|
7
|
-
import { cloneLayout, createDefaultConfig } from "../types/config";
|
|
7
|
+
import { cloneLayout, createDefaultConfig, type KeybindingsConfig } from "../types/config";
|
|
8
8
|
import { useAppGlobalShortcuts } from "./global-shortcuts";
|
|
9
|
+
import { resolveKeybindings } from "./keybindings";
|
|
9
10
|
|
|
10
11
|
let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
|
|
11
12
|
let root: ReturnType<typeof createRoot> | undefined;
|
|
@@ -63,6 +64,7 @@ function ShortcutHarness({
|
|
|
63
64
|
dispatch,
|
|
64
65
|
focusedTickerSymbol,
|
|
65
66
|
isDetachedWindow: false,
|
|
67
|
+
keybindings: resolveKeybindings(state.config.keybindings),
|
|
66
68
|
pluginRegistry,
|
|
67
69
|
refreshTicker,
|
|
68
70
|
startUpdate: () => {},
|
|
@@ -71,6 +73,12 @@ function ShortcutHarness({
|
|
|
71
73
|
return <text>ready</text>;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function stateWithKeybindings(suffix: string, keybindings: KeybindingsConfig, options: Partial<AppState> = {}): AppState {
|
|
77
|
+
const config = createDefaultConfig(`/tmp/gloomberb-global-shortcuts-${suffix}`);
|
|
78
|
+
config.keybindings = keybindings;
|
|
79
|
+
return { ...createInitialState(config), ...options };
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
async function renderHarness(
|
|
75
83
|
state: AppState,
|
|
76
84
|
registry: PluginRegistry,
|
|
@@ -152,6 +160,82 @@ describe("useAppGlobalShortcuts", () => {
|
|
|
152
160
|
expect(event.propagationStopped).toBe(true);
|
|
153
161
|
});
|
|
154
162
|
|
|
163
|
+
test("a rebound action answers to its new key and not the old one", async () => {
|
|
164
|
+
const actions: AppAction[] = [];
|
|
165
|
+
const state = stateWithKeybindings("rebound", {
|
|
166
|
+
actions: { "ticker-search": "Ctrl+Shift+S", "command-bar": "Ctrl+Shift+P", help: null },
|
|
167
|
+
});
|
|
168
|
+
const openedPanes: string[] = [];
|
|
169
|
+
await renderHarness(state, createRegistry(undefined, (paneId) => openedPanes.push(paneId)), (action) => actions.push(action));
|
|
170
|
+
|
|
171
|
+
const oldKey = await emitKeypress({ name: "`" });
|
|
172
|
+
expect(actions).toEqual([]);
|
|
173
|
+
expect(oldKey.defaultPrevented).toBe(false);
|
|
174
|
+
|
|
175
|
+
await emitKeypress({ name: "s", ctrl: true, shift: true });
|
|
176
|
+
expect(actions).toEqual([{
|
|
177
|
+
type: "SET_COMMAND_BAR",
|
|
178
|
+
open: true,
|
|
179
|
+
query: "",
|
|
180
|
+
launch: { kind: "ticker-search", query: "" },
|
|
181
|
+
}]);
|
|
182
|
+
|
|
183
|
+
await emitKeypress({ name: "p", ctrl: true });
|
|
184
|
+
expect(actions).toHaveLength(1);
|
|
185
|
+
await emitKeypress({ name: "p", ctrl: true, shift: true });
|
|
186
|
+
expect(actions[1]).toEqual({ type: "TOGGLE_COMMAND_BAR" });
|
|
187
|
+
|
|
188
|
+
const help = await emitKeypress({ name: "?", shift: true });
|
|
189
|
+
expect(openedPanes).toEqual([]);
|
|
190
|
+
expect(help.defaultPrevented).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a command binding opens the bar with the text to run, unless the bar or a dialog owns the keyboard", async () => {
|
|
194
|
+
const actions: AppAction[] = [];
|
|
195
|
+
const state = stateWithKeybindings("command", { commands: { "Alt+1": "DES AAPL" } });
|
|
196
|
+
await renderHarness(state, createRegistry(), (action) => actions.push(action));
|
|
197
|
+
|
|
198
|
+
const event = await emitKeypress({ name: "1", alt: true });
|
|
199
|
+
|
|
200
|
+
expect(actions).toEqual([{
|
|
201
|
+
type: "SET_COMMAND_BAR",
|
|
202
|
+
open: true,
|
|
203
|
+
query: "DES AAPL",
|
|
204
|
+
launch: { kind: "run-query", query: "DES AAPL" },
|
|
205
|
+
}]);
|
|
206
|
+
expect(event.defaultPrevented).toBe(true);
|
|
207
|
+
expect(event.propagationStopped).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a Control command binding yields to the editor while text is being typed", async () => {
|
|
211
|
+
const actions: AppAction[] = [];
|
|
212
|
+
const state = stateWithKeybindings("command-typing", { commands: { "Ctrl+N": "NEWS", F5: "NEWS" } }, { inputCaptured: true });
|
|
213
|
+
await renderHarness(state, createRegistry(), (action) => actions.push(action));
|
|
214
|
+
|
|
215
|
+
const ctrl = await emitKeypress({ name: "n", ctrl: true });
|
|
216
|
+
expect(actions).toEqual([]);
|
|
217
|
+
expect(ctrl.defaultPrevented).toBe(false);
|
|
218
|
+
|
|
219
|
+
await emitKeypress({ name: "f5" });
|
|
220
|
+
expect(actions).toEqual([{
|
|
221
|
+
type: "SET_COMMAND_BAR",
|
|
222
|
+
open: true,
|
|
223
|
+
query: "NEWS",
|
|
224
|
+
launch: { kind: "run-query", query: "NEWS" },
|
|
225
|
+
}]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("a plugin shortcut honours its override", async () => {
|
|
229
|
+
let executed = 0;
|
|
230
|
+
const state = stateWithKeybindings("plugin-override", { actions: { "plugin:test-shortcut": "Alt+X" } });
|
|
231
|
+
await renderHarness(state, createRegistry(() => { executed += 1; }), () => {});
|
|
232
|
+
|
|
233
|
+
await emitKeypress({ name: "x" });
|
|
234
|
+
expect(executed).toBe(0);
|
|
235
|
+
await emitKeypress({ name: "x", alt: true });
|
|
236
|
+
expect(executed).toBe(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
155
239
|
function layoutState(suffix: string, options: { commandBarOpen?: boolean } = {}) {
|
|
156
240
|
const config = createDefaultConfig(`/tmp/gloomberb-global-shortcuts-${suffix}`);
|
|
157
241
|
config.layouts = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useEffect, useRef, type Dispatch } from "react";
|
|
2
2
|
import { useShortcut } from "../react/input";
|
|
3
3
|
import { useNativeRenderer, useRendererHost } from "../ui";
|
|
4
4
|
import { useDialogState } from "../ui/dialog";
|
|
@@ -14,11 +14,49 @@ import {
|
|
|
14
14
|
isPasteShortcut,
|
|
15
15
|
pasteSystemClipboard,
|
|
16
16
|
} from "../utils/selection-clipboard";
|
|
17
|
+
import {
|
|
18
|
+
describeKeybindingIssue,
|
|
19
|
+
getDefaultKeybindings,
|
|
20
|
+
matchKeybinding,
|
|
21
|
+
matchesKeyChord,
|
|
22
|
+
resolvePluginShortcutChords,
|
|
23
|
+
type ResolvedKeybindings,
|
|
24
|
+
} from "./keybindings";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tells the user once per launch when `config.json` holds a binding that does
|
|
28
|
+
* not parse, names no action, or lands on a key another action already has.
|
|
29
|
+
* Silence would read as "I set it and nothing happened".
|
|
30
|
+
*/
|
|
31
|
+
function useKeybindingIssueNotice(
|
|
32
|
+
keybindings: ResolvedKeybindings,
|
|
33
|
+
pluginRegistry: PluginRegistry,
|
|
34
|
+
initialized: boolean,
|
|
35
|
+
) {
|
|
36
|
+
const noticedRef = useRef<string | null>(null);
|
|
37
|
+
const issueKey = keybindings.issues.map(describeKeybindingIssue).join("\n");
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!initialized || !issueKey || noticedRef.current === issueKey) return;
|
|
40
|
+
noticedRef.current = issueKey;
|
|
41
|
+
const lines = keybindings.issues.map(describeKeybindingIssue);
|
|
42
|
+
const shown = lines.slice(0, 3);
|
|
43
|
+
const more = lines.length - shown.length;
|
|
44
|
+
pluginRegistry.notify({
|
|
45
|
+
body: [
|
|
46
|
+
"Keybindings need attention:",
|
|
47
|
+
...shown,
|
|
48
|
+
...(more > 0 ? [`and ${more} more; see Help > Shortcuts.`] : []),
|
|
49
|
+
].join("\n"),
|
|
50
|
+
type: "error",
|
|
51
|
+
});
|
|
52
|
+
}, [initialized, issueKey, keybindings.issues, pluginRegistry]);
|
|
53
|
+
}
|
|
17
54
|
|
|
18
55
|
export function useAppGlobalShortcuts({
|
|
19
56
|
dispatch,
|
|
20
57
|
focusedTickerSymbol,
|
|
21
58
|
isDetachedWindow,
|
|
59
|
+
keybindings = getDefaultKeybindings(),
|
|
22
60
|
pluginRegistry,
|
|
23
61
|
refreshTicker,
|
|
24
62
|
startUpdate,
|
|
@@ -27,6 +65,8 @@ export function useAppGlobalShortcuts({
|
|
|
27
65
|
dispatch: Dispatch<AppAction>;
|
|
28
66
|
focusedTickerSymbol: string | null;
|
|
29
67
|
isDetachedWindow: boolean;
|
|
68
|
+
/** The resolved table; the app resolves it once and shares it with the shell. */
|
|
69
|
+
keybindings?: ResolvedKeybindings;
|
|
30
70
|
pluginRegistry: PluginRegistry;
|
|
31
71
|
refreshTicker: (symbol: string, exchange?: string, tickerOverride?: TickerRecord | null, priority?: number) => void;
|
|
32
72
|
startUpdate: (release: ReleaseInfo) => void;
|
|
@@ -35,6 +75,7 @@ export function useAppGlobalShortcuts({
|
|
|
35
75
|
const dialogOpen = useDialogState((s) => s.isOpen);
|
|
36
76
|
const nativeRenderer = useNativeRenderer();
|
|
37
77
|
const rendererHost = useRendererHost();
|
|
78
|
+
useKeybindingIssueNotice(keybindings, pluginRegistry, state.initialized);
|
|
38
79
|
|
|
39
80
|
useShortcut((event) => {
|
|
40
81
|
if (isCopyShortcut(event) && copyActiveSelection(nativeRenderer)) {
|
|
@@ -48,17 +89,18 @@ export function useAppGlobalShortcuts({
|
|
|
48
89
|
return;
|
|
49
90
|
}
|
|
50
91
|
|
|
92
|
+
const match = matchKeybinding(keybindings, event);
|
|
93
|
+
const action = match?.kind === "action" ? match.id : null;
|
|
94
|
+
|
|
51
95
|
// Terminals send Ctrl; the browser and the desktop webview send Cmd on
|
|
52
96
|
// macOS, which the OpenTUI host also reports as `super` under the kitty
|
|
53
|
-
// protocol.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
if (!isDetachedWindow
|
|
58
|
-
&& /^[1-9]$/.test(event.name ?? "")
|
|
59
|
-
&& (event.ctrl || event.meta || event.super)) {
|
|
97
|
+
// protocol. While a dialog, the command bar or an editable field owns the
|
|
98
|
+
// keyboard the digit must not move layouts, but it still has to be
|
|
99
|
+
// swallowed there or the webview hands Cmd-digit to the browser's own tab
|
|
100
|
+
// switcher.
|
|
101
|
+
if (!isDetachedWindow && action === "switch-layout" && match?.kind === "action" && match.digit !== null) {
|
|
60
102
|
const layouts = state.config.layouts ?? [];
|
|
61
|
-
const idx =
|
|
103
|
+
const idx = match.digit - 1;
|
|
62
104
|
const uiOwnsKeyboard = dialogOpen || state.commandBarOpen || event.targetEditable === true;
|
|
63
105
|
if (!uiOwnsKeyboard && idx < layouts.length && idx !== state.config.activeLayoutIndex) {
|
|
64
106
|
dispatch({ type: "SWITCH_LAYOUT", index: idx });
|
|
@@ -70,16 +112,13 @@ export function useAppGlobalShortcuts({
|
|
|
70
112
|
|
|
71
113
|
if (dialogOpen) return;
|
|
72
114
|
|
|
73
|
-
if (!isDetachedWindow &&
|
|
74
|
-
(event.name === "p" && event.ctrl)
|
|
75
|
-
|| (event.name === "k" && (event.ctrl || event.meta || event.super))
|
|
76
|
-
)) {
|
|
115
|
+
if (!isDetachedWindow && action === "command-bar") {
|
|
77
116
|
event.preventDefault();
|
|
78
117
|
event.stopPropagation();
|
|
79
118
|
dispatch({ type: "TOGGLE_COMMAND_BAR" });
|
|
80
119
|
return;
|
|
81
120
|
}
|
|
82
|
-
if (!isDetachedWindow &&
|
|
121
|
+
if (!isDetachedWindow && action === "ticker-search" && !state.commandBarOpen) {
|
|
83
122
|
event.preventDefault();
|
|
84
123
|
event.stopPropagation();
|
|
85
124
|
dispatch({
|
|
@@ -91,11 +130,9 @@ export function useAppGlobalShortcuts({
|
|
|
91
130
|
return;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
const hasShortcutModifier = event.ctrl || event.meta || event.super || event.alt;
|
|
95
|
-
|
|
96
133
|
if (state.commandBarOpen) return;
|
|
97
134
|
|
|
98
|
-
if (
|
|
135
|
+
if (action === "focus-next-pane" || action === "focus-prev-pane") {
|
|
99
136
|
const paneOrder = getVisiblePaneCycleOrder(
|
|
100
137
|
state.config.layout,
|
|
101
138
|
pluginRegistry,
|
|
@@ -103,50 +140,60 @@ export function useAppGlobalShortcuts({
|
|
|
103
140
|
);
|
|
104
141
|
if (paneOrder.length === 0) return;
|
|
105
142
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
143
|
+
dispatch({ type: action === "focus-prev-pane" ? "FOCUS_PREV" : "FOCUS_NEXT", paneOrder });
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
event.stopPropagation();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (match?.kind === "command" && !isDetachedWindow) {
|
|
150
|
+
// Control chords belong to the editor while text is being typed; Alt,
|
|
151
|
+
// Command and function keys are never text, so they still fire.
|
|
152
|
+
const editing = state.inputCaptured || event.targetEditable === true;
|
|
153
|
+
if (editing && event.ctrl && !(event.meta || event.super)) return;
|
|
111
154
|
event.preventDefault();
|
|
112
155
|
event.stopPropagation();
|
|
156
|
+
dispatch({
|
|
157
|
+
type: "SET_COMMAND_BAR",
|
|
158
|
+
open: true,
|
|
159
|
+
query: match.command.query,
|
|
160
|
+
launch: { kind: "run-query", query: match.command.query },
|
|
161
|
+
});
|
|
113
162
|
return;
|
|
114
163
|
}
|
|
115
164
|
|
|
116
165
|
if (state.inputCaptured) return;
|
|
117
166
|
|
|
118
|
-
|
|
119
|
-
|| event.key === "?"
|
|
120
|
-
|| event.sequence === "?"
|
|
121
|
-
|| (event.name === "/" && event.shift);
|
|
122
|
-
if (!isDetachedWindow && !hasShortcutModifier && isQuestionMark) {
|
|
167
|
+
if (!isDetachedWindow && action === "help") {
|
|
123
168
|
event.preventDefault();
|
|
124
169
|
event.stopPropagation();
|
|
125
170
|
pluginRegistry.showPane("help");
|
|
126
171
|
return;
|
|
127
172
|
}
|
|
128
173
|
|
|
129
|
-
if (!
|
|
174
|
+
if (!isDetachedWindow && action === "quit") {
|
|
130
175
|
rendererHost.requestExit();
|
|
131
|
-
} else if (
|
|
176
|
+
} else if (action === "refresh-ticker") {
|
|
132
177
|
if (focusedTickerSymbol) {
|
|
133
178
|
const ticker = state.tickers.get(focusedTickerSymbol);
|
|
134
179
|
if (ticker) refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 0);
|
|
135
180
|
}
|
|
136
|
-
} else if (
|
|
181
|
+
} else if (action === "refresh-all") {
|
|
137
182
|
for (const ticker of state.tickers.values()) {
|
|
138
183
|
refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 1);
|
|
139
184
|
}
|
|
140
|
-
} else if (
|
|
141
|
-
|
|
142
|
-
|
|
185
|
+
} else if (action === "install-update") {
|
|
186
|
+
if (state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
|
|
187
|
+
startUpdate(state.updateAvailable);
|
|
188
|
+
}
|
|
189
|
+
} else if (!action) {
|
|
143
190
|
const disabledPlugins = new Set(state.config.disabledPlugins || []);
|
|
191
|
+
const keybindingsConfig = state.config.keybindings;
|
|
144
192
|
for (const shortcut of pluginRegistry.shortcuts.values()) {
|
|
145
193
|
const ownerId = pluginRegistry.getShortcutPluginId(shortcut.id);
|
|
146
194
|
if (ownerId && disabledPlugins.has(ownerId)) continue;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
&& (shortcut.shift ?? false) === (event.shift ?? false)) {
|
|
195
|
+
const chords = resolvePluginShortcutChords(keybindingsConfig, shortcut);
|
|
196
|
+
if (chords.some((chord) => matchesKeyChord(chord, event))) {
|
|
150
197
|
shortcut.execute();
|
|
151
198
|
break;
|
|
152
199
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every built-in action a key can be bound to, with the chords it ships with.
|
|
3
|
+
* The ids are what `config.json` and the CLI use, so they read as actions, not
|
|
4
|
+
* as the keys they happen to sit on today.
|
|
5
|
+
*/
|
|
6
|
+
export type CoreKeybindingActionId =
|
|
7
|
+
| "command-bar"
|
|
8
|
+
| "ticker-search"
|
|
9
|
+
| "help"
|
|
10
|
+
| "switch-layout"
|
|
11
|
+
| "focus-next-pane"
|
|
12
|
+
| "focus-prev-pane"
|
|
13
|
+
| "refresh-ticker"
|
|
14
|
+
| "refresh-all"
|
|
15
|
+
| "install-update"
|
|
16
|
+
| "quit"
|
|
17
|
+
| "pane-close"
|
|
18
|
+
| "close-floating-panes"
|
|
19
|
+
| "pane-settings"
|
|
20
|
+
| "pane-fullscreen"
|
|
21
|
+
| "pane-float"
|
|
22
|
+
| "pane-pop-out"
|
|
23
|
+
| "pane-screenshot"
|
|
24
|
+
| "pane-export-csv"
|
|
25
|
+
| "pane-share"
|
|
26
|
+
| "layout-gallery"
|
|
27
|
+
| "tidy-windows"
|
|
28
|
+
| "window-move-mode"
|
|
29
|
+
| "window-resize-mode";
|
|
30
|
+
|
|
31
|
+
export type KeybindingActionCategory = "Global Keys" | "Pane Management";
|
|
32
|
+
|
|
33
|
+
export interface KeybindingActionDef {
|
|
34
|
+
id: CoreKeybindingActionId;
|
|
35
|
+
category: KeybindingActionCategory;
|
|
36
|
+
/** Help text. Kept in English here; the help pane translates it. */
|
|
37
|
+
description: string;
|
|
38
|
+
defaults: readonly string[];
|
|
39
|
+
/** Only the desktop app has this action; the terminal hides the row. */
|
|
40
|
+
desktopOnly?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const PANE_ACTION_IDS = new Set<CoreKeybindingActionId>([
|
|
44
|
+
"pane-close",
|
|
45
|
+
"close-floating-panes",
|
|
46
|
+
"pane-settings",
|
|
47
|
+
"pane-fullscreen",
|
|
48
|
+
"pane-float",
|
|
49
|
+
"pane-pop-out",
|
|
50
|
+
"pane-screenshot",
|
|
51
|
+
"pane-export-csv",
|
|
52
|
+
"pane-share",
|
|
53
|
+
"layout-gallery",
|
|
54
|
+
"tidy-windows",
|
|
55
|
+
"window-move-mode",
|
|
56
|
+
"window-resize-mode",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Order matters twice: it is the order the help pane lists rows in, and the
|
|
61
|
+
* order chords are matched in when two actions share one.
|
|
62
|
+
*/
|
|
63
|
+
export const KEYBINDING_ACTIONS: readonly KeybindingActionDef[] = [
|
|
64
|
+
{
|
|
65
|
+
id: "command-bar",
|
|
66
|
+
category: "Global Keys",
|
|
67
|
+
description: "Open the command bar.",
|
|
68
|
+
defaults: ["Ctrl+P", "CmdOrCtrl+K"],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "ticker-search",
|
|
72
|
+
category: "Global Keys",
|
|
73
|
+
description: "Open ticker search directly.",
|
|
74
|
+
// One chord on purpose: the command bar already searches symbols for
|
|
75
|
+
// free text, so a layout without a backtick key still has Ctrl+P and
|
|
76
|
+
// typing. A dedicated key is a rebind away for anyone who wants one.
|
|
77
|
+
defaults: ["`"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "help",
|
|
81
|
+
category: "Global Keys",
|
|
82
|
+
description: "Open help.",
|
|
83
|
+
defaults: ["?"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "switch-layout",
|
|
87
|
+
category: "Global Keys",
|
|
88
|
+
description: "Switch saved layouts by number.",
|
|
89
|
+
defaults: ["CmdOrCtrl+Digit"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "focus-next-pane",
|
|
93
|
+
category: "Global Keys",
|
|
94
|
+
description: "Move focus to the next pane or floating window.",
|
|
95
|
+
defaults: ["Tab"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "focus-prev-pane",
|
|
99
|
+
category: "Global Keys",
|
|
100
|
+
description: "Move focus to the previous pane or floating window.",
|
|
101
|
+
defaults: ["Shift+Tab"],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "refresh-ticker",
|
|
105
|
+
category: "Global Keys",
|
|
106
|
+
description: "Refresh the focused ticker.",
|
|
107
|
+
defaults: ["R"],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "refresh-all",
|
|
111
|
+
category: "Global Keys",
|
|
112
|
+
description: "Refresh everything.",
|
|
113
|
+
defaults: ["Shift+R"],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "install-update",
|
|
117
|
+
category: "Global Keys",
|
|
118
|
+
description: "Install an available app update when one is shown.",
|
|
119
|
+
defaults: ["U"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "quit",
|
|
123
|
+
category: "Global Keys",
|
|
124
|
+
description: "Quit the terminal app.",
|
|
125
|
+
defaults: ["Q"],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "pane-close",
|
|
129
|
+
category: "Pane Management",
|
|
130
|
+
description: "Close the focused pane, docked or floating. Locked panes stay open.",
|
|
131
|
+
defaults: ["CmdOrCtrl+W"],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "close-floating-panes",
|
|
135
|
+
category: "Pane Management",
|
|
136
|
+
description: "Close all floating panes except locked ones.",
|
|
137
|
+
defaults: ["CmdOrCtrl+Alt+W"],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "pane-settings",
|
|
141
|
+
category: "Pane Management",
|
|
142
|
+
description: "Edit settings for the focused pane.",
|
|
143
|
+
defaults: ["CmdOrCtrl+,"],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "pane-fullscreen",
|
|
147
|
+
category: "Pane Management",
|
|
148
|
+
description: "Fill the window with the focused pane, or restore it.",
|
|
149
|
+
defaults: ["CmdOrCtrl+Shift+F"],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "pane-float",
|
|
153
|
+
category: "Pane Management",
|
|
154
|
+
description: "Dock or float the focused pane.",
|
|
155
|
+
defaults: ["CmdOrCtrl+Shift+D"],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "pane-pop-out",
|
|
159
|
+
category: "Pane Management",
|
|
160
|
+
description: "Pop the focused pane out to a desktop window.",
|
|
161
|
+
defaults: ["CmdOrCtrl+Shift+O"],
|
|
162
|
+
desktopOnly: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "pane-screenshot",
|
|
166
|
+
category: "Pane Management",
|
|
167
|
+
description: "Copy a screenshot of the focused pane.",
|
|
168
|
+
defaults: ["CmdOrCtrl+Shift+C"],
|
|
169
|
+
desktopOnly: true,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "pane-export-csv",
|
|
173
|
+
category: "Pane Management",
|
|
174
|
+
description: "Export the focused pane's table as CSV.",
|
|
175
|
+
defaults: ["CmdOrCtrl+Shift+E"],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "pane-share",
|
|
179
|
+
category: "Pane Management",
|
|
180
|
+
description: "Share the focused pane.",
|
|
181
|
+
defaults: ["CmdOrCtrl+Shift+S"],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "layout-gallery",
|
|
185
|
+
category: "Pane Management",
|
|
186
|
+
description: "Open layout actions.",
|
|
187
|
+
defaults: ["CmdOrCtrl+Shift+L"],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "tidy-windows",
|
|
191
|
+
category: "Pane Management",
|
|
192
|
+
description: "Tidy Windows",
|
|
193
|
+
defaults: ["CmdOrCtrl+Shift+G"],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "window-move-mode",
|
|
197
|
+
category: "Pane Management",
|
|
198
|
+
description: "Enter window move mode.",
|
|
199
|
+
defaults: ["CmdOrCtrl+Shift+M"],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "window-resize-mode",
|
|
203
|
+
category: "Pane Management",
|
|
204
|
+
description: "Enter window resize mode.",
|
|
205
|
+
defaults: ["CmdOrCtrl+Shift+R"],
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const ACTION_BY_ID = new Map(KEYBINDING_ACTIONS.map((action) => [action.id, action]));
|
|
210
|
+
|
|
211
|
+
export function getKeybindingAction(id: string): KeybindingActionDef | undefined {
|
|
212
|
+
return ACTION_BY_ID.get(id as CoreKeybindingActionId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isCoreKeybindingActionId(id: string): id is CoreKeybindingActionId {
|
|
216
|
+
return ACTION_BY_ID.has(id as CoreKeybindingActionId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const PLUGIN_ACTION_PREFIX = "plugin:";
|
|
220
|
+
|
|
221
|
+
export function pluginShortcutActionId(shortcutId: string): string {
|
|
222
|
+
return `${PLUGIN_ACTION_PREFIX}${shortcutId}`;
|
|
223
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A request to bind a key to command bar text, handed from the command bar to
|
|
3
|
+
* the help pane. The bar cannot capture a key itself: the moment it opens, it
|
|
4
|
+
* owns the keyboard, and the capture has to outlive the bar closing.
|
|
5
|
+
*/
|
|
6
|
+
export interface KeybindingCaptureRequest {
|
|
7
|
+
kind: "command";
|
|
8
|
+
query: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let pending: KeybindingCaptureRequest | null = null;
|
|
12
|
+
const listeners = new Set<() => void>();
|
|
13
|
+
|
|
14
|
+
export function requestKeybindingCapture(request: KeybindingCaptureRequest): void {
|
|
15
|
+
pending = request;
|
|
16
|
+
for (const listener of listeners) listener();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Whether a request is waiting, for a pane deciding which tab to open on. */
|
|
20
|
+
export function hasKeybindingCaptureRequest(): boolean {
|
|
21
|
+
return pending !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Returns and clears the pending request, so it is honoured exactly once. */
|
|
25
|
+
export function takeKeybindingCaptureRequest(): KeybindingCaptureRequest | null {
|
|
26
|
+
const request = pending;
|
|
27
|
+
pending = null;
|
|
28
|
+
return request;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function subscribeKeybindingCapture(listener: () => void): () => void {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(listener);
|
|
35
|
+
};
|
|
36
|
+
}
|