gloomberb 0.4.1 → 0.5.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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/react";
|
|
4
|
+
import type { GloomPlugin, DetailTabProps } from "../../../types/plugin";
|
|
5
|
+
import type { SecFilingItem } from "../../../types/data-provider";
|
|
6
|
+
import {
|
|
7
|
+
useResolvedEntryValue,
|
|
8
|
+
useSecFilingContent,
|
|
9
|
+
useSecFilingsQuery,
|
|
10
|
+
} from "../../../market-data/hooks";
|
|
11
|
+
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
12
|
+
import { usePaneTicker } from "../../../state/app-context";
|
|
13
|
+
import { colors } from "../../../theme/colors";
|
|
14
|
+
import { Spinner } from "../../../components/spinner";
|
|
15
|
+
import { isUsEquityTicker } from "../../../utils/sec";
|
|
16
|
+
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
17
|
+
import { parseForm4Xml, transactionTypeLabel, type InsiderTransaction } from "./insider-data";
|
|
18
|
+
|
|
19
|
+
const FORM4_LIMIT = 20;
|
|
20
|
+
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
23
|
+
|
|
24
|
+
function formatDate(d: Date | string | number): string {
|
|
25
|
+
const date = d instanceof Date ? d : new Date(d);
|
|
26
|
+
if (isNaN(date.getTime())) return "—";
|
|
27
|
+
return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, " ")} ${date.getFullYear()}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function transactionColor(type: InsiderTransaction["transactionType"]): string {
|
|
31
|
+
if (type === "P") return colors.positive;
|
|
32
|
+
if (type === "S") return colors.negative;
|
|
33
|
+
return colors.textDim;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function wrapLines(text: string, width: number): string[] {
|
|
37
|
+
const words = text.trim().split(/\s+/);
|
|
38
|
+
const lines: string[] = [];
|
|
39
|
+
let current = "";
|
|
40
|
+
for (const word of words) {
|
|
41
|
+
if (!current) { current = word; continue; }
|
|
42
|
+
if (current.length + 1 + word.length <= width) {
|
|
43
|
+
current = `${current} ${word}`;
|
|
44
|
+
} else {
|
|
45
|
+
lines.push(current);
|
|
46
|
+
current = word;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (current) lines.push(current);
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderNotice(message: string, width: number) {
|
|
54
|
+
const lines = wrapLines(message, width - 4);
|
|
55
|
+
return (
|
|
56
|
+
<box flexDirection="column" paddingX={1} paddingY={1}>
|
|
57
|
+
{lines.map((line, i) => (
|
|
58
|
+
<box key={i} height={1}>
|
|
59
|
+
<text fg={colors.textDim}>{line}</text>
|
|
60
|
+
</box>
|
|
61
|
+
))}
|
|
62
|
+
</box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ParsedFiling {
|
|
67
|
+
filing: SecFilingItem;
|
|
68
|
+
transaction: InsiderTransaction | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildSummary(parsed: ParsedFiling[]): string {
|
|
72
|
+
const cutoff = new Date(Date.now() - NINETY_DAYS_MS);
|
|
73
|
+
let buyShares = 0;
|
|
74
|
+
let sellShares = 0;
|
|
75
|
+
let buyValue = 0;
|
|
76
|
+
let sellValue = 0;
|
|
77
|
+
|
|
78
|
+
for (const { transaction } of parsed) {
|
|
79
|
+
if (!transaction) continue;
|
|
80
|
+
const txDate = transaction.filingDate instanceof Date ? transaction.filingDate : new Date(transaction.filingDate);
|
|
81
|
+
if (isNaN(txDate.getTime()) || txDate < cutoff) continue;
|
|
82
|
+
if (transaction.transactionType === "P") {
|
|
83
|
+
buyShares += transaction.shares;
|
|
84
|
+
buyValue += transaction.totalValue ?? 0;
|
|
85
|
+
} else if (transaction.transactionType === "S") {
|
|
86
|
+
sellShares += transaction.shares;
|
|
87
|
+
sellValue += transaction.totalValue ?? 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (buyShares === 0 && sellShares === 0) return "No buy/sell activity in last 90 days.";
|
|
92
|
+
|
|
93
|
+
const parts: string[] = [];
|
|
94
|
+
if (buyShares > 0) parts.push(`Bought ${formatCompact(buyShares)} shares (${formatCurrency(buyValue)})`);
|
|
95
|
+
if (sellShares > 0) parts.push(`Sold ${formatCompact(sellShares)} shares (${formatCurrency(sellValue)})`);
|
|
96
|
+
return parts.join(" | ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function InsiderTab({ width, height, focused }: DetailTabProps) {
|
|
100
|
+
const { ticker } = usePaneTicker();
|
|
101
|
+
const eligibleTicker = isUsEquityTicker(ticker);
|
|
102
|
+
const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
|
|
103
|
+
|
|
104
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
105
|
+
const [nameFilter, setNameFilter] = useState<string | null>(null);
|
|
106
|
+
const [detailFiling, setDetailFiling] = useState<ParsedFiling | null>(null);
|
|
107
|
+
|
|
108
|
+
const filingsEntry = useSecFilingsQuery(
|
|
109
|
+
instrument && eligibleTicker ? { instrument, count: FORM4_LIMIT } : null,
|
|
110
|
+
);
|
|
111
|
+
const allFilings = useResolvedEntryValue(filingsEntry) ?? [];
|
|
112
|
+
const form4Filings = allFilings.filter((f) => f.form.trim() === "4");
|
|
113
|
+
|
|
114
|
+
const loading =
|
|
115
|
+
filingsEntry?.phase === "loading" ||
|
|
116
|
+
(filingsEntry?.phase === "refreshing" && allFilings.length === 0);
|
|
117
|
+
const error =
|
|
118
|
+
filingsEntry?.phase === "error"
|
|
119
|
+
? (filingsEntry.error?.message ?? "Failed to load SEC filings")
|
|
120
|
+
: null;
|
|
121
|
+
|
|
122
|
+
// Fetch content for each Form 4 filing sequentially via an index pointer
|
|
123
|
+
const [contentMap, setContentMap] = useState<Map<string, string | null>>(new Map());
|
|
124
|
+
const fetchIndexRef = useRef(0);
|
|
125
|
+
const [fetchPointer, setFetchPointer] = useState(0);
|
|
126
|
+
|
|
127
|
+
// Reset when ticker changes
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
fetchIndexRef.current += 1;
|
|
130
|
+
setContentMap(new Map());
|
|
131
|
+
setFetchPointer(0);
|
|
132
|
+
}, [ticker?.metadata.ticker]);
|
|
133
|
+
|
|
134
|
+
// Pick the next un-fetched filing to load
|
|
135
|
+
const nextToFetch = form4Filings.find((f) => !contentMap.has(f.accessionNumber)) ?? null;
|
|
136
|
+
const contentEntry = useSecFilingContent(nextToFetch);
|
|
137
|
+
const contentValue = useResolvedEntryValue(contentEntry);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!nextToFetch) return;
|
|
141
|
+
if (contentEntry?.phase === "error") {
|
|
142
|
+
setContentMap((prev) => new Map(prev).set(nextToFetch.accessionNumber, null));
|
|
143
|
+
setFetchPointer((p) => p + 1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (contentValue !== null && contentValue !== undefined) {
|
|
147
|
+
setContentMap((prev) => new Map(prev).set(nextToFetch.accessionNumber, contentValue));
|
|
148
|
+
setFetchPointer((p) => p + 1);
|
|
149
|
+
}
|
|
150
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
|
+
}, [contentValue, contentEntry?.phase, nextToFetch?.accessionNumber, fetchPointer]);
|
|
152
|
+
|
|
153
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
154
|
+
|
|
155
|
+
const allParsed: ParsedFiling[] = form4Filings.map((filing) => {
|
|
156
|
+
const xml = contentMap.get(filing.accessionNumber) ?? null;
|
|
157
|
+
return {
|
|
158
|
+
filing,
|
|
159
|
+
transaction: xml ? parseForm4Xml(xml) : null,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Apply name filter
|
|
164
|
+
const parsed = nameFilter
|
|
165
|
+
? allParsed.filter((p) => p.transaction?.reportedName === nameFilter)
|
|
166
|
+
: allParsed;
|
|
167
|
+
|
|
168
|
+
// Keyboard handling
|
|
169
|
+
useKeyboard((event) => {
|
|
170
|
+
if (!focused) return;
|
|
171
|
+
|
|
172
|
+
if (event.name === "escape") {
|
|
173
|
+
if (detailFiling) { setDetailFiling(null); return; }
|
|
174
|
+
if (nameFilter) { setNameFilter(null); setSelectedIdx(0); return; }
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (detailFiling) return; // No navigation in detail view
|
|
179
|
+
|
|
180
|
+
if (event.name === "j" || event.name === "down") {
|
|
181
|
+
setSelectedIdx((prev) => Math.min(prev + 1, parsed.length - 1));
|
|
182
|
+
} else if (event.name === "k" || event.name === "up") {
|
|
183
|
+
setSelectedIdx((prev) => Math.max(prev - 1, 0));
|
|
184
|
+
} else if (event.name === "return") {
|
|
185
|
+
const item = parsed[selectedIdx];
|
|
186
|
+
if (item?.transaction) setDetailFiling(item);
|
|
187
|
+
} else if (event.name === "f") {
|
|
188
|
+
// Toggle name filter on selected row's insider
|
|
189
|
+
const item = parsed[selectedIdx];
|
|
190
|
+
if (item?.transaction) {
|
|
191
|
+
if (nameFilter === item.transaction.reportedName) {
|
|
192
|
+
setNameFilter(null);
|
|
193
|
+
} else {
|
|
194
|
+
setNameFilter(item.transaction.reportedName);
|
|
195
|
+
}
|
|
196
|
+
setSelectedIdx(0);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Scroll to follow selection
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const sb = scrollRef.current;
|
|
204
|
+
if (!sb?.viewport || parsed.length === 0 || selectedIdx < 0) return;
|
|
205
|
+
const viewportHeight = Math.max(sb.viewport.height, 1);
|
|
206
|
+
if (selectedIdx < sb.scrollTop) sb.scrollTo(selectedIdx);
|
|
207
|
+
else if (selectedIdx >= sb.scrollTop + viewportHeight) sb.scrollTo(selectedIdx - viewportHeight + 1);
|
|
208
|
+
}, [selectedIdx, parsed.length]);
|
|
209
|
+
|
|
210
|
+
if (!ticker) return <text fg={colors.textDim}>Select a ticker to view insider activity.</text>;
|
|
211
|
+
if (!eligibleTicker) return renderNotice("Insider transactions are only shown for US equities.", width);
|
|
212
|
+
if (loading && allFilings.length === 0) return <Spinner label="Loading insider filings..." />;
|
|
213
|
+
if (error) return renderNotice(`Error: ${error}`, width);
|
|
214
|
+
if (!loading && form4Filings.length === 0) {
|
|
215
|
+
return renderNotice(`No Form 4 filings found for ${ticker.metadata.ticker}.`, width);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Detail view
|
|
219
|
+
if (detailFiling?.transaction) {
|
|
220
|
+
const tx = detailFiling.transaction;
|
|
221
|
+
const filing = detailFiling.filing;
|
|
222
|
+
return (
|
|
223
|
+
<box flexDirection="column" width={width} height={height}>
|
|
224
|
+
<box height={1} paddingX={1}>
|
|
225
|
+
<text fg={colors.textBright} attributes={TextAttributes.BOLD}>◂ {tx.reportedName}</text>
|
|
226
|
+
{tx.title && <text fg={colors.textDim}> — {tx.title}</text>}
|
|
227
|
+
</box>
|
|
228
|
+
<scrollbox flexGrow={1} scrollY focusable={false}>
|
|
229
|
+
<box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
|
|
230
|
+
<box flexDirection="column">
|
|
231
|
+
<box flexDirection="row" height={1}>
|
|
232
|
+
<box width={16}><text fg={colors.textDim}>Transaction</text></box>
|
|
233
|
+
<text fg={transactionColor(tx.transactionType)} attributes={TextAttributes.BOLD}>
|
|
234
|
+
{transactionTypeLabel(tx.transactionType)}
|
|
235
|
+
</text>
|
|
236
|
+
</box>
|
|
237
|
+
<box flexDirection="row" height={1}>
|
|
238
|
+
<box width={16}><text fg={colors.textDim}>Date</text></box>
|
|
239
|
+
<text fg={colors.text}>{formatDate(tx.filingDate)}</text>
|
|
240
|
+
</box>
|
|
241
|
+
<box flexDirection="row" height={1}>
|
|
242
|
+
<box width={16}><text fg={colors.textDim}>Shares</text></box>
|
|
243
|
+
<text fg={colors.text}>{formatCompact(tx.shares)}</text>
|
|
244
|
+
</box>
|
|
245
|
+
<box flexDirection="row" height={1}>
|
|
246
|
+
<box width={16}><text fg={colors.textDim}>Price/Share</text></box>
|
|
247
|
+
<text fg={colors.text}>
|
|
248
|
+
{tx.pricePerShare != null ? formatCurrency(tx.pricePerShare) : "—"}
|
|
249
|
+
</text>
|
|
250
|
+
</box>
|
|
251
|
+
<box flexDirection="row" height={1}>
|
|
252
|
+
<box width={16}><text fg={colors.textDim}>Total Value</text></box>
|
|
253
|
+
<text fg={transactionColor(tx.transactionType)}>
|
|
254
|
+
{tx.totalValue != null ? formatCurrency(tx.totalValue) : "—"}
|
|
255
|
+
</text>
|
|
256
|
+
</box>
|
|
257
|
+
<box flexDirection="row" height={1}>
|
|
258
|
+
<box width={16}><text fg={colors.textDim}>Shares Owned</text></box>
|
|
259
|
+
<text fg={colors.text}>
|
|
260
|
+
{tx.sharesOwned != null ? formatCompact(tx.sharesOwned) : "—"}
|
|
261
|
+
</text>
|
|
262
|
+
</box>
|
|
263
|
+
</box>
|
|
264
|
+
<box flexDirection="column">
|
|
265
|
+
<box height={1}>
|
|
266
|
+
<text fg={colors.textDim} attributes={TextAttributes.BOLD}>Filing</text>
|
|
267
|
+
</box>
|
|
268
|
+
<box flexDirection="row" height={1}>
|
|
269
|
+
<box width={16}><text fg={colors.textDim}>Accession</text></box>
|
|
270
|
+
<text fg={colors.text}>{filing.accessionNumber}</text>
|
|
271
|
+
</box>
|
|
272
|
+
<box flexDirection="row" height={1}>
|
|
273
|
+
<box width={16}><text fg={colors.textDim}>Filed</text></box>
|
|
274
|
+
<text fg={colors.text}>{formatDate(filing.filingDate)}</text>
|
|
275
|
+
</box>
|
|
276
|
+
<box flexDirection="row" height={1}>
|
|
277
|
+
<box width={16}><text fg={colors.textDim}>Form</text></box>
|
|
278
|
+
<text fg={colors.text}>{filing.form}</text>
|
|
279
|
+
</box>
|
|
280
|
+
</box>
|
|
281
|
+
</box>
|
|
282
|
+
</scrollbox>
|
|
283
|
+
</box>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const summary = buildSummary(allParsed);
|
|
288
|
+
const dateW = 12;
|
|
289
|
+
const typeW = 7;
|
|
290
|
+
const sharesW = 10;
|
|
291
|
+
const priceW = 10;
|
|
292
|
+
const valueW = 12;
|
|
293
|
+
const nameW = Math.max(10, width - dateW - typeW - sharesW - priceW - valueW - 2);
|
|
294
|
+
const pendingCount = form4Filings.filter((f) => !contentMap.has(f.accessionNumber)).length;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<box flexDirection="column" width={width} height={height}>
|
|
298
|
+
{/* Summary bar */}
|
|
299
|
+
<box height={1} paddingX={1} flexDirection="row">
|
|
300
|
+
<text fg={colors.textDim}>{summary}</text>
|
|
301
|
+
{nameFilter && (
|
|
302
|
+
<box marginLeft={1}>
|
|
303
|
+
<text fg={colors.warning}>[filtered: {nameFilter}]</text>
|
|
304
|
+
</box>
|
|
305
|
+
)}
|
|
306
|
+
{pendingCount > 0 && (
|
|
307
|
+
<>
|
|
308
|
+
<box flexGrow={1} />
|
|
309
|
+
<text fg={colors.textMuted}>loading {pendingCount}…</text>
|
|
310
|
+
</>
|
|
311
|
+
)}
|
|
312
|
+
</box>
|
|
313
|
+
|
|
314
|
+
{/* Column headers */}
|
|
315
|
+
<box height={1} paddingX={1} flexDirection="row">
|
|
316
|
+
<box width={dateW}><text fg={colors.textDim}>DATE</text></box>
|
|
317
|
+
<box width={nameW}><text fg={colors.textDim}>INSIDER</text></box>
|
|
318
|
+
<box width={typeW}><text fg={colors.textDim}>TYPE</text></box>
|
|
319
|
+
<box width={sharesW} justifyContent="flex-end"><text fg={colors.textDim}>SHARES</text></box>
|
|
320
|
+
<box width={priceW} justifyContent="flex-end"><text fg={colors.textDim}>PRICE</text></box>
|
|
321
|
+
<box width={valueW} justifyContent="flex-end"><text fg={colors.textDim}>VALUE</text></box>
|
|
322
|
+
</box>
|
|
323
|
+
|
|
324
|
+
<scrollbox ref={scrollRef} flexGrow={1} scrollY focusable={false}>
|
|
325
|
+
<box flexDirection="column">
|
|
326
|
+
{parsed.map(({ filing, transaction }, i) => {
|
|
327
|
+
const isSelected = i === selectedIdx;
|
|
328
|
+
const bg = isSelected ? colors.selected : undefined;
|
|
329
|
+
|
|
330
|
+
if (!transaction) {
|
|
331
|
+
const isLoading = !contentMap.has(filing.accessionNumber);
|
|
332
|
+
return (
|
|
333
|
+
<box key={filing.accessionNumber} height={1} paddingX={1} flexDirection="row" backgroundColor={bg}>
|
|
334
|
+
<box width={dateW}>
|
|
335
|
+
<text fg={colors.textDim}>{formatDate(filing.filingDate)}</text>
|
|
336
|
+
</box>
|
|
337
|
+
<box flexGrow={1}>
|
|
338
|
+
<text fg={colors.textMuted}>{isLoading ? "loading…" : "—"}</text>
|
|
339
|
+
</box>
|
|
340
|
+
</box>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const typeLabel = transactionTypeLabel(transaction.transactionType);
|
|
345
|
+
const typeFg = isSelected ? colors.selectedText : transactionColor(transaction.transactionType);
|
|
346
|
+
const fg = isSelected ? colors.selectedText : colors.text;
|
|
347
|
+
const dimFg = isSelected ? colors.selectedText : colors.textDim;
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<box
|
|
351
|
+
key={filing.accessionNumber}
|
|
352
|
+
height={1}
|
|
353
|
+
paddingX={1}
|
|
354
|
+
flexDirection="row"
|
|
355
|
+
backgroundColor={bg}
|
|
356
|
+
onMouseDown={() => setSelectedIdx(i)}
|
|
357
|
+
>
|
|
358
|
+
<box width={dateW}>
|
|
359
|
+
<text fg={dimFg}>{formatDate(transaction.filingDate)}</text>
|
|
360
|
+
</box>
|
|
361
|
+
<box
|
|
362
|
+
width={nameW}
|
|
363
|
+
onMouseDown={(ev: any) => {
|
|
364
|
+
ev.preventDefault?.();
|
|
365
|
+
if (nameFilter === transaction.reportedName) {
|
|
366
|
+
setNameFilter(null);
|
|
367
|
+
} else {
|
|
368
|
+
setNameFilter(transaction.reportedName);
|
|
369
|
+
setSelectedIdx(0);
|
|
370
|
+
}
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
<text fg={fg} attributes={TextAttributes.UNDERLINE}>
|
|
374
|
+
{transaction.reportedName.slice(0, nameW - 1)}
|
|
375
|
+
</text>
|
|
376
|
+
</box>
|
|
377
|
+
<box width={typeW}>
|
|
378
|
+
<text fg={typeFg} attributes={TextAttributes.BOLD}>{typeLabel}</text>
|
|
379
|
+
</box>
|
|
380
|
+
<box width={sharesW} justifyContent="flex-end">
|
|
381
|
+
<text fg={fg}>{formatCompact(transaction.shares)}</text>
|
|
382
|
+
</box>
|
|
383
|
+
<box width={priceW} justifyContent="flex-end">
|
|
384
|
+
<text fg={fg}>
|
|
385
|
+
{transaction.pricePerShare != null ? formatCurrency(transaction.pricePerShare) : "—"}
|
|
386
|
+
</text>
|
|
387
|
+
</box>
|
|
388
|
+
<box width={valueW} justifyContent="flex-end">
|
|
389
|
+
<text fg={typeFg}>
|
|
390
|
+
{transaction.totalValue != null ? formatCompact(transaction.totalValue) : "—"}
|
|
391
|
+
</text>
|
|
392
|
+
</box>
|
|
393
|
+
</box>
|
|
394
|
+
);
|
|
395
|
+
})}
|
|
396
|
+
</box>
|
|
397
|
+
</scrollbox>
|
|
398
|
+
</box>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export const insiderPlugin: GloomPlugin = {
|
|
403
|
+
id: "insider",
|
|
404
|
+
name: "Insider Trading",
|
|
405
|
+
version: "1.0.0",
|
|
406
|
+
description: "SEC Form 4 insider transaction activity",
|
|
407
|
+
|
|
408
|
+
setup(ctx) {
|
|
409
|
+
ctx.registerDetailTab({
|
|
410
|
+
id: "insider",
|
|
411
|
+
name: "Insider",
|
|
412
|
+
order: 47,
|
|
413
|
+
component: InsiderTab,
|
|
414
|
+
isVisible: ({ ticker }) => isUsEquityTicker(ticker),
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parseForm4Xml, transactionTypeLabel } from "./insider-data";
|
|
3
|
+
|
|
4
|
+
const SAMPLE_FORM4 = `<?xml version="1.0"?>
|
|
5
|
+
<ownershipDocument>
|
|
6
|
+
<reportingOwner>
|
|
7
|
+
<reportingOwnerId><rptOwnerName>COOK TIMOTHY D</rptOwnerName></reportingOwnerId>
|
|
8
|
+
<reportingOwnerRelationship>
|
|
9
|
+
<isOfficer>1</isOfficer>
|
|
10
|
+
<officerTitle>Chief Executive Officer</officerTitle>
|
|
11
|
+
</reportingOwnerRelationship>
|
|
12
|
+
</reportingOwner>
|
|
13
|
+
<nonDerivativeTable>
|
|
14
|
+
<nonDerivativeTransaction>
|
|
15
|
+
<transactionDate><value>2024-04-01</value></transactionDate>
|
|
16
|
+
<transactionCoding><transactionCode>S</transactionCode></transactionCoding>
|
|
17
|
+
<transactionAmounts>
|
|
18
|
+
<transactionShares><value>50000</value></transactionShares>
|
|
19
|
+
<transactionPricePerShare><value>171.50</value></transactionPricePerShare>
|
|
20
|
+
</transactionAmounts>
|
|
21
|
+
<postTransactionAmounts>
|
|
22
|
+
<sharesOwnedFollowingTransaction><value>3500000</value></sharesOwnedFollowingTransaction>
|
|
23
|
+
</postTransactionAmounts>
|
|
24
|
+
</nonDerivativeTransaction>
|
|
25
|
+
</nonDerivativeTable>
|
|
26
|
+
</ownershipDocument>`;
|
|
27
|
+
|
|
28
|
+
describe("parseForm4Xml", () => {
|
|
29
|
+
test("parses insider sale from Form 4 XML", () => {
|
|
30
|
+
const result = parseForm4Xml(SAMPLE_FORM4);
|
|
31
|
+
expect(result).not.toBeNull();
|
|
32
|
+
expect(result!.reportedName).toBe("COOK TIMOTHY D");
|
|
33
|
+
expect(result!.title).toBe("Chief Executive Officer");
|
|
34
|
+
expect(result!.transactionType).toBe("S");
|
|
35
|
+
expect(result!.shares).toBe(50000);
|
|
36
|
+
expect(result!.pricePerShare).toBe(171.50);
|
|
37
|
+
expect(result!.totalValue).toBe(50000 * 171.50);
|
|
38
|
+
expect(result!.sharesOwned).toBe(3500000);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns null for non-Form-4 XML", () => {
|
|
42
|
+
expect(parseForm4Xml("<html>not a form</html>")).toBeNull();
|
|
43
|
+
expect(parseForm4Xml("")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("transactionTypeLabel", () => {
|
|
48
|
+
test("maps codes to labels", () => {
|
|
49
|
+
expect(transactionTypeLabel("P")).toBe("BUY");
|
|
50
|
+
expect(transactionTypeLabel("S")).toBe("SELL");
|
|
51
|
+
expect(transactionTypeLabel("A")).toBe("AWARD");
|
|
52
|
+
expect(transactionTypeLabel("D")).toBe("DISPOSE");
|
|
53
|
+
expect(transactionTypeLabel("")).toBe("—");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface InsiderTransaction {
|
|
2
|
+
filingDate: Date;
|
|
3
|
+
reportedName: string;
|
|
4
|
+
title: string;
|
|
5
|
+
transactionType: "P" | "S" | "A" | "D" | "";
|
|
6
|
+
shares: number;
|
|
7
|
+
pricePerShare: number | null;
|
|
8
|
+
totalValue: number | null;
|
|
9
|
+
sharesOwned: number | null;
|
|
10
|
+
form: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseForm4Xml(xml: string): InsiderTransaction | null {
|
|
14
|
+
const nameMatch = xml.match(/<rptOwnerName>([^<]+)<\/rptOwnerName>/);
|
|
15
|
+
if (!nameMatch) return null;
|
|
16
|
+
|
|
17
|
+
const titleMatch = xml.match(/<officerTitle>([^<]+)<\/officerTitle>/);
|
|
18
|
+
const codeMatch = xml.match(/<transactionCode>([^<]+)<\/transactionCode>/);
|
|
19
|
+
const sharesMatch = xml.match(/<transactionShares>[\s\S]*?<value>([^<]+)<\/value>/);
|
|
20
|
+
const priceMatch = xml.match(/<transactionPricePerShare>[\s\S]*?<value>([^<]+)<\/value>/);
|
|
21
|
+
const ownedMatch = xml.match(/<sharesOwnedFollowingTransaction>[\s\S]*?<value>([^<]+)<\/value>/);
|
|
22
|
+
const dateMatch = xml.match(/<transactionDate>[\s\S]*?<value>([^<]+)<\/value>/);
|
|
23
|
+
|
|
24
|
+
const shares = sharesMatch ? parseFloat(sharesMatch[1]!) : 0;
|
|
25
|
+
const price = priceMatch ? parseFloat(priceMatch[1]!) : null;
|
|
26
|
+
const code = codeMatch?.[1] ?? "";
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
filingDate: dateMatch ? new Date(dateMatch[1]!) : new Date(),
|
|
30
|
+
reportedName: nameMatch[1]!.trim(),
|
|
31
|
+
title: titleMatch?.[1]?.trim() ?? "",
|
|
32
|
+
transactionType: (code === "P" || code === "S" || code === "A" || code === "D") ? code : "",
|
|
33
|
+
shares: isNaN(shares) ? 0 : shares,
|
|
34
|
+
pricePerShare: price !== null && !isNaN(price) ? price : null,
|
|
35
|
+
totalValue: price !== null && !isNaN(price) && !isNaN(shares) ? shares * price : null,
|
|
36
|
+
sharesOwned: ownedMatch ? parseFloat(ownedMatch[1]!) : null,
|
|
37
|
+
form: "4",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function transactionTypeLabel(type: InsiderTransaction["transactionType"]): string {
|
|
42
|
+
switch (type) {
|
|
43
|
+
case "P": return "BUY";
|
|
44
|
+
case "S": return "SELL";
|
|
45
|
+
case "A": return "AWARD";
|
|
46
|
+
case "D": return "DISPOSE";
|
|
47
|
+
default: return "—";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { saveConfig } from "../../data/config-store";
|
|
2
2
|
import { findPaneInstance, type LayoutConfig } from "../../types/config";
|
|
3
|
-
import type { GloomPlugin, GloomPluginContext } from "../../types/plugin";
|
|
3
|
+
import type { AppNotificationRequest, GloomPlugin, GloomPluginContext } from "../../types/plugin";
|
|
4
4
|
import type { AppAction } from "../../state/app-context";
|
|
5
5
|
import { getSharedRegistry } from "../registry";
|
|
6
|
+
import { notifyGridlockComplete } from "../gridlock-notification";
|
|
6
7
|
import {
|
|
7
8
|
dockPane,
|
|
8
9
|
floatPane,
|
|
@@ -54,6 +55,10 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
54
55
|
description: "Pane layout management commands",
|
|
55
56
|
|
|
56
57
|
setup(ctx) {
|
|
58
|
+
const notify = (body: string, options?: Omit<AppNotificationRequest, "body">) => {
|
|
59
|
+
ctx.notify({ body, ...options });
|
|
60
|
+
};
|
|
61
|
+
|
|
57
62
|
ctx.registerCommand({
|
|
58
63
|
id: "float-pane",
|
|
59
64
|
label: "Float Pane",
|
|
@@ -68,7 +73,7 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
68
73
|
const { layout, termWidth, termHeight, focusedPaneId } = getStateRef();
|
|
69
74
|
const focusedPane = getFocusedPane(layout, focusedPaneId);
|
|
70
75
|
if (!focusedPane || !isPaneDocked(layout, focusedPane.instanceId)) {
|
|
71
|
-
|
|
76
|
+
notify("Focus a docked pane to float it", { type: "info" });
|
|
72
77
|
return;
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -93,7 +98,7 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
93
98
|
const { layout, focusedPaneId } = getStateRef();
|
|
94
99
|
const focusedPane = getFocusedPane(layout, focusedPaneId);
|
|
95
100
|
if (!focusedPane || !layout.floating.some((entry) => entry.instanceId === focusedPane.instanceId)) {
|
|
96
|
-
|
|
101
|
+
notify("Focus a floating pane to dock it", { type: "info" });
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
99
104
|
|
|
@@ -112,7 +117,7 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
112
117
|
hidden: () => true,
|
|
113
118
|
execute: async () => {
|
|
114
119
|
ctx.openCommandBar("NP ");
|
|
115
|
-
|
|
120
|
+
notify("Choose a pane template to create", { type: "info" });
|
|
116
121
|
},
|
|
117
122
|
});
|
|
118
123
|
|
|
@@ -121,12 +126,15 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
121
126
|
label: "Gridlock All Windows",
|
|
122
127
|
description: "Arrange all visible panes into a tiled grid",
|
|
123
128
|
keywords: ["grid", "gridlock", "tile", "arrange", "windows", "layout"],
|
|
129
|
+
shortcut: "GL",
|
|
124
130
|
category: "config",
|
|
125
131
|
execute: async () => {
|
|
126
132
|
if (!getStateRef) return;
|
|
127
133
|
const { layout, termWidth, termHeight } = getStateRef();
|
|
128
134
|
persistLayout(gridlockAllPanes(layout, { x: 0, y: 0, width: termWidth, height: termHeight }));
|
|
129
|
-
ctx.
|
|
135
|
+
notifyGridlockComplete(ctx.notify, () => {
|
|
136
|
+
dispatchRef?.({ type: "UNDO_LAYOUT" });
|
|
137
|
+
});
|
|
130
138
|
},
|
|
131
139
|
});
|
|
132
140
|
|
|
@@ -141,7 +149,7 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
141
149
|
const { layout, focusedPaneId } = getStateRef();
|
|
142
150
|
const focusedPane = getFocusedPane(layout, focusedPaneId);
|
|
143
151
|
if (!focusedPane || !isPaneInLayout(layout, focusedPane.instanceId)) {
|
|
144
|
-
|
|
152
|
+
notify("Focus a pane to remove it", { type: "info" });
|
|
145
153
|
return;
|
|
146
154
|
}
|
|
147
155
|
persistLayout(removePane(layout, focusedPane.instanceId));
|
|
@@ -158,11 +166,11 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
158
166
|
execute: async (values) => {
|
|
159
167
|
const name = values?.name?.trim();
|
|
160
168
|
if (!name) {
|
|
161
|
-
|
|
169
|
+
notify("Layout name is required", { type: "error" });
|
|
162
170
|
return;
|
|
163
171
|
}
|
|
164
172
|
dispatchRef?.({ type: "NEW_LAYOUT", name });
|
|
165
|
-
|
|
173
|
+
notify(`Layout "${name}" created`, { type: "success" });
|
|
166
174
|
},
|
|
167
175
|
});
|
|
168
176
|
|
|
@@ -191,13 +199,13 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
191
199
|
if (!registry) return;
|
|
192
200
|
const config = registry.getConfigFn();
|
|
193
201
|
if (config.layouts.length <= 1) {
|
|
194
|
-
|
|
202
|
+
notify("Can't delete the only layout", { type: "error" });
|
|
195
203
|
return;
|
|
196
204
|
}
|
|
197
205
|
const index = config.activeLayoutIndex;
|
|
198
206
|
const name = config.layouts[index]!.name;
|
|
199
207
|
dispatchRef?.({ type: "DELETE_LAYOUT", index });
|
|
200
|
-
|
|
208
|
+
notify(`Layout "${name}" deleted`, { type: "success" });
|
|
201
209
|
},
|
|
202
210
|
});
|
|
203
211
|
|
|
@@ -211,13 +219,13 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
211
219
|
execute: async (values) => {
|
|
212
220
|
const name = values?.name?.trim();
|
|
213
221
|
if (!name) {
|
|
214
|
-
|
|
222
|
+
notify("Name is required", { type: "error" });
|
|
215
223
|
return;
|
|
216
224
|
}
|
|
217
225
|
const registry = getSharedRegistry();
|
|
218
226
|
if (!registry) return;
|
|
219
227
|
dispatchRef?.({ type: "RENAME_LAYOUT", index: registry.getConfigFn().activeLayoutIndex, name });
|
|
220
|
-
|
|
228
|
+
notify(`Layout renamed to "${name}"`, { type: "success" });
|
|
221
229
|
},
|
|
222
230
|
});
|
|
223
231
|
|
|
@@ -231,7 +239,7 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
231
239
|
const registry = getSharedRegistry();
|
|
232
240
|
if (!registry) return;
|
|
233
241
|
dispatchRef?.({ type: "DUPLICATE_LAYOUT", index: registry.getConfigFn().activeLayoutIndex });
|
|
234
|
-
|
|
242
|
+
notify("Layout duplicated", { type: "success" });
|
|
235
243
|
},
|
|
236
244
|
});
|
|
237
245
|
|
|
@@ -247,11 +255,11 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
247
255
|
const focusedPane = getFocusedPane(layout, focusedPaneId);
|
|
248
256
|
const dockedPaneIds = getDockedPaneIds(layout);
|
|
249
257
|
if (dockedPaneIds.length < 2) {
|
|
250
|
-
|
|
258
|
+
notify("Need at least 2 docked panes to swap", { type: "info" });
|
|
251
259
|
return;
|
|
252
260
|
}
|
|
253
261
|
if (!focusedPane || !isPaneDocked(layout, focusedPane.instanceId)) {
|
|
254
|
-
|
|
262
|
+
notify("Focus a docked pane to swap it", { type: "info" });
|
|
255
263
|
return;
|
|
256
264
|
}
|
|
257
265
|
|
|
@@ -262,7 +270,7 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
ctx.openCommandBar("LAY ");
|
|
265
|
-
|
|
273
|
+
notify("Choose a swap target from layout mode", { type: "info" });
|
|
266
274
|
},
|
|
267
275
|
});
|
|
268
276
|
},
|