gloomberb 0.1.0 → 0.3.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.
Files changed (311) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +68 -121
  3. package/bin/gloomberb +3 -0
  4. package/package.json +13 -4
  5. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  6. package/src/app.tsx +1330 -0
  7. package/src/brokers/sync-broker-instance.test.ts +100 -0
  8. package/src/brokers/sync-broker-instance.ts +341 -0
  9. package/src/cli/commands/plugins.ts +173 -0
  10. package/src/cli/commands/portfolio.ts +210 -0
  11. package/src/cli/commands/search.ts +120 -0
  12. package/src/cli/commands/ticker.ts +375 -0
  13. package/src/cli/commands/watchlist.ts +210 -0
  14. package/src/cli/context.ts +62 -0
  15. package/src/cli/errors.ts +13 -0
  16. package/src/cli/helpers.ts +85 -0
  17. package/src/cli/index.ts +143 -0
  18. package/src/cli/ticker-resolution.ts +39 -0
  19. package/src/cli/types.ts +15 -0
  20. package/src/cli.test.ts +361 -0
  21. package/src/components/chart/chart-data.ts +239 -0
  22. package/src/components/chart/chart-renderer.ts +1199 -0
  23. package/src/components/chart/chart-types.ts +82 -0
  24. package/src/components/chart/chart-viewport.ts +21 -0
  25. package/src/components/chart/chart.test.ts +439 -0
  26. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  27. package/src/components/chart/comparison-chart-data.ts +316 -0
  28. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  29. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  30. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  31. package/src/components/chart/cursor-motion.ts +48 -0
  32. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  33. package/src/components/chart/native/kitty-adapter.ts +12 -0
  34. package/src/components/chart/native/kitty-manager.ts +142 -0
  35. package/src/components/chart/native/kitty-protocol.ts +110 -0
  36. package/src/components/chart/native/kitty-support.ts +71 -0
  37. package/src/components/chart/native/kitty.test.ts +620 -0
  38. package/src/components/chart/native/renderer-selection.ts +24 -0
  39. package/src/components/chart/native/surface-manager.ts +225 -0
  40. package/src/components/chart/native/surface-sync.ts +34 -0
  41. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  42. package/src/components/chart/stock-chart.test.ts +107 -0
  43. package/src/components/chart/stock-chart.tsx +1556 -0
  44. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  45. package/src/components/command-bar/command-bar.test.tsx +1524 -0
  46. package/src/components/command-bar/command-bar.tsx +4823 -0
  47. package/src/components/command-bar/command-registry.ts +261 -0
  48. package/src/components/command-bar/helpers.test.ts +224 -0
  49. package/src/components/command-bar/helpers.ts +371 -0
  50. package/src/components/command-bar/root-shortcuts.ts +158 -0
  51. package/src/components/command-bar/view-model.test.ts +152 -0
  52. package/src/components/command-bar/view-model.ts +185 -0
  53. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  54. package/src/components/command-bar/workflow-ops.ts +433 -0
  55. package/src/components/command-bar/workflow-types.ts +127 -0
  56. package/src/components/detail-feed-view.test.tsx +105 -0
  57. package/src/components/detail-feed-view.tsx +295 -0
  58. package/src/components/index.ts +32 -0
  59. package/src/components/layout/floating-pane.tsx +49 -0
  60. package/src/components/layout/header.tsx +129 -0
  61. package/src/components/layout/pane-header.tsx +44 -0
  62. package/src/components/layout/pane.tsx +49 -0
  63. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  64. package/src/components/layout/pixel-mouse.test.ts +40 -0
  65. package/src/components/layout/shell.test.tsx +625 -0
  66. package/src/components/layout/shell.tsx +1200 -0
  67. package/src/components/layout/status-bar.test.tsx +173 -0
  68. package/src/components/layout/status-bar.tsx +138 -0
  69. package/src/components/markdown-editor.tsx +134 -0
  70. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  71. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  72. package/src/components/pane-settings-dialog.tsx +468 -0
  73. package/src/components/pane-template-wizard.test.tsx +52 -0
  74. package/src/components/pane-template-wizard.tsx +199 -0
  75. package/src/components/price-selector-dialog.tsx +195 -0
  76. package/src/components/spinner.tsx +2 -0
  77. package/src/components/tab-bar.tsx +17 -0
  78. package/src/components/ticker-badge-text.test.tsx +160 -0
  79. package/src/components/ticker-badge-text.tsx +58 -0
  80. package/src/components/ticker-badge.tsx +88 -0
  81. package/src/components/ticker-list-table.tsx +188 -0
  82. package/src/components/toggle-list.tsx +74 -0
  83. package/src/components/ui/button.tsx +77 -0
  84. package/src/components/ui/data-table.tsx +210 -0
  85. package/src/components/ui/external-link.tsx +22 -0
  86. package/src/components/ui/fields.tsx +187 -0
  87. package/src/components/ui/frame.tsx +73 -0
  88. package/src/components/ui/index.ts +53 -0
  89. package/src/components/ui/list-view.tsx +166 -0
  90. package/src/components/ui/loading.tsx +70 -0
  91. package/src/components/ui/status.tsx +83 -0
  92. package/src/components/ui/tabs.tsx +47 -0
  93. package/src/components/ui/toggle.tsx +164 -0
  94. package/src/components/ui/ui.test.tsx +134 -0
  95. package/src/data/app-persistence.ts +25 -0
  96. package/src/data/config-store.test.ts +310 -0
  97. package/src/data/config-store.ts +469 -0
  98. package/src/data/plugin-state-store.ts +102 -0
  99. package/src/data/resource-store.ts +323 -0
  100. package/src/data/session-store.ts +52 -0
  101. package/src/data/sqlite-cache.test.ts +103 -0
  102. package/src/data/sqlite-database.ts +62 -0
  103. package/src/data/sqlite-json.ts +19 -0
  104. package/src/data/ticker-repository.ts +45 -0
  105. package/src/data/ticker-store.ts +46 -0
  106. package/src/index.tsx +67 -0
  107. package/src/market-data/coordinator.test.ts +369 -0
  108. package/src/market-data/coordinator.ts +580 -0
  109. package/src/market-data/hooks.tsx +234 -0
  110. package/src/market-data/query-store.ts +28 -0
  111. package/src/market-data/request-types.test.ts +64 -0
  112. package/src/market-data/request-types.ts +70 -0
  113. package/src/market-data/result-types.ts +56 -0
  114. package/src/market-data/selectors.ts +166 -0
  115. package/src/market-data/trace.ts +7 -0
  116. package/src/pane-settings.test.ts +71 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
  119. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  120. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  121. package/src/plugins/builtin/ai/index.tsx +86 -0
  122. package/src/plugins/builtin/ai/providers.ts +99 -0
  123. package/src/plugins/builtin/ai/runner.ts +82 -0
  124. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  125. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  126. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  127. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  128. package/src/plugins/builtin/ai/settings.ts +79 -0
  129. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  130. package/src/plugins/builtin/ai/utils.ts +71 -0
  131. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  132. package/src/plugins/builtin/ask-ai.tsx +9 -0
  133. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  134. package/src/plugins/builtin/chat-controller.ts +340 -0
  135. package/src/plugins/builtin/chat.test.tsx +451 -0
  136. package/src/plugins/builtin/chat.tsx +620 -0
  137. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  138. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  139. package/src/plugins/builtin/data-providers.ts +16 -0
  140. package/src/plugins/builtin/debug.tsx +351 -0
  141. package/src/plugins/builtin/help.test.tsx +110 -0
  142. package/src/plugins/builtin/help.tsx +281 -0
  143. package/src/plugins/builtin/layout-manager.tsx +264 -0
  144. package/src/plugins/builtin/manual-entry.tsx +25 -0
  145. package/src/plugins/builtin/news.tsx +143 -0
  146. package/src/plugins/builtin/notes-files.ts +58 -0
  147. package/src/plugins/builtin/notes.tsx +396 -0
  148. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  149. package/src/plugins/builtin/options-availability.ts +158 -0
  150. package/src/plugins/builtin/options.tsx +364 -0
  151. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  152. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  153. package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
  156. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  157. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  158. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  159. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  160. package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
  161. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  162. package/src/plugins/builtin/sec.tsx +250 -0
  163. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  164. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  165. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  166. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  167. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  168. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  169. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  170. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  171. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  172. package/src/plugins/builtin/yahoo.ts +18 -0
  173. package/src/plugins/event-bus.ts +37 -0
  174. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  175. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  176. package/src/plugins/ibkr/account-cache.ts +137 -0
  177. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  178. package/src/plugins/ibkr/config.test.ts +68 -0
  179. package/src/plugins/ibkr/config.ts +262 -0
  180. package/src/plugins/ibkr/dialogs.tsx +83 -0
  181. package/src/plugins/ibkr/flex.test.ts +107 -0
  182. package/src/plugins/ibkr/flex.ts +267 -0
  183. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  184. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  185. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  186. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  187. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  188. package/src/plugins/ibkr/index.test.tsx +183 -0
  189. package/src/plugins/ibkr/index.tsx +153 -0
  190. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  191. package/src/plugins/ibkr/instance-selection.ts +36 -0
  192. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  193. package/src/plugins/ibkr/price-normalization.ts +59 -0
  194. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  195. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  196. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  197. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  198. package/src/plugins/ibkr/trade-utils.ts +149 -0
  199. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  200. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  201. package/src/plugins/ibkr/trading-state.ts +339 -0
  202. package/src/plugins/loader.ts +71 -0
  203. package/src/plugins/pane-manager.test.ts +103 -0
  204. package/src/plugins/pane-manager.ts +974 -0
  205. package/src/plugins/plugin-persistence.ts +85 -0
  206. package/src/plugins/plugin-runtime.test.tsx +185 -0
  207. package/src/plugins/plugin-runtime.tsx +175 -0
  208. package/src/plugins/prediction-markets/cache.ts +114 -0
  209. package/src/plugins/prediction-markets/categories.ts +223 -0
  210. package/src/plugins/prediction-markets/chart.tsx +512 -0
  211. package/src/plugins/prediction-markets/columns.ts +133 -0
  212. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  213. package/src/plugins/prediction-markets/controller.ts +725 -0
  214. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  215. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  216. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  217. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  218. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  219. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  220. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  221. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  222. package/src/plugins/prediction-markets/index.tsx +102 -0
  223. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  224. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  225. package/src/plugins/prediction-markets/launch.ts +240 -0
  226. package/src/plugins/prediction-markets/metrics.ts +311 -0
  227. package/src/plugins/prediction-markets/navigation.ts +90 -0
  228. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  229. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  230. package/src/plugins/prediction-markets/pane.tsx +294 -0
  231. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  232. package/src/plugins/prediction-markets/rows.ts +229 -0
  233. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  234. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  235. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  236. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  237. package/src/plugins/prediction-markets/settings.ts +164 -0
  238. package/src/plugins/prediction-markets/status.ts +24 -0
  239. package/src/plugins/prediction-markets/table.tsx +84 -0
  240. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  241. package/src/plugins/prediction-markets/types.ts +186 -0
  242. package/src/plugins/registry.ts +633 -0
  243. package/src/sources/exchange-rates.ts +18 -0
  244. package/src/sources/gloomberb-cloud.test.ts +199 -0
  245. package/src/sources/gloomberb-cloud.ts +326 -0
  246. package/src/sources/provider-errors.ts +19 -0
  247. package/src/sources/provider-router.test.ts +1476 -0
  248. package/src/sources/provider-router.ts +1405 -0
  249. package/src/sources/sec-edgar.test.ts +286 -0
  250. package/src/sources/sec-edgar.ts +526 -0
  251. package/src/sources/yahoo-finance.test.ts +14 -0
  252. package/src/sources/yahoo-finance.ts +1017 -0
  253. package/src/state/app-activity.test.ts +41 -0
  254. package/src/state/app-activity.ts +106 -0
  255. package/src/state/app-bootstrap.test.ts +471 -0
  256. package/src/state/app-bootstrap.ts +308 -0
  257. package/src/state/app-context.test.ts +406 -0
  258. package/src/state/app-context.test.tsx +132 -0
  259. package/src/state/app-context.tsx +969 -0
  260. package/src/state/selectors.ts +67 -0
  261. package/src/state/session-persistence.test.ts +128 -0
  262. package/src/state/session-persistence.ts +161 -0
  263. package/src/state/ticker-refresh-queue.test.ts +46 -0
  264. package/src/state/ticker-refresh-queue.ts +40 -0
  265. package/src/state/use-inline-tickers.ts +174 -0
  266. package/src/state/use-quote-streaming.test.ts +37 -0
  267. package/src/state/use-quote-streaming.test.tsx +70 -0
  268. package/src/state/use-quote-streaming.ts +80 -0
  269. package/src/state/use-session-snapshot.ts +84 -0
  270. package/src/test-support/data-provider.ts +22 -0
  271. package/src/theme/color-utils.ts +67 -0
  272. package/src/theme/colors.ts +128 -0
  273. package/src/theme/themes.test.ts +45 -0
  274. package/src/theme/themes.ts +491 -0
  275. package/src/types/broker.ts +105 -0
  276. package/src/types/config.ts +434 -0
  277. package/src/types/data-provider.ts +81 -0
  278. package/src/types/financials.ts +142 -0
  279. package/src/types/instrument.ts +29 -0
  280. package/src/types/persistence.ts +21 -0
  281. package/src/types/plugin.ts +354 -0
  282. package/src/types/ticker.ts +58 -0
  283. package/src/types/trading.ts +85 -0
  284. package/src/updater.test.ts +162 -0
  285. package/src/updater.ts +247 -0
  286. package/src/utils/api-client.ts +666 -0
  287. package/src/utils/ascii-chart.ts +172 -0
  288. package/src/utils/broker-instances.ts +47 -0
  289. package/src/utils/cli-output.ts +83 -0
  290. package/src/utils/currency-units.test.ts +49 -0
  291. package/src/utils/currency-units.ts +82 -0
  292. package/src/utils/debug-log.ts +133 -0
  293. package/src/utils/exchange-rate-map.ts +8 -0
  294. package/src/utils/format.ts +158 -0
  295. package/src/utils/fuzzy-search.ts +34 -0
  296. package/src/utils/market-status.ts +95 -0
  297. package/src/utils/options.ts +86 -0
  298. package/src/utils/price-history.ts +39 -0
  299. package/src/utils/quote-time.test.ts +22 -0
  300. package/src/utils/quote-time.ts +50 -0
  301. package/src/utils/sec.test.ts +55 -0
  302. package/src/utils/sec.ts +45 -0
  303. package/src/utils/ticker-list.test.ts +22 -0
  304. package/src/utils/ticker-list.ts +35 -0
  305. package/src/utils/ticker-metadata.ts +51 -0
  306. package/src/utils/ticker-search.test.ts +227 -0
  307. package/src/utils/ticker-search.ts +764 -0
  308. package/src/utils/ticker-tokenizer.test.ts +48 -0
  309. package/src/utils/ticker-tokenizer.ts +72 -0
  310. package/src/version.ts +1 -0
  311. package/install.mjs +0 -69
@@ -0,0 +1,633 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ import { createReactSlotRegistry, createSlot } from "@opentui/react";
3
+ import { createElement } from "react";
4
+ import type { AppPersistence } from "../data/app-persistence";
5
+ import type { TickerRepository } from "../data/ticker-repository";
6
+ import type { BrokerAdapter } from "../types/broker";
7
+ import type { BrokerInstanceConfig, LayoutConfig, PaneInstanceConfig } from "../types/config";
8
+ import type { DataProvider } from "../types/data-provider";
9
+ import type { TickerFinancials } from "../types/financials";
10
+ import type {
11
+ CommandDef,
12
+ CustomColumnDef,
13
+ DetailTabDef,
14
+ GloomPlugin,
15
+ GloomPluginContext,
16
+ GloomSlots,
17
+ KeyboardShortcut,
18
+ PaneDef,
19
+ PaneSettingsContext,
20
+ PaneSettingsDef,
21
+ PaneTemplateCreateOptions,
22
+ PaneTemplateDef,
23
+ PluginPaneSettingsState,
24
+ PluginResumeState,
25
+ TickerAction,
26
+ } from "../types/plugin";
27
+ import type { TickerRecord } from "../types/ticker";
28
+ import { addPaneFloating, removePane } from "./pane-manager";
29
+ import { EventBus, type PluginEvents } from "./event-bus";
30
+ import { createPaneInstance } from "../types/config";
31
+ import { createPluginPersistence } from "./plugin-persistence";
32
+ import { debugLog } from "../utils/debug-log";
33
+ import { PluginRenderProvider, type PluginRuntimeAccess } from "./plugin-runtime";
34
+ import {
35
+ resolveCollectionForPane,
36
+ resolveTickerForPane,
37
+ type PaneRuntimeState,
38
+ } from "../state/app-context";
39
+ import { deletePaneSetting, getPaneSettings, setPaneSetting } from "../pane-settings";
40
+
41
+ let sharedDataProvider: DataProvider | undefined;
42
+ let sharedRegistry: PluginRegistry | undefined;
43
+ const SLOT_REGISTRY_CONTEXT: Record<string, never> = {};
44
+
45
+ export function getSharedDataProvider(): DataProvider | undefined { return sharedDataProvider; }
46
+ export function getSharedRegistry(): PluginRegistry | undefined { return sharedRegistry; }
47
+ export function setSharedDataProviderForTests(provider: DataProvider | undefined): void {
48
+ sharedDataProvider = provider;
49
+ }
50
+ export function setSharedRegistryForTests(registry: PluginRegistry | undefined): void {
51
+ sharedRegistry = registry;
52
+ }
53
+
54
+ interface PluginItems {
55
+ panes: string[];
56
+ paneTemplates: string[];
57
+ commands: string[];
58
+ columns: string[];
59
+ brokers: string[];
60
+ dataProviders: string[];
61
+ detailTabs: string[];
62
+ shortcuts: string[];
63
+ tickerActions: string[];
64
+ eventDisposers: Array<() => void>;
65
+ }
66
+
67
+ export class PluginRegistry implements PluginRuntimeAccess {
68
+ private slotRegistry;
69
+ private plugins = new Map<string, GloomPlugin>();
70
+ private unregisterFns = new Map<string, () => void>();
71
+ private pluginItems = new Map<string, PluginItems>();
72
+ private commandOwners = new Map<string, string>();
73
+ private paneTemplateOwners = new Map<string, string>();
74
+ private shortcutOwners = new Map<string, string>();
75
+ private dataProviderOwners = new Map<string, string>();
76
+ private pluginResumeListeners = new Map<string, Set<() => void>>();
77
+
78
+ private panesMap = new Map<string, PaneDef>();
79
+ private paneTemplatesMap = new Map<string, PaneTemplateDef>();
80
+ private commandsMap = new Map<string, CommandDef>();
81
+ private columnsMap = new Map<string, CustomColumnDef>();
82
+ private brokersMap = new Map<string, BrokerAdapter>();
83
+ private dataProvidersMap = new Map<string, DataProvider>();
84
+ private detailTabsMap = new Map<string, DetailTabDef>();
85
+ private shortcutsMap = new Map<string, KeyboardShortcut>();
86
+ private tickerActionsMap = new Map<string, TickerAction>();
87
+
88
+ readonly events: EventBus;
89
+ readonly dataProvider: DataProvider;
90
+ readonly tickerRepository: TickerRepository;
91
+ readonly persistence: AppPersistence;
92
+
93
+ getTickerFn: ((symbol: string) => TickerRecord | null) = () => null;
94
+ getDataFn: ((symbol: string) => TickerFinancials | null) = () => null;
95
+ getConfigFn: (() => import("../types/config").AppConfig) = () => { throw new Error("getConfigFn not set"); };
96
+ createBrokerInstanceFn: ((brokerType: string, label: string, values: Record<string, unknown>) => Promise<BrokerInstanceConfig>) = async () => {
97
+ throw new Error("createBrokerInstanceFn not set");
98
+ };
99
+ updateBrokerInstanceFn: ((instanceId: string, values: Record<string, unknown>) => Promise<void>) = async () => {};
100
+ syncBrokerInstanceFn: ((instanceId: string) => Promise<void>) = async () => {};
101
+ removeBrokerInstanceFn: ((instanceId: string) => Promise<void>) = async () => {};
102
+
103
+ selectTickerFn: ((symbol: string, paneId?: string) => void) = () => {};
104
+ switchPanelFn: ((panel: "left" | "right") => void) = () => {};
105
+ switchTabFn: ((tabId: string, paneId?: string) => void) = () => {};
106
+ openCommandBarFn: ((query?: string) => void) = () => {};
107
+ openPaneSettingsFn: ((paneId?: string) => void) = () => {};
108
+ showPaneFn: ((paneId: string) => void) = () => {};
109
+ createPaneFromTemplateFn: ((templateId: string, options?: PaneTemplateCreateOptions) => void) = () => {};
110
+ createPaneFromTemplateAsyncFn: ((templateId: string, options?: PaneTemplateCreateOptions) => Promise<void>) = async () => {};
111
+ hidePaneFn: ((paneId: string) => void) = () => {};
112
+ focusPaneFn: ((paneId: string) => void) = () => {};
113
+ pinTickerFn: ((symbol: string, options?: { floating?: boolean; paneType?: string }) => void) = () => {};
114
+
115
+ getLayoutFn: (() => LayoutConfig) = () => ({ dockRoot: null, instances: [], floating: [] });
116
+ updateLayoutFn: ((layout: LayoutConfig) => void) = () => {};
117
+ getTermSizeFn: (() => { width: number; height: number }) = () => ({ width: 120, height: 40 });
118
+
119
+ showToastFn: ((message: string, options?: { duration?: number; type?: "info" | "success" | "error" }) => void) = () => {};
120
+ getPaneRuntimeStateFn: ((paneId: string) => PaneRuntimeState | null) = () => null;
121
+ updatePaneRuntimeStateFn: ((paneId: string, patch: Partial<PaneRuntimeState>) => void) = () => {};
122
+ applyPaneSettingValueFn: ((paneId: string, field: import("../types/plugin").PaneSettingField, value: unknown) => Promise<void>) = async () => {};
123
+ getPluginConfigValueFn: (<T = unknown>(pluginId: string, key: string) => T | null) = () => null;
124
+ setPluginConfigValueFn: ((pluginId: string, key: string, value: unknown) => Promise<void>) = async () => {};
125
+ deletePluginConfigValueFn: ((pluginId: string, key: string) => Promise<void>) = async () => {};
126
+
127
+ readonly Slot;
128
+
129
+ constructor(renderer: CliRenderer, dataProvider: DataProvider, tickerRepository: TickerRepository, persistence: AppPersistence) {
130
+ this.dataProvider = dataProvider;
131
+ this.tickerRepository = tickerRepository;
132
+ this.persistence = persistence;
133
+ this.events = new EventBus();
134
+
135
+ sharedDataProvider = dataProvider;
136
+ sharedRegistry = this;
137
+ (globalThis as any).__gloomRegistry = this;
138
+
139
+ this.slotRegistry = createReactSlotRegistry<GloomSlots & Record<string, object>>(renderer, SLOT_REGISTRY_CONTEXT);
140
+ this.Slot = createSlot(this.slotRegistry) as any;
141
+ }
142
+
143
+ get panes(): ReadonlyMap<string, PaneDef> { return this.panesMap; }
144
+ get paneTemplates(): ReadonlyMap<string, PaneTemplateDef> { return this.paneTemplatesMap; }
145
+ get commands(): ReadonlyMap<string, CommandDef> { return this.commandsMap; }
146
+ get columns(): ReadonlyMap<string, CustomColumnDef> { return this.columnsMap; }
147
+ get brokers(): ReadonlyMap<string, BrokerAdapter> { return this.brokersMap; }
148
+ get dataProviders(): ReadonlyMap<string, DataProvider> { return this.dataProvidersMap; }
149
+ get detailTabs(): ReadonlyMap<string, DetailTabDef> { return this.detailTabsMap; }
150
+ get shortcuts(): ReadonlyMap<string, KeyboardShortcut> { return this.shortcutsMap; }
151
+ get tickerActions(): ReadonlyMap<string, TickerAction> { return this.tickerActionsMap; }
152
+ get allPlugins(): ReadonlyMap<string, GloomPlugin> { return this.plugins; }
153
+
154
+ getDataProviderPluginId(providerId: string): string | undefined {
155
+ return this.dataProviderOwners.get(providerId);
156
+ }
157
+
158
+ getEnabledDataProviders(): DataProvider[] {
159
+ const disabledPlugins = new Set(this.getConfigFn().disabledPlugins ?? []);
160
+ return [...this.dataProvidersMap.values()].filter((provider) => {
161
+ const ownerId = this.dataProviderOwners.get(provider.id);
162
+ return !ownerId || !disabledPlugins.has(ownerId);
163
+ });
164
+ }
165
+
166
+ getActiveProvider(): DataProvider | undefined {
167
+ let active: DataProvider | undefined;
168
+ for (const provider of this.dataProvidersMap.values()) active = provider;
169
+ return active;
170
+ }
171
+
172
+ getPluginPaneIds(pluginId: string): string[] {
173
+ return this.pluginItems.get(pluginId)?.panes ?? [];
174
+ }
175
+
176
+ getPluginPaneTemplateIds(pluginId: string): string[] {
177
+ return this.pluginItems.get(pluginId)?.paneTemplates ?? [];
178
+ }
179
+
180
+ private getOrCreatePluginItems(pluginId: string): PluginItems {
181
+ const existing = this.pluginItems.get(pluginId);
182
+ if (existing) return existing;
183
+
184
+ const items: PluginItems = {
185
+ panes: [],
186
+ paneTemplates: [],
187
+ commands: [],
188
+ columns: [],
189
+ brokers: [],
190
+ dataProviders: [],
191
+ detailTabs: [],
192
+ shortcuts: [],
193
+ tickerActions: [],
194
+ eventDisposers: [],
195
+ };
196
+ this.pluginItems.set(pluginId, items);
197
+ return items;
198
+ }
199
+
200
+ private wrapPaneDef(pluginId: string, pane: PaneDef): PaneDef {
201
+ return {
202
+ ...pane,
203
+ component: (props) => createElement(
204
+ PluginRenderProvider,
205
+ { pluginId, runtime: this },
206
+ createElement(pane.component as any, props),
207
+ ),
208
+ };
209
+ }
210
+
211
+ private wrapDetailTabDef(pluginId: string, tab: DetailTabDef): DetailTabDef {
212
+ return {
213
+ ...tab,
214
+ component: (props) => createElement(
215
+ PluginRenderProvider,
216
+ { pluginId, runtime: this },
217
+ createElement(tab.component as any, props),
218
+ ),
219
+ };
220
+ }
221
+
222
+ private emitResumeState(pluginId: string, key: string): void {
223
+ const listenerKey = `${pluginId}:${key}`;
224
+ for (const listener of this.pluginResumeListeners.get(listenerKey) ?? []) {
225
+ listener();
226
+ }
227
+ }
228
+
229
+ subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void {
230
+ const listenerKey = `${pluginId}:${key}`;
231
+ if (!this.pluginResumeListeners.has(listenerKey)) {
232
+ this.pluginResumeListeners.set(listenerKey, new Set());
233
+ }
234
+ this.pluginResumeListeners.get(listenerKey)!.add(listener);
235
+ return () => {
236
+ const listeners = this.pluginResumeListeners.get(listenerKey);
237
+ if (!listeners) return;
238
+ listeners.delete(listener);
239
+ if (listeners.size === 0) {
240
+ this.pluginResumeListeners.delete(listenerKey);
241
+ }
242
+ };
243
+ }
244
+
245
+ getResumeState<T = unknown>(pluginId: string, key: string, schemaVersion?: number): T | null {
246
+ return this.persistence.pluginState.get<T>(pluginId, `resume:${key}`, schemaVersion)?.value ?? null;
247
+ }
248
+
249
+ setResumeState(pluginId: string, key: string, value: unknown, schemaVersion?: number): void {
250
+ this.persistence.pluginState.set(pluginId, `resume:${key}`, value, schemaVersion);
251
+ this.emitResumeState(pluginId, key);
252
+ }
253
+
254
+ deleteResumeState(pluginId: string, key: string): void {
255
+ this.persistence.pluginState.delete(pluginId, `resume:${key}`);
256
+ this.emitResumeState(pluginId, key);
257
+ }
258
+
259
+ getConfigState<T = unknown>(pluginId: string, key: string): T | null {
260
+ return this.getPluginConfigValueFn<T>(pluginId, key);
261
+ }
262
+
263
+ setConfigState(pluginId: string, key: string, value: unknown): Promise<void> {
264
+ return this.setPluginConfigValueFn(pluginId, key, value);
265
+ }
266
+
267
+ deleteConfigState(pluginId: string, key: string): Promise<void> {
268
+ return this.deletePluginConfigValueFn(pluginId, key);
269
+ }
270
+
271
+ getConfigStateKeys(pluginId: string): string[] {
272
+ return Object.keys(this.getConfigFn().pluginConfig[pluginId] ?? {}).sort();
273
+ }
274
+
275
+ private resolvePrimaryPaneInstanceId(paneId: string): string | undefined {
276
+ const layout = this.getLayoutFn();
277
+ return layout.instances.find((instance) => instance.paneId === paneId)?.instanceId;
278
+ }
279
+
280
+ private resolvePaneTarget(paneId: string): string | undefined {
281
+ const layout = this.getLayoutFn();
282
+ const isInstanceId = layout.instances.some((instance) => instance.instanceId === paneId);
283
+ if (isInstanceId) return paneId;
284
+ return this.resolvePrimaryPaneInstanceId(paneId);
285
+ }
286
+
287
+ resolvePaneSettings(paneId: string): {
288
+ paneId: string;
289
+ pane: PaneInstanceConfig;
290
+ paneDef: PaneDef;
291
+ settingsDef: PaneSettingsDef;
292
+ context: PaneSettingsContext;
293
+ } | null {
294
+ const targetPaneId = this.resolvePaneTarget(paneId);
295
+ if (!targetPaneId) return null;
296
+
297
+ const layout = this.getLayoutFn();
298
+ const pane = layout.instances.find((instance) => instance.instanceId === targetPaneId);
299
+ if (!pane) return null;
300
+
301
+ const paneDef = this.panesMap.get(pane.paneId);
302
+ if (!paneDef?.settings) return null;
303
+
304
+ const config = this.getConfigFn();
305
+ const paneStateMap = Object.fromEntries(
306
+ layout.instances.map((instance) => [instance.instanceId, this.getPaneRuntimeStateFn(instance.instanceId) ?? {}]),
307
+ );
308
+ const paneState = paneStateMap[targetPaneId] ?? {};
309
+ const stateView = {
310
+ config,
311
+ paneState: paneStateMap,
312
+ } as any;
313
+ const context: PaneSettingsContext = {
314
+ config,
315
+ layout,
316
+ paneId: targetPaneId,
317
+ paneType: pane.paneId,
318
+ pane,
319
+ settings: getPaneSettings(pane),
320
+ paneState,
321
+ activeTicker: resolveTickerForPane(stateView, targetPaneId),
322
+ activeCollectionId: resolveCollectionForPane(stateView, targetPaneId),
323
+ };
324
+ const settingsDef = typeof paneDef.settings === "function"
325
+ ? paneDef.settings(context)
326
+ : paneDef.settings;
327
+ if (!settingsDef) return null;
328
+
329
+ return {
330
+ paneId: targetPaneId,
331
+ pane,
332
+ paneDef,
333
+ settingsDef,
334
+ context,
335
+ };
336
+ }
337
+
338
+ hasPaneSettings(paneId: string): boolean {
339
+ return this.resolvePaneSettings(paneId) !== null;
340
+ }
341
+
342
+ getCommandPluginId(commandId: string): string | undefined {
343
+ return this.commandOwners.get(commandId);
344
+ }
345
+
346
+ getPaneTemplatePluginId(templateId: string): string | undefined {
347
+ return this.paneTemplateOwners.get(templateId);
348
+ }
349
+
350
+ getShortcutPluginId(shortcutId: string): string | undefined {
351
+ return this.shortcutOwners.get(shortcutId);
352
+ }
353
+
354
+ isPaneFloating(paneId: string): boolean {
355
+ try {
356
+ const target = this.resolvePaneTarget(paneId);
357
+ return !!target && this.getLayoutFn().floating.some((entry) => entry.instanceId === target);
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ showWidget(paneId: string): void {
364
+ try {
365
+ const disabledPlugins = new Set(this.getConfigFn().disabledPlugins);
366
+ for (const [pluginId, items] of this.pluginItems) {
367
+ if (items.panes.includes(paneId) && disabledPlugins.has(pluginId)) {
368
+ return;
369
+ }
370
+ }
371
+ } catch {
372
+ return;
373
+ }
374
+
375
+ const layout = this.getLayoutFn();
376
+ const existingInstanceId = this.resolvePaneTarget(paneId);
377
+ if (existingInstanceId && layout.floating.some((entry) => entry.instanceId === existingInstanceId)) {
378
+ this.focusPaneFn(existingInstanceId);
379
+ return;
380
+ }
381
+
382
+ const def = this.panesMap.get(paneId);
383
+ const { width, height } = this.getTermSizeFn();
384
+ const instance = existingInstanceId
385
+ ? layout.instances.find((entry) => entry.instanceId === existingInstanceId)!
386
+ : createPaneInstance(paneId, { instanceId: `${paneId}:main` });
387
+ const nextLayout = addPaneFloating(layout, instance, width, height, def);
388
+ this.updateLayoutFn(nextLayout);
389
+ this.focusPaneFn(instance.instanceId);
390
+ }
391
+
392
+ hideWidget(paneId: string): void {
393
+ const target = this.resolvePaneTarget(paneId);
394
+ if (!target) return;
395
+ this.updateLayoutFn(removePane(this.getLayoutFn(), target));
396
+ }
397
+
398
+ private createContext(pluginId: string): GloomPluginContext {
399
+ const items = this.getOrCreatePluginItems(pluginId);
400
+
401
+ const pluginNamespace = `plugin:${pluginId}`;
402
+
403
+ const persistence = createPluginPersistence(this.persistence.pluginState, this.persistence.resources, pluginNamespace, pluginId);
404
+ const log = debugLog.createLogger(pluginId);
405
+ const resume: PluginResumeState = {
406
+ getState: (key, options) => this.getResumeState(pluginId, key, options?.schemaVersion),
407
+ setState: (key, value, options) => this.setResumeState(pluginId, key, value, options?.schemaVersion),
408
+ deleteState: (key) => this.deleteResumeState(pluginId, key),
409
+ getPaneState: (paneId, key) => (
410
+ this.getPaneRuntimeStateFn(paneId)?.pluginState?.[pluginId]?.[key] ?? null
411
+ ),
412
+ setPaneState: (paneId, key, value) => {
413
+ const currentPaneState = this.getPaneRuntimeStateFn(paneId) ?? {};
414
+ const pluginState = {
415
+ ...(currentPaneState.pluginState ?? {}),
416
+ [pluginId]: {
417
+ ...(currentPaneState.pluginState?.[pluginId] ?? {}),
418
+ [key]: value,
419
+ },
420
+ };
421
+ this.updatePaneRuntimeStateFn(paneId, { pluginState });
422
+ },
423
+ deletePaneState: (paneId, key) => {
424
+ const currentPaneState = this.getPaneRuntimeStateFn(paneId) ?? {};
425
+ const currentPluginState = currentPaneState.pluginState?.[pluginId];
426
+ if (!currentPluginState || !(key in currentPluginState)) return;
427
+ const nextPluginState = { ...currentPluginState };
428
+ delete nextPluginState[key];
429
+ const nextAllPluginState = { ...(currentPaneState.pluginState ?? {}) };
430
+ if (Object.keys(nextPluginState).length === 0) {
431
+ delete nextAllPluginState[pluginId];
432
+ } else {
433
+ nextAllPluginState[pluginId] = nextPluginState;
434
+ }
435
+ this.updatePaneRuntimeStateFn(paneId, {
436
+ pluginState: Object.keys(nextAllPluginState).length > 0 ? nextAllPluginState : undefined,
437
+ });
438
+ },
439
+ };
440
+ const paneSettings: PluginPaneSettingsState = {
441
+ get: (paneId, key) => {
442
+ const target = this.resolvePaneTarget(paneId);
443
+ if (!target) return null;
444
+ return (this.getLayoutFn().instances.find((instance) => instance.instanceId === target)?.settings?.[key] as any) ?? null;
445
+ },
446
+ set: async (paneId, key, value) => {
447
+ const target = this.resolvePaneTarget(paneId);
448
+ if (!target) return;
449
+ const nextLayout = setPaneSetting(this.getLayoutFn(), target, key, value);
450
+ this.updateLayoutFn(nextLayout);
451
+ },
452
+ delete: async (paneId, key) => {
453
+ const target = this.resolvePaneTarget(paneId);
454
+ if (!target) return;
455
+ const nextLayout = deletePaneSetting(this.getLayoutFn(), target, key);
456
+ this.updateLayoutFn(nextLayout);
457
+ },
458
+ };
459
+
460
+ return {
461
+ registerPane: (pane) => { this.panesMap.set(pane.id, this.wrapPaneDef(pluginId, pane)); items.panes.push(pane.id); },
462
+ registerPaneTemplate: (template) => {
463
+ this.paneTemplatesMap.set(template.id, template);
464
+ this.paneTemplateOwners.set(template.id, pluginId);
465
+ items.paneTemplates.push(template.id);
466
+ },
467
+ registerCommand: (command) => {
468
+ this.commandsMap.set(command.id, command);
469
+ this.commandOwners.set(command.id, pluginId);
470
+ items.commands.push(command.id);
471
+ },
472
+ registerColumn: (column) => { this.columnsMap.set(column.id, column); items.columns.push(column.id); },
473
+ registerBroker: (broker) => { this.brokersMap.set(broker.id, broker); items.brokers.push(broker.id); },
474
+ registerDataProvider: (provider) => {
475
+ this.dataProvidersMap.set(provider.id, provider);
476
+ this.dataProviderOwners.set(provider.id, pluginId);
477
+ items.dataProviders.push(provider.id);
478
+ },
479
+ registerDetailTab: (tab) => { this.detailTabsMap.set(tab.id, this.wrapDetailTabDef(pluginId, tab)); items.detailTabs.push(tab.id); },
480
+ registerShortcut: (shortcut) => {
481
+ this.shortcutsMap.set(shortcut.id, shortcut);
482
+ this.shortcutOwners.set(shortcut.id, pluginId);
483
+ items.shortcuts.push(shortcut.id);
484
+ },
485
+ registerTickerAction: (action) => { this.tickerActionsMap.set(action.id, action); items.tickerActions.push(action.id); },
486
+
487
+ getData: (ticker) => this.getDataFn(ticker),
488
+ getTicker: (ticker) => this.getTickerFn(ticker),
489
+ getConfig: () => this.getConfigFn(),
490
+
491
+ dataProvider: this.dataProvider,
492
+ tickerRepository: this.tickerRepository,
493
+ persistence,
494
+ log,
495
+ resume,
496
+ paneSettings,
497
+ configState: {
498
+ get: (key) => this.getConfigState(pluginId, key),
499
+ set: (key, value) => this.setConfigState(pluginId, key, value),
500
+ delete: (key) => this.deleteConfigState(pluginId, key),
501
+ keys: () => this.getConfigStateKeys(pluginId),
502
+ },
503
+
504
+ createBrokerInstance: (brokerType, label, values) => this.createBrokerInstanceFn(brokerType, label, values),
505
+ updateBrokerInstance: (instanceId, values) => this.updateBrokerInstanceFn(instanceId, values),
506
+ syncBrokerInstance: (instanceId) => this.syncBrokerInstanceFn(instanceId),
507
+ removeBrokerInstance: (instanceId) => this.removeBrokerInstanceFn(instanceId),
508
+
509
+ selectTicker: (symbol, paneId) => this.selectTickerFn(symbol, paneId),
510
+ switchPanel: (panel) => this.switchPanelFn(panel),
511
+ switchTab: (tabId, paneId) => this.switchTabFn(tabId, paneId),
512
+ openCommandBar: (query) => this.openCommandBarFn(query),
513
+ showPane: (paneId) => this.showPaneFn(paneId),
514
+ createPaneFromTemplate: (templateId, options) => this.createPaneFromTemplateFn(templateId, options),
515
+ hidePane: (paneId) => this.hidePaneFn(paneId),
516
+ focusPane: (paneId) => this.focusPaneFn(paneId),
517
+ pinTicker: (symbol, options) => this.pinTickerFn(symbol, options),
518
+ openPaneSettings: (paneId) => this.openPaneSettingsFn(paneId),
519
+
520
+ on: <K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void) => {
521
+ const dispose = this.events.on(event, handler);
522
+ items.eventDisposers.push(dispose);
523
+ return dispose;
524
+ },
525
+ emit: (event, payload) => this.events.emit(event, payload),
526
+
527
+ showWidget: (widgetId) => this.showWidget(widgetId),
528
+ hideWidget: (widgetId) => this.hideWidget(widgetId),
529
+ showToast: (message, options) => this.showToastFn(message, options),
530
+ };
531
+ }
532
+
533
+ private registryLog = debugLog.createLogger("registry");
534
+
535
+ async register(plugin: GloomPlugin): Promise<void> {
536
+ this.registryLog.info(`Registering plugin: ${plugin.id} v${plugin.version ?? "?"}`);
537
+ const items = this.getOrCreatePluginItems(plugin.id);
538
+ if (plugin.panes) {
539
+ for (const pane of plugin.panes) {
540
+ this.panesMap.set(pane.id, this.wrapPaneDef(plugin.id, pane));
541
+ items.panes.push(pane.id);
542
+ }
543
+ }
544
+
545
+ if (plugin.paneTemplates) {
546
+ for (const template of plugin.paneTemplates) {
547
+ this.paneTemplatesMap.set(template.id, template);
548
+ this.paneTemplateOwners.set(template.id, plugin.id);
549
+ items.paneTemplates.push(template.id);
550
+ }
551
+ }
552
+
553
+ if (plugin.broker) {
554
+ this.brokersMap.set(plugin.broker.id, plugin.broker);
555
+ items.brokers.push(plugin.broker.id);
556
+ }
557
+
558
+ if (plugin.dataProvider) {
559
+ this.dataProvidersMap.set(plugin.dataProvider.id, plugin.dataProvider);
560
+ this.dataProviderOwners.set(plugin.dataProvider.id, plugin.id);
561
+ items.dataProviders.push(plugin.dataProvider.id);
562
+ }
563
+
564
+ if (plugin.slots) {
565
+ const corePlugin = {
566
+ id: plugin.id,
567
+ order: plugin.order,
568
+ slots: {} as Record<string, unknown>,
569
+ };
570
+
571
+ for (const [slotName, renderer] of Object.entries(plugin.slots)) {
572
+ if (renderer) {
573
+ corePlugin.slots[slotName] = (_ctx: unknown, props: unknown) => createElement(
574
+ PluginRenderProvider,
575
+ { pluginId: plugin.id, runtime: this },
576
+ (renderer as any)(props),
577
+ );
578
+ }
579
+ }
580
+
581
+ const unregister = this.slotRegistry.register(corePlugin as any);
582
+ this.unregisterFns.set(plugin.id, unregister);
583
+ }
584
+
585
+ this.plugins.set(plugin.id, plugin);
586
+
587
+ if (plugin.setup) {
588
+ await plugin.setup(this.createContext(plugin.id));
589
+ }
590
+
591
+ this.events.emit("plugin:registered", { pluginId: plugin.id });
592
+ }
593
+
594
+ unregister(pluginId: string): void {
595
+ const plugin = this.plugins.get(pluginId);
596
+ if (!plugin) return;
597
+ this.registryLog.info(`Unregistering plugin: ${pluginId}`);
598
+
599
+ plugin.dispose?.();
600
+ this.unregisterFns.get(pluginId)?.();
601
+ this.unregisterFns.delete(pluginId);
602
+
603
+ const items = this.pluginItems.get(pluginId);
604
+ if (items) {
605
+ for (const paneId of items.panes) this.panesMap.delete(paneId);
606
+ for (const templateId of items.paneTemplates) {
607
+ this.paneTemplatesMap.delete(templateId);
608
+ this.paneTemplateOwners.delete(templateId);
609
+ }
610
+ for (const commandId of items.commands) {
611
+ this.commandsMap.delete(commandId);
612
+ this.commandOwners.delete(commandId);
613
+ }
614
+ for (const columnId of items.columns) this.columnsMap.delete(columnId);
615
+ for (const brokerId of items.brokers) this.brokersMap.delete(brokerId);
616
+ for (const providerId of items.dataProviders) {
617
+ this.dataProvidersMap.delete(providerId);
618
+ this.dataProviderOwners.delete(providerId);
619
+ }
620
+ for (const tabId of items.detailTabs) this.detailTabsMap.delete(tabId);
621
+ for (const shortcutId of items.shortcuts) {
622
+ this.shortcutsMap.delete(shortcutId);
623
+ this.shortcutOwners.delete(shortcutId);
624
+ }
625
+ for (const actionId of items.tickerActions) this.tickerActionsMap.delete(actionId);
626
+ for (const dispose of items.eventDisposers) dispose();
627
+ this.pluginItems.delete(pluginId);
628
+ }
629
+
630
+ this.plugins.delete(pluginId);
631
+ this.events.emit("plugin:unregistered", { pluginId });
632
+ }
633
+ }
@@ -0,0 +1,18 @@
1
+ import type { DataProvider } from "../types/data-provider";
2
+
3
+ /** Convert a value from one currency to the base currency */
4
+ export async function convertToBase(
5
+ value: number,
6
+ fromCurrency: string,
7
+ baseCurrency: string,
8
+ dataProvider: DataProvider,
9
+ ): Promise<number> {
10
+ if (fromCurrency === baseCurrency) return value;
11
+
12
+ // Convert: fromCurrency -> USD -> baseCurrency
13
+ const fromToUsd = await dataProvider.getExchangeRate(fromCurrency);
14
+ const baseToUsd = await dataProvider.getExchangeRate(baseCurrency);
15
+
16
+ if (baseToUsd === 0) return value;
17
+ return (value * fromToUsd) / baseToUsd;
18
+ }