@talex-touch/utils 1.0.38 → 1.0.40

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.
@@ -14,6 +14,24 @@ import { ensureRendererChannel } from './channel'
14
14
  */
15
15
  export type InputChangeHandler = (input: string) => void
16
16
 
17
+ /**
18
+ * Keyboard event data forwarded from CoreBox
19
+ */
20
+ export interface ForwardedKeyEvent {
21
+ key: string
22
+ code: string
23
+ metaKey: boolean
24
+ ctrlKey: boolean
25
+ altKey: boolean
26
+ shiftKey: boolean
27
+ repeat: boolean
28
+ }
29
+
30
+ /**
31
+ * Key event handler
32
+ */
33
+ export type KeyEventHandler = (event: ForwardedKeyEvent) => void
34
+
17
35
  /**
18
36
  * Feature SDK interface for plugins
19
37
  *
@@ -125,6 +143,36 @@ export interface FeatureSDK {
125
143
  * ```
126
144
  */
127
145
  onInputChange(handler: InputChangeHandler): () => void
146
+
147
+ /**
148
+ * Registers a listener for keyboard events forwarded from CoreBox
149
+ *
150
+ * When a plugin's UI view is attached to CoreBox, certain key events
151
+ * (Enter, Arrow keys, Meta+key combinations) are forwarded to the plugin.
152
+ *
153
+ * @param handler - Callback function invoked when a key event is forwarded
154
+ * @returns Unsubscribe function
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * const unsubscribe = plugin.feature.onKeyEvent((event) => {
159
+ * if (event.key === 'Enter') {
160
+ * // Handle enter key
161
+ * submitSelection()
162
+ * } else if (event.key === 'ArrowDown') {
163
+ * // Navigate down in list
164
+ * selectNext()
165
+ * } else if (event.metaKey && event.key === 'k') {
166
+ * // Handle Cmd+K
167
+ * openSearch()
168
+ * }
169
+ * })
170
+ *
171
+ * // Later, unsubscribe
172
+ * unsubscribe()
173
+ * ```
174
+ */
175
+ onKeyEvent(handler: KeyEventHandler): () => void
128
176
  }
129
177
 
130
178
  /**
@@ -138,25 +186,48 @@ export interface FeatureSDK {
138
186
  */
139
187
  export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
140
188
  const inputChangeHandlers: Set<InputChangeHandler> = new Set()
189
+ const keyEventHandlers: Set<KeyEventHandler> = new Set()
141
190
 
142
191
  // Register listener for input change events from main process
143
- const registerListener = () => {
192
+ const registerInputListener = () => {
144
193
  if (channel.onMain) {
145
194
  // Main process plugin context
146
- channel.onMain('core-box:input-changed', (event: any) => {
147
- const input = event.data?.input || event.input || ''
195
+ channel.onMain('core-box:input-change', (event: any) => {
196
+ const input = event.data?.input || event.data?.query?.text || event.input || ''
148
197
  inputChangeHandlers.forEach(handler => handler(input))
149
198
  })
150
199
  } else if (channel.on) {
151
200
  // Renderer process context
152
- channel.on('core-box:input-changed', (data: any) => {
153
- const input = data?.input || data || ''
201
+ channel.on('core-box:input-change', (data: any) => {
202
+ const input = data?.input || data?.query?.text || data || ''
154
203
  inputChangeHandlers.forEach(handler => handler(input))
155
204
  })
156
205
  }
157
206
  }
158
207
 
159
- registerListener()
208
+ // Register listener for key events from main process
209
+ const registerKeyListener = () => {
210
+ if (channel.onMain) {
211
+ // Main process plugin context
212
+ channel.onMain('core-box:key-event', (event: any) => {
213
+ const keyEvent = event.data as ForwardedKeyEvent
214
+ if (keyEvent) {
215
+ keyEventHandlers.forEach(handler => handler(keyEvent))
216
+ }
217
+ })
218
+ } else if (channel.on) {
219
+ // Renderer process context
220
+ channel.on('core-box:key-event', (data: any) => {
221
+ const keyEvent = data as ForwardedKeyEvent
222
+ if (keyEvent) {
223
+ keyEventHandlers.forEach(handler => handler(keyEvent))
224
+ }
225
+ })
226
+ }
227
+ }
228
+
229
+ registerInputListener()
230
+ registerKeyListener()
160
231
 
161
232
  return {
162
233
  pushItems(items: TuffItem[]): void {
@@ -200,6 +271,14 @@ export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
200
271
  return () => {
201
272
  inputChangeHandlers.delete(handler)
202
273
  }
274
+ },
275
+
276
+ onKeyEvent(handler: KeyEventHandler): () => void {
277
+ keyEventHandlers.add(handler)
278
+
279
+ return () => {
280
+ keyEventHandlers.delete(handler)
281
+ }
203
282
  }
204
283
  }
205
284
  }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Flow SDK
3
+ *
4
+ * Plugin-side API for Flow Transfer operations.
5
+ * Allows plugins to dispatch flows and receive flow data.
6
+ */
7
+
8
+ import type {
9
+ FlowPayload,
10
+ FlowDispatchOptions,
11
+ FlowDispatchResult,
12
+ FlowTargetInfo,
13
+ FlowSessionUpdate,
14
+ FlowPayloadType
15
+ } from '../../types/flow'
16
+ import { FlowIPCChannel } from '../../types/flow'
17
+
18
+ /**
19
+ * Flow SDK interface
20
+ */
21
+ export interface IFlowSDK {
22
+ /**
23
+ * Dispatches a flow payload to another plugin
24
+ *
25
+ * @param payload - Data to transfer
26
+ * @param options - Dispatch options
27
+ * @returns Dispatch result
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const result = await flow.dispatch(
32
+ * {
33
+ * type: 'text',
34
+ * data: 'Hello from plugin A',
35
+ * context: { sourcePluginId: 'plugin-a' }
36
+ * },
37
+ * {
38
+ * title: 'Share Text',
39
+ * preferredTarget: 'plugin-b.quick-note'
40
+ * }
41
+ * )
42
+ * ```
43
+ */
44
+ dispatch(payload: FlowPayload, options?: FlowDispatchOptions): Promise<FlowDispatchResult>
45
+
46
+ /**
47
+ * Gets available flow targets
48
+ *
49
+ * @param payloadType - Filter by payload type (optional)
50
+ * @returns List of available targets
51
+ */
52
+ getAvailableTargets(payloadType?: FlowPayloadType): Promise<FlowTargetInfo[]>
53
+
54
+ /**
55
+ * Listens for session updates
56
+ *
57
+ * @param sessionId - Session to listen to
58
+ * @param handler - Update handler
59
+ * @returns Unsubscribe function
60
+ */
61
+ onSessionUpdate(
62
+ sessionId: string,
63
+ handler: (update: FlowSessionUpdate) => void
64
+ ): () => void
65
+
66
+ /**
67
+ * Cancels a flow session
68
+ *
69
+ * @param sessionId - Session to cancel
70
+ */
71
+ cancel(sessionId: string): Promise<void>
72
+
73
+ /**
74
+ * Acknowledges a received flow (for target plugins)
75
+ *
76
+ * @param sessionId - Session to acknowledge
77
+ * @param ackPayload - Optional acknowledgment data
78
+ */
79
+ acknowledge(sessionId: string, ackPayload?: any): Promise<void>
80
+
81
+ /**
82
+ * Reports an error for a received flow (for target plugins)
83
+ *
84
+ * @param sessionId - Session to report error for
85
+ * @param message - Error message
86
+ */
87
+ reportError(sessionId: string, message: string): Promise<void>
88
+ }
89
+
90
+ /**
91
+ * Creates a Flow SDK instance
92
+ *
93
+ * @param channel - Channel for IPC communication
94
+ * @param pluginId - Current plugin ID
95
+ * @returns Flow SDK instance
96
+ */
97
+ export function createFlowSDK(
98
+ channel: { send: (event: string, data?: any) => Promise<any> },
99
+ pluginId: string
100
+ ): IFlowSDK {
101
+ const sessionListeners = new Map<string, Set<(update: FlowSessionUpdate) => void>>()
102
+
103
+ // Listen for session updates
104
+ if (typeof window !== 'undefined') {
105
+ window.addEventListener('message', (event) => {
106
+ if (event.data?.type === FlowIPCChannel.SESSION_UPDATE) {
107
+ const update = event.data.payload as FlowSessionUpdate
108
+ const listeners = sessionListeners.get(update.sessionId)
109
+ if (listeners) {
110
+ for (const listener of listeners) {
111
+ try {
112
+ listener(update)
113
+ } catch (error) {
114
+ console.error('[FlowSDK] Error in session listener:', error)
115
+ }
116
+ }
117
+ }
118
+ }
119
+ })
120
+ }
121
+
122
+ return {
123
+ async dispatch(payload: FlowPayload, options?: FlowDispatchOptions): Promise<FlowDispatchResult> {
124
+ // Ensure context has sourcePluginId
125
+ const enrichedPayload: FlowPayload = {
126
+ ...payload,
127
+ context: {
128
+ ...payload.context,
129
+ sourcePluginId: payload.context?.sourcePluginId || pluginId
130
+ }
131
+ }
132
+
133
+ const response = await channel.send(FlowIPCChannel.DISPATCH, {
134
+ senderId: pluginId,
135
+ payload: enrichedPayload,
136
+ options
137
+ })
138
+
139
+ if (!response?.success) {
140
+ throw new Error(response?.error?.message || 'Flow dispatch failed')
141
+ }
142
+
143
+ return response.data
144
+ },
145
+
146
+ async getAvailableTargets(payloadType?: FlowPayloadType): Promise<FlowTargetInfo[]> {
147
+ const response = await channel.send(FlowIPCChannel.GET_TARGETS, {
148
+ payloadType
149
+ })
150
+
151
+ if (!response?.success) {
152
+ throw new Error(response?.error?.message || 'Failed to get targets')
153
+ }
154
+
155
+ return response.data || []
156
+ },
157
+
158
+ onSessionUpdate(
159
+ sessionId: string,
160
+ handler: (update: FlowSessionUpdate) => void
161
+ ): () => void {
162
+ if (!sessionListeners.has(sessionId)) {
163
+ sessionListeners.set(sessionId, new Set())
164
+ }
165
+ sessionListeners.get(sessionId)!.add(handler)
166
+
167
+ return () => {
168
+ const listeners = sessionListeners.get(sessionId)
169
+ if (listeners) {
170
+ listeners.delete(handler)
171
+ if (listeners.size === 0) {
172
+ sessionListeners.delete(sessionId)
173
+ }
174
+ }
175
+ }
176
+ },
177
+
178
+ async cancel(sessionId: string): Promise<void> {
179
+ const response = await channel.send(FlowIPCChannel.CANCEL, {
180
+ sessionId
181
+ })
182
+
183
+ if (!response?.success) {
184
+ throw new Error(response?.error?.message || 'Failed to cancel session')
185
+ }
186
+ },
187
+
188
+ async acknowledge(sessionId: string, ackPayload?: any): Promise<void> {
189
+ const response = await channel.send(FlowIPCChannel.ACKNOWLEDGE, {
190
+ sessionId,
191
+ ackPayload
192
+ })
193
+
194
+ if (!response?.success) {
195
+ throw new Error(response?.error?.message || 'Failed to acknowledge session')
196
+ }
197
+ },
198
+
199
+ async reportError(sessionId: string, message: string): Promise<void> {
200
+ const response = await channel.send(FlowIPCChannel.REPORT_ERROR, {
201
+ sessionId,
202
+ message
203
+ })
204
+
205
+ if (!response?.success) {
206
+ throw new Error(response?.error?.message || 'Failed to report error')
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Helper to extract flow data from TuffQuery
214
+ *
215
+ * When a feature is triggered via Flow, the query will contain flow information.
216
+ *
217
+ * @param query - TuffQuery from feature trigger
218
+ * @returns Flow data if present, null otherwise
219
+ */
220
+ export function extractFlowData(query: any): {
221
+ sessionId: string
222
+ payload: FlowPayload
223
+ senderId: string
224
+ senderName?: string
225
+ } | null {
226
+ if (!query?.flow) {
227
+ return null
228
+ }
229
+
230
+ return {
231
+ sessionId: query.flow.sessionId,
232
+ payload: query.flow.payload,
233
+ senderId: query.flow.senderId,
234
+ senderName: query.flow.senderName
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Helper to check if a feature was triggered via Flow
240
+ *
241
+ * @param query - TuffQuery from feature trigger
242
+ * @returns True if triggered via Flow
243
+ */
244
+ export function isFlowTriggered(query: any): boolean {
245
+ return !!query?.flow
246
+ }
@@ -12,6 +12,7 @@ export type BridgeHook<T = any> = (data: T) => void
12
12
  const __hooks: Record<BridgeEvent, Array<BridgeHook>> = {
13
13
  [BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE]: [],
14
14
  [BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE]: [],
15
+ [BridgeEventForCoreBox.CORE_BOX_KEY_EVENT]: [],
15
16
  }
16
17
 
17
18
  /**
@@ -65,6 +66,22 @@ export const createBridgeHook = <T>(type: BridgeEvent) => (hook: BridgeHook<T>)
65
66
  * The hook receives the new input value as a string.
66
67
  * @param data The input change data (string).
67
68
  */
68
- export const onCoreBoxInputChange = createBridgeHook<{ query: string }>(BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE)
69
+ export const onCoreBoxInputChange = createBridgeHook<{ query: { inputs: Array<any>, text: string } }>(BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE)
69
70
 
70
71
  export const onCoreBoxClipboardChange = createBridgeHook<{ item: any }>(BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE)
72
+
73
+ /**
74
+ * Hook for when a keyboard event is forwarded from CoreBox.
75
+ * This is triggered when the plugin's UI view is attached and the user
76
+ * presses certain keys (Enter, Arrow keys, Meta+key combinations).
77
+ * @param data The forwarded keyboard event data.
78
+ */
79
+ export const onCoreBoxKeyEvent = createBridgeHook<{
80
+ key: string
81
+ code: string
82
+ metaKey: boolean
83
+ ctrlKey: boolean
84
+ altKey: boolean
85
+ shiftKey: boolean
86
+ repeat: boolean
87
+ }>(BridgeEventForCoreBox.CORE_BOX_KEY_EVENT)
@@ -13,9 +13,11 @@ export * from './clipboard'
13
13
  export * from './core-box'
14
14
  export * from './division-box'
15
15
  export * from './feature-sdk'
16
+ export * from './flow'
16
17
  export { createFeaturesManager, useFeatures } from './features'
17
18
 
18
19
  export * from './hooks/index'
20
+ export * from './performance'
19
21
  export * from './service/index'
20
22
  export * from './storage'
21
23
  export * from './system'
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Plugin Performance SDK
3
+ *
4
+ * Provides APIs for plugins to access their own performance metrics
5
+ * and storage statistics.
6
+ */
7
+ import type { ITouchClientChannel } from '@talex-touch/utils/channel'
8
+ import { ensureRendererChannel } from './channel'
9
+
10
+ /**
11
+ * Storage statistics interface
12
+ */
13
+ export interface StorageStats {
14
+ /** Total size in bytes */
15
+ totalSize: number
16
+ /** Number of files */
17
+ fileCount: number
18
+ /** Number of directories */
19
+ dirCount: number
20
+ /** Maximum allowed size in bytes */
21
+ maxSize: number
22
+ /** Usage percentage (0-100) */
23
+ usagePercent: number
24
+ }
25
+
26
+ /**
27
+ * Performance metrics interface
28
+ */
29
+ export interface PerformanceMetrics {
30
+ /** Plugin load time in milliseconds */
31
+ loadTime: number
32
+ /** Estimated memory usage in bytes */
33
+ memoryUsage: number
34
+ /** CPU usage percentage (0-100) */
35
+ cpuUsage: number
36
+ /** Last active timestamp */
37
+ lastActiveTime: number
38
+ }
39
+
40
+ /**
41
+ * Plugin paths interface
42
+ */
43
+ export interface PluginPaths {
44
+ /** Plugin installation directory */
45
+ pluginPath: string
46
+ /** Plugin data directory */
47
+ dataPath: string
48
+ /** Plugin config directory */
49
+ configPath: string
50
+ /** Plugin logs directory */
51
+ logsPath: string
52
+ /** Plugin temp directory */
53
+ tempPath: string
54
+ }
55
+
56
+ /**
57
+ * Performance SDK interface
58
+ */
59
+ export interface PerformanceSDK {
60
+ /**
61
+ * Get storage statistics for the current plugin
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const stats = await plugin.performance.getStorageStats()
66
+ * console.log(`Using ${stats.usagePercent}% of storage`)
67
+ * ```
68
+ */
69
+ getStorageStats: () => Promise<StorageStats>
70
+
71
+ /**
72
+ * Get performance metrics for the current plugin
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const metrics = await plugin.performance.getMetrics()
77
+ * console.log(`Load time: ${metrics.loadTime}ms`)
78
+ * ```
79
+ */
80
+ getMetrics: () => Promise<PerformanceMetrics>
81
+
82
+ /**
83
+ * Get all paths for the current plugin
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const paths = await plugin.performance.getPaths()
88
+ * console.log(`Plugin installed at: ${paths.pluginPath}`)
89
+ * ```
90
+ */
91
+ getPaths: () => Promise<PluginPaths>
92
+
93
+ /**
94
+ * Get combined performance data (storage + metrics + paths)
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const data = await plugin.performance.getAll()
99
+ * console.log(data.storage, data.metrics, data.paths)
100
+ * ```
101
+ */
102
+ getAll: () => Promise<{
103
+ storage: StorageStats
104
+ metrics: PerformanceMetrics
105
+ paths: PluginPaths
106
+ }>
107
+ }
108
+
109
+ /**
110
+ * Creates a Performance SDK instance for plugin use
111
+ *
112
+ * @param channel - The plugin channel bridge for IPC communication
113
+ * @returns Configured Performance SDK instance
114
+ *
115
+ * @internal
116
+ */
117
+ export function createPerformanceSDK(channel: ITouchClientChannel): PerformanceSDK {
118
+ return {
119
+ async getStorageStats(): Promise<StorageStats> {
120
+ try {
121
+ const result = await channel.send('plugin:storage:get-stats')
122
+ return result?.data || {
123
+ totalSize: 0,
124
+ fileCount: 0,
125
+ dirCount: 0,
126
+ maxSize: 10 * 1024 * 1024,
127
+ usagePercent: 0,
128
+ }
129
+ }
130
+ catch (error) {
131
+ console.error('[Performance SDK] Failed to get storage stats:', error)
132
+ throw error
133
+ }
134
+ },
135
+
136
+ async getMetrics(): Promise<PerformanceMetrics> {
137
+ try {
138
+ const result = await channel.send('plugin:performance:get-metrics')
139
+ return result?.data || {
140
+ loadTime: 0,
141
+ memoryUsage: 0,
142
+ cpuUsage: 0,
143
+ lastActiveTime: 0,
144
+ }
145
+ }
146
+ catch (error) {
147
+ console.error('[Performance SDK] Failed to get performance metrics:', error)
148
+ throw error
149
+ }
150
+ },
151
+
152
+ async getPaths(): Promise<PluginPaths> {
153
+ try {
154
+ const result = await channel.send('plugin:performance:get-paths')
155
+ return result?.data || {
156
+ pluginPath: '',
157
+ dataPath: '',
158
+ configPath: '',
159
+ logsPath: '',
160
+ tempPath: '',
161
+ }
162
+ }
163
+ catch (error) {
164
+ console.error('[Performance SDK] Failed to get plugin paths:', error)
165
+ throw error
166
+ }
167
+ },
168
+
169
+ async getAll(): Promise<{
170
+ storage: StorageStats
171
+ metrics: PerformanceMetrics
172
+ paths: PluginPaths
173
+ }> {
174
+ const [storage, metrics, paths] = await Promise.all([
175
+ this.getStorageStats(),
176
+ this.getMetrics(),
177
+ this.getPaths(),
178
+ ])
179
+
180
+ return { storage, metrics, paths }
181
+ },
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Hook for using Performance SDK in plugin context
187
+ *
188
+ * @returns Performance SDK instance
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * const performance = usePerformance()
193
+ *
194
+ * const stats = await performance.getStorageStats()
195
+ * const metrics = await performance.getMetrics()
196
+ * ```
197
+ */
198
+ export function usePerformance(): PerformanceSDK {
199
+ const channel = ensureRendererChannel('[Performance SDK] Channel not available. Make sure this is called in a plugin context.')
200
+ return createPerformanceSDK(channel)
201
+ }
package/plugin/widget.ts CHANGED
@@ -18,6 +18,11 @@ export interface WidgetRegistrationPayload {
18
18
  code: string
19
19
  styles: string
20
20
  hash: string
21
+ /**
22
+ * List of allowed module dependencies for widget sandbox
23
+ * Widget 沙箱允许的模块依赖列表
24
+ */
25
+ dependencies?: string[]
21
26
  }
22
27
 
23
28
  export function makeWidgetId(pluginName: string, featureId: string): string {