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,281 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { useKeyboard } from "@opentui/react";
3
+ import { useState } from "react";
4
+ import type { ReactNode } from "react";
5
+ import type { GloomPlugin, PaneProps } from "../../types/plugin";
6
+ import { colors, hoverBg } from "../../theme/colors";
7
+ import { getSharedRegistry } from "../registry";
8
+
9
+ function ShortcutBadge({ label }: { label: string }) {
10
+ return (
11
+ <box backgroundColor={colors.selected}>
12
+ <text fg={colors.selectedText} attributes={TextAttributes.BOLD}>
13
+ {` ${label} `}
14
+ </text>
15
+ </box>
16
+ );
17
+ }
18
+
19
+ function ShortcutRow({
20
+ badges,
21
+ description,
22
+ compact,
23
+ }: {
24
+ badges: string[];
25
+ description: string;
26
+ compact: boolean;
27
+ }) {
28
+ return (
29
+ <box flexDirection={compact ? "column" : "row"} gap={1}>
30
+ <box flexDirection="row" gap={1} flexShrink={0}>
31
+ {badges.map((badge) => <ShortcutBadge key={badge} label={badge} />)}
32
+ </box>
33
+ <box flexGrow={1}>
34
+ <text fg={colors.text}>{description}</text>
35
+ </box>
36
+ </box>
37
+ );
38
+ }
39
+
40
+ function HelpSection({ title, children }: { title: string; children: ReactNode }) {
41
+ return (
42
+ <box flexDirection="column">
43
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{title}</text>
44
+ <box height={1} />
45
+ {children}
46
+ </box>
47
+ );
48
+ }
49
+
50
+ function ActionButton({
51
+ id,
52
+ label,
53
+ hovered,
54
+ onHover,
55
+ onPress,
56
+ }: {
57
+ id: string;
58
+ label: string;
59
+ hovered: boolean;
60
+ onHover: (id: string | null) => void;
61
+ onPress: () => void;
62
+ }) {
63
+ return (
64
+ <box
65
+ backgroundColor={hovered ? hoverBg() : colors.panel}
66
+ onMouseMove={() => onHover(id)}
67
+ onMouseOut={() => onHover(null)}
68
+ onMouseDown={(event: any) => {
69
+ event.stopPropagation?.();
70
+ event.preventDefault?.();
71
+ onPress();
72
+ }}
73
+ >
74
+ <text fg={hovered ? colors.textBright : colors.text}>{` ${label} `}</text>
75
+ </box>
76
+ );
77
+ }
78
+
79
+ function resolveWindowTemplates(registry: ReturnType<typeof getSharedRegistry>) {
80
+ if (!registry || !registry.paneTemplates) return [];
81
+
82
+ const disabledPlugins = new Set(registry.getConfigFn?.().disabledPlugins ?? []);
83
+ const allPlugins = registry.allPlugins ?? new Map<string, { name?: string }>();
84
+
85
+ return [...registry.paneTemplates.values()]
86
+ .filter((template) => template.shortcut)
87
+ .filter((template) => {
88
+ const pluginId = registry.getPaneTemplatePluginId?.(template.id);
89
+ return !pluginId || !disabledPlugins.has(pluginId);
90
+ })
91
+ .map((template) => {
92
+ const shortcut = template.shortcut!;
93
+ const pluginId = registry.getPaneTemplatePluginId?.(template.id);
94
+ const pluginName = pluginId ? allPlugins.get(pluginId)?.name : null;
95
+ return {
96
+ id: template.id,
97
+ badges: [
98
+ shortcut.prefix,
99
+ shortcut.argPlaceholder ? `<${shortcut.argPlaceholder}>` : null,
100
+ ].filter((value): value is string => !!value),
101
+ description: pluginName ? `${template.label} (${pluginName})` : template.label,
102
+ };
103
+ })
104
+ .sort((left, right) => (
105
+ left.badges.join(" ").localeCompare(right.badges.join(" "))
106
+ || left.description.localeCompare(right.description)
107
+ ));
108
+ }
109
+
110
+ export function HelpPane({ focused, width, height, close }: PaneProps) {
111
+ const registry = getSharedRegistry();
112
+ const compact = width < 78;
113
+ const [hoveredAction, setHoveredAction] = useState<string | null>(null);
114
+ const windowTemplates = resolveWindowTemplates(registry);
115
+
116
+ useKeyboard((event) => {
117
+ if (!focused) return;
118
+ if (event.name === "escape") {
119
+ close?.();
120
+ }
121
+ });
122
+
123
+ const openDebugLog = () => {
124
+ close?.();
125
+ registry?.showWidget("debug");
126
+ };
127
+
128
+ const openLayoutActions = () => {
129
+ close?.();
130
+ registry?.openCommandBarFn("LAY ");
131
+ };
132
+
133
+ const openPluginManager = () => {
134
+ close?.();
135
+ registry?.openCommandBarFn("PL ");
136
+ };
137
+
138
+ return (
139
+ <box flexDirection="column" width={width} height={height}>
140
+ <scrollbox flexGrow={1} scrollY>
141
+ <box flexDirection="column" padding={1}>
142
+ <box flexDirection="column" gap={1}>
143
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>How To Use Gloomberb</text>
144
+ <box flexDirection="column">
145
+ <text fg={colors.textDim}>Gloomberb is command-bar first.</text>
146
+ <text fg={colors.textDim}>Use the keyboard for speed, and the mouse for windows.</text>
147
+ </box>
148
+ </box>
149
+
150
+ <box flexDirection={compact ? "column" : "row"} gap={1}>
151
+ <ActionButton
152
+ id="debug"
153
+ label="Open Debug Log"
154
+ hovered={hoveredAction === "debug"}
155
+ onHover={setHoveredAction}
156
+ onPress={openDebugLog}
157
+ />
158
+ <ActionButton
159
+ id="layout"
160
+ label="Layout Actions"
161
+ hovered={hoveredAction === "layout"}
162
+ onHover={setHoveredAction}
163
+ onPress={openLayoutActions}
164
+ />
165
+ <ActionButton
166
+ id="plugins"
167
+ label="Manage Plugins"
168
+ hovered={hoveredAction === "plugins"}
169
+ onHover={setHoveredAction}
170
+ onPress={openPluginManager}
171
+ />
172
+ </box>
173
+
174
+ <HelpSection title="Command Bar">
175
+ <ShortcutRow
176
+ badges={["Ctrl+P", "`"]}
177
+ description="Open the command bar from anywhere."
178
+ compact={compact}
179
+ />
180
+ <ShortcutRow
181
+ badges={["help"]}
182
+ description="Open this help window directly."
183
+ compact={compact}
184
+ />
185
+ <ShortcutRow
186
+ badges={["T <ticker>"]}
187
+ description="Search for a ticker or company and open it."
188
+ compact={compact}
189
+ />
190
+ <ShortcutRow
191
+ badges={["NP", "PS", "PL"]}
192
+ description="Create panes, edit pane settings, and manage plugins."
193
+ compact={compact}
194
+ />
195
+ </HelpSection>
196
+
197
+ <HelpSection title="Window Templates">
198
+ {windowTemplates.length > 0 ? windowTemplates.map((template) => (
199
+ <ShortcutRow
200
+ key={template.id}
201
+ badges={template.badges}
202
+ description={template.description}
203
+ compact={compact}
204
+ />
205
+ )) : (
206
+ <text fg={colors.textDim}>No shortcut window templates are currently registered.</text>
207
+ )}
208
+ </HelpSection>
209
+
210
+ <HelpSection title="Move Around">
211
+ <ShortcutRow
212
+ badges={["Tab", "Shift+Tab"]}
213
+ description="Move focus between panes and floating windows."
214
+ compact={compact}
215
+ />
216
+ <ShortcutRow
217
+ badges={["Ctrl+W"]}
218
+ description="Close the focused pane, docked or floating."
219
+ compact={compact}
220
+ />
221
+ <ShortcutRow
222
+ badges={["j", "k"]}
223
+ description="Move through lists in most panes. Arrow keys also work."
224
+ compact={compact}
225
+ />
226
+ <ShortcutRow
227
+ badges={["r", "Shift+R"]}
228
+ description="Refresh the focused ticker or refresh everything."
229
+ compact={compact}
230
+ />
231
+ </HelpSection>
232
+
233
+ <HelpSection title="Layout System">
234
+ <ShortcutRow
235
+ badges={["LAY"]}
236
+ description="Open layout actions."
237
+ compact={compact}
238
+ />
239
+ <text fg={colors.text}>Docked panes stay in the saved layout.</text>
240
+ <box flexDirection="column">
241
+ <text fg={colors.text}>Floating panes can be dragged by the title bar</text>
242
+ <text fg={colors.text}>and resized from the lower-right corner.</text>
243
+ </box>
244
+ <text fg={colors.text}>Run Gridlock All Windows if the layout gets messy.</text>
245
+ </HelpSection>
246
+
247
+ <HelpSection title="If There Is A Bug">
248
+ <text fg={colors.text}>Open Debug Log, then run Export Debug Log from the command bar.</text>
249
+ <box flexDirection="column">
250
+ <text fg={colors.text}>The file lands in ~/Downloads. Include steps, ticker or layout, plugin,</text>
251
+ <text fg={colors.text}>and a screenshot if it is visual.</text>
252
+ </box>
253
+ </HelpSection>
254
+ </box>
255
+ </scrollbox>
256
+
257
+ <box height={1} paddingX={1}>
258
+ <text fg={colors.textMuted}>Esc closes this window. Ctrl+W closes the focused pane.</text>
259
+ </box>
260
+ </box>
261
+ );
262
+ }
263
+
264
+ export const helpPlugin: GloomPlugin = {
265
+ id: "help",
266
+ name: "Help",
267
+ version: "1.0.0",
268
+ description: "Shortcut and layout help",
269
+
270
+ panes: [
271
+ {
272
+ id: "help",
273
+ name: "Help",
274
+ icon: "?",
275
+ component: HelpPane,
276
+ defaultPosition: "right",
277
+ defaultMode: "floating",
278
+ defaultFloatingSize: { width: 88, height: 32 },
279
+ },
280
+ ],
281
+ };
@@ -0,0 +1,264 @@
1
+ import { saveConfig } from "../../data/config-store";
2
+ import { findPaneInstance, type LayoutConfig } from "../../types/config";
3
+ import type { GloomPlugin, GloomPluginContext } from "../../types/plugin";
4
+ import type { AppAction } from "../../state/app-context";
5
+ import { getSharedRegistry } from "../registry";
6
+ import {
7
+ dockPane,
8
+ floatPane,
9
+ getDockedPaneIds,
10
+ gridlockAllPanes,
11
+ isPaneInLayout,
12
+ isPaneDocked,
13
+ removePane,
14
+ swapPanes,
15
+ } from "../pane-manager";
16
+
17
+ let dispatchRef: ((action: AppAction) => void) | null = null;
18
+ let getStateRef: (() => { layout: LayoutConfig; termWidth: number; termHeight: number; focusedPaneId: string | null }) | null = null;
19
+
20
+ export function setLayoutManagerDispatch(
21
+ dispatch: (action: AppAction) => void,
22
+ getState: () => { layout: LayoutConfig; termWidth: number; termHeight: number; focusedPaneId: string | null },
23
+ ) {
24
+ dispatchRef = dispatch;
25
+ getStateRef = getState;
26
+ }
27
+
28
+ function persistLayout(layout: LayoutConfig) {
29
+ if (!dispatchRef) return;
30
+ dispatchRef({ type: "PUSH_LAYOUT_HISTORY" });
31
+ dispatchRef({ type: "UPDATE_LAYOUT", layout });
32
+ const registry = getSharedRegistry();
33
+ if (!registry) return;
34
+ const config = registry.getConfigFn();
35
+ const layouts = config.layouts.map((savedLayout, index) => (
36
+ index === config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
37
+ ));
38
+ saveConfig({ ...config, layout, layouts }).catch(() => {});
39
+ }
40
+
41
+ function getFocusedPane(layout: LayoutConfig, focusedPaneId: string | null) {
42
+ return focusedPaneId ? findPaneInstance(layout, focusedPaneId) ?? null : null;
43
+ }
44
+
45
+ export const layoutManagerPlugin: GloomPlugin = {
46
+ id: "layout-manager",
47
+ name: "Layout Manager",
48
+ version: "1.0.0",
49
+ description: "Pane layout management commands",
50
+
51
+ setup(ctx) {
52
+ ctx.registerCommand({
53
+ id: "float-pane",
54
+ label: "Float Pane",
55
+ description: "Detach a docked pane into a floating window",
56
+ keywords: ["float", "detach", "undock", "window", "pane"],
57
+ category: "config",
58
+ execute: async () => {
59
+ if (!getStateRef) return;
60
+ const registry = getSharedRegistry();
61
+ if (!registry) return;
62
+
63
+ const { layout, termWidth, termHeight, focusedPaneId } = getStateRef();
64
+ const focusedPane = getFocusedPane(layout, focusedPaneId);
65
+ if (!focusedPane || !isPaneDocked(layout, focusedPane.instanceId)) {
66
+ ctx.showToast("Focus a docked pane to float it", { type: "info" });
67
+ return;
68
+ }
69
+
70
+ const def = registry.panes.get(focusedPane.paneId);
71
+ const nextLayout = floatPane(layout, focusedPane.instanceId, termWidth, termHeight, def);
72
+ persistLayout(nextLayout);
73
+ dispatchRef?.({ type: "FOCUS_PANE", paneId: focusedPane.instanceId });
74
+ },
75
+ });
76
+
77
+ ctx.registerCommand({
78
+ id: "dock-pane",
79
+ label: "Dock Pane",
80
+ description: "Dock a floating pane back into the layout",
81
+ keywords: ["dock", "attach", "pin", "pane"],
82
+ category: "config",
83
+ execute: async () => {
84
+ if (!getStateRef) return;
85
+ const registry = getSharedRegistry();
86
+ if (!registry) return;
87
+
88
+ const { layout, focusedPaneId } = getStateRef();
89
+ const focusedPane = getFocusedPane(layout, focusedPaneId);
90
+ if (!focusedPane || !layout.floating.some((entry) => entry.instanceId === focusedPane.instanceId)) {
91
+ ctx.showToast("Focus a floating pane to dock it", { type: "info" });
92
+ return;
93
+ }
94
+
95
+ const nextLayout = dockPane(layout, focusedPane.instanceId);
96
+ persistLayout(nextLayout);
97
+ dispatchRef?.({ type: "FOCUS_PANE", paneId: focusedPane.instanceId });
98
+ },
99
+ });
100
+
101
+ ctx.registerCommand({
102
+ id: "add-pane",
103
+ label: "New Pane",
104
+ description: "Create a new pane from plugin templates",
105
+ keywords: ["new", "add", "pane", "panel", "show"],
106
+ category: "config",
107
+ hidden: () => true,
108
+ execute: async () => {
109
+ ctx.openCommandBar("NP ");
110
+ ctx.showToast("Choose a pane template to create", { type: "info" });
111
+ },
112
+ });
113
+
114
+ ctx.registerCommand({
115
+ id: "gridlock-all",
116
+ label: "Gridlock All Windows",
117
+ description: "Arrange all visible panes into a tiled grid",
118
+ keywords: ["grid", "gridlock", "tile", "arrange", "windows", "layout"],
119
+ category: "config",
120
+ execute: async () => {
121
+ if (!getStateRef) return;
122
+ const { layout, termWidth, termHeight } = getStateRef();
123
+ persistLayout(gridlockAllPanes(layout, { x: 0, y: 0, width: termWidth, height: termHeight }));
124
+ ctx.showToast("Retiled all panes", { type: "success" });
125
+ },
126
+ });
127
+
128
+ ctx.registerCommand({
129
+ id: "remove-pane",
130
+ label: "Remove Pane",
131
+ description: "Remove a pane from the layout",
132
+ keywords: ["remove", "pane", "close", "hide", "panel"],
133
+ category: "config",
134
+ execute: async () => {
135
+ if (!getStateRef) return;
136
+ const { layout, focusedPaneId } = getStateRef();
137
+ const focusedPane = getFocusedPane(layout, focusedPaneId);
138
+ if (!focusedPane || !isPaneInLayout(layout, focusedPane.instanceId)) {
139
+ ctx.showToast("Focus a pane to remove it", { type: "info" });
140
+ return;
141
+ }
142
+ persistLayout(removePane(layout, focusedPane.instanceId));
143
+ },
144
+ });
145
+
146
+ ctx.registerCommand({
147
+ id: "new-layout",
148
+ label: "New Layout",
149
+ description: "Create a new layout",
150
+ keywords: ["new", "create", "add", "layout", "workspace"],
151
+ category: "config",
152
+ wizard: [{ key: "name", label: "Layout name", placeholder: "e.g. Trading, Research, Overview" }],
153
+ execute: async (values) => {
154
+ const name = values?.name?.trim();
155
+ if (!name) {
156
+ ctx.showToast("Layout name is required", { type: "error" });
157
+ return;
158
+ }
159
+ dispatchRef?.({ type: "NEW_LAYOUT", name });
160
+ ctx.showToast(`Layout "${name}" created`, { type: "success" });
161
+ },
162
+ });
163
+
164
+ ctx.registerCommand({
165
+ id: "delete-layout",
166
+ label: "Delete Layout",
167
+ description: "Delete the current layout preset",
168
+ keywords: ["delete", "remove", "layout", "preset"],
169
+ category: "config",
170
+ confirm: () => {
171
+ const registry = getSharedRegistry();
172
+ if (!registry) return null;
173
+ const config = registry.getConfigFn();
174
+ const layout = config.layouts[config.activeLayoutIndex];
175
+ if (!layout) return null;
176
+ return {
177
+ title: "Delete Layout",
178
+ body: [`Delete layout "${layout.name}"? This cannot be undone.`],
179
+ confirmLabel: "Delete Layout",
180
+ cancelLabel: "Back",
181
+ tone: "danger",
182
+ };
183
+ },
184
+ execute: async () => {
185
+ const registry = getSharedRegistry();
186
+ if (!registry) return;
187
+ const config = registry.getConfigFn();
188
+ if (config.layouts.length <= 1) {
189
+ ctx.showToast("Can't delete the only layout", { type: "error" });
190
+ return;
191
+ }
192
+ const index = config.activeLayoutIndex;
193
+ const name = config.layouts[index]!.name;
194
+ dispatchRef?.({ type: "DELETE_LAYOUT", index });
195
+ ctx.showToast(`Layout "${name}" deleted`, { type: "success" });
196
+ },
197
+ });
198
+
199
+ ctx.registerCommand({
200
+ id: "rename-layout",
201
+ label: "Rename Layout",
202
+ description: "Rename the current layout preset",
203
+ keywords: ["rename", "layout", "preset"],
204
+ category: "config",
205
+ wizard: [{ key: "name", label: "New name", placeholder: "Layout name" }],
206
+ execute: async (values) => {
207
+ const name = values?.name?.trim();
208
+ if (!name) {
209
+ ctx.showToast("Name is required", { type: "error" });
210
+ return;
211
+ }
212
+ const registry = getSharedRegistry();
213
+ if (!registry) return;
214
+ dispatchRef?.({ type: "RENAME_LAYOUT", index: registry.getConfigFn().activeLayoutIndex, name });
215
+ ctx.showToast(`Layout renamed to "${name}"`, { type: "success" });
216
+ },
217
+ });
218
+
219
+ ctx.registerCommand({
220
+ id: "duplicate-layout",
221
+ label: "Duplicate Layout",
222
+ description: "Create a copy of the current layout",
223
+ keywords: ["duplicate", "copy", "clone", "layout"],
224
+ category: "config",
225
+ execute: async () => {
226
+ const registry = getSharedRegistry();
227
+ if (!registry) return;
228
+ dispatchRef?.({ type: "DUPLICATE_LAYOUT", index: registry.getConfigFn().activeLayoutIndex });
229
+ ctx.showToast("Layout duplicated", { type: "success" });
230
+ },
231
+ });
232
+
233
+ ctx.registerCommand({
234
+ id: "swap-panes",
235
+ label: "Swap Panes",
236
+ description: "Swap two pane positions",
237
+ keywords: ["swap", "switch", "pane", "panel"],
238
+ category: "config",
239
+ execute: async () => {
240
+ if (!getStateRef) return;
241
+ const { layout, focusedPaneId } = getStateRef();
242
+ const focusedPane = getFocusedPane(layout, focusedPaneId);
243
+ const dockedPaneIds = getDockedPaneIds(layout);
244
+ if (dockedPaneIds.length < 2) {
245
+ ctx.showToast("Need at least 2 docked panes to swap", { type: "info" });
246
+ return;
247
+ }
248
+ if (!focusedPane || !isPaneDocked(layout, focusedPane.instanceId)) {
249
+ ctx.showToast("Focus a docked pane to swap it", { type: "info" });
250
+ return;
251
+ }
252
+
253
+ const others = dockedPaneIds.filter((instanceId) => instanceId !== focusedPane.instanceId);
254
+ if (others.length === 1) {
255
+ persistLayout(swapPanes(layout, focusedPane.instanceId, others[0]!));
256
+ return;
257
+ }
258
+
259
+ ctx.openCommandBar("LAY ");
260
+ ctx.showToast("Choose a swap target from layout mode", { type: "info" });
261
+ },
262
+ });
263
+ },
264
+ };
@@ -0,0 +1,25 @@
1
+ import type { GloomPlugin } from "../../types/plugin";
2
+ import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
3
+
4
+ const manualBroker: BrokerAdapter = {
5
+ id: "manual",
6
+ name: "Manual Entry",
7
+
8
+ async validate() {
9
+ return true; // Always valid
10
+ },
11
+
12
+ async importPositions(): Promise<BrokerPosition[]> {
13
+ // Manual entry doesn't import - positions are created directly
14
+ return [];
15
+ },
16
+
17
+ configSchema: [],
18
+ };
19
+
20
+ export const manualEntryPlugin: GloomPlugin = {
21
+ id: "manual-entry",
22
+ name: "Manual Entry",
23
+ version: "1.0.0",
24
+ broker: manualBroker,
25
+ };