@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.
@@ -1005,15 +1005,31 @@ export interface TuffMeta {
1005
1005
  permissions?: string[]
1006
1006
  }
1007
1007
 
1008
- /**
1009
- * TuffIntelligence
1010
- */
1008
+ /** TuffIntelligence */
1011
1009
  intelligence?: any
1012
1010
 
1011
+ /** Keep CoreBox open after action execution */
1012
+ keepCoreBoxOpen?: boolean
1013
+
1014
+ /** 固定配置 */
1015
+ pinned?: {
1016
+ isPinned: boolean
1017
+ pinnedAt?: number
1018
+ order?: number
1019
+ }
1020
+
1021
+ /** 推荐来源标记 */
1022
+ recommendation?: {
1023
+ source: 'frequent' | 'recent' | 'time-based' | 'trending' | 'pinned' | 'context'
1024
+ score?: number
1025
+ }
1026
+
1013
1027
  /**
1014
- * Keep CoreBox open after action execution
1028
+ * 额外关键词
1029
+ * @description 用于搜索索引的额外关键词,会参与关键字索引匹配
1030
+ * @warning 不建议添加太多关键词(建议 <= 10),过多会影响搜索性能
1015
1031
  */
1016
- keepCoreBoxOpen?: boolean
1032
+ keywords?: string[]
1017
1033
  }
1018
1034
 
1019
1035
  // ==================== 前端展示结构 ====================
@@ -1267,71 +1283,69 @@ export interface SortStat {
1267
1283
  duration: number
1268
1284
  }
1269
1285
 
1270
- /**
1271
- * 搜索结果结构
1272
- *
1273
- * @description
1274
- * 定义搜索返回的结果集合和元数据。
1275
- * 包含匹配项、统计信息和分页数据等。
1276
- */
1286
+ /** 容器布局配置 */
1287
+ export interface TuffContainerLayout {
1288
+ /** 布局模式 */
1289
+ mode: 'list' | 'grid'
1290
+ /** 宫格配置 */
1291
+ grid?: {
1292
+ /** 列数 */
1293
+ columns: number
1294
+ /** 间距(px) */
1295
+ gap?: number
1296
+ /** 单项尺寸 */
1297
+ itemSize?: 'small' | 'medium' | 'large'
1298
+ }
1299
+ /** 分组配置 */
1300
+ sections?: TuffSection[]
1301
+ }
1302
+
1303
+ /** 分组元数据 */
1304
+ export interface TuffSectionMeta {
1305
+ /** 是否为智能推荐分组 */
1306
+ intelligence?: boolean
1307
+ /** 扩展字段 */
1308
+ [key: string]: unknown
1309
+ }
1310
+
1311
+ /** 分组定义 */
1312
+ export interface TuffSection {
1313
+ id: string
1314
+ title?: string
1315
+ layout: 'list' | 'grid'
1316
+ /** 该分组的 item ids */
1317
+ itemIds: string[]
1318
+ collapsed?: boolean
1319
+ /** 分组元数据 */
1320
+ meta?: TuffSectionMeta
1321
+ }
1322
+
1323
+ /** 搜索结果结构 */
1277
1324
  export interface TuffSearchResult {
1278
- /**
1279
- * A unique identifier for this specific search operation.
1280
- * This is crucial for the streaming model to associate updates with the correct search instance.
1281
- */
1325
+ /** 搜索会话 ID */
1282
1326
  sessionId?: string
1283
-
1284
- /**
1285
- * 结果项目
1286
- * @description 匹配的TuffItem列表
1287
- * @required
1288
- */
1327
+ /** 结果项目 */
1289
1328
  items: TuffItem[]
1290
1329
 
1291
- /**
1292
- * 查询信息
1293
- * @description 原始查询参数
1294
- * @required
1295
- */
1330
+ /** 原始查询参数 */
1296
1331
  query: TuffQuery
1297
-
1298
- /**
1299
- * 搜索耗时
1300
- * @description 搜索执行的毫秒数
1301
- * @required
1302
- */
1332
+ /** 搜索耗时(ms) */
1303
1333
  duration: number
1304
-
1305
- /**
1306
- * 来源统计
1307
- * @description 各数据来源的结果统计
1308
- * @required
1309
- */
1334
+ /** 来源统计 */
1310
1335
  sources: Array<{
1311
- /** Provider's unique ID. */
1312
1336
  providerId: string
1313
- /** Provider's display name. */
1314
1337
  providerName: string
1315
- /** Search duration in milliseconds. */
1316
1338
  duration: number
1317
- /** Number of results returned. */
1318
1339
  resultCount: number
1319
- /** Status of the search operation. */
1320
1340
  status: 'success' | 'timeout' | 'error'
1321
1341
  }>
1322
-
1323
- /**
1324
- * AI 推荐
1325
- * @description AI生成的搜索建议
1326
- */
1342
+ /** 容器布局配置 */
1343
+ containerLayout?: TuffContainerLayout
1344
+ /** AI 推荐建议 */
1327
1345
  suggestions?: string[]
1328
-
1329
- /**
1330
- * The provider(s) to activate after this search result.
1331
- */
1346
+ /** 激活的 provider */
1332
1347
  activate?: IProviderActivate[]
1333
-
1334
- /** Optional statistics about the sorting process. */
1348
+ /** 排序统计 */
1335
1349
  sort_stats?: SortStat[]
1336
1350
  }
1337
1351
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talex-touch/utils",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "private": false,
5
5
  "description": "Tuff series utils",
6
6
  "author": "TalexDreamSoul",
package/plugin/index.ts CHANGED
@@ -142,10 +142,15 @@ export interface IPluginFeature {
142
142
  name: string
143
143
  desc: string
144
144
  icon: ITuffIcon
145
+ keywords?: string[]
145
146
  push: boolean
146
147
  platform: IPlatform
147
148
  commands: IFeatureCommand[]
148
149
  interaction?: IFeatureInteraction
150
+ /**
151
+ * Internal search tokens generated at runtime for better matching
152
+ */
153
+ searchTokens?: string[]
149
154
  /**
150
155
  * Priority of the feature for sorting in search results
151
156
  * Higher numbers have higher priority (displayed first)
@@ -66,6 +66,53 @@ const unsubscribe = plugin.feature.onInputChange((input) => {
66
66
 
67
67
  // 取消监听
68
68
  unsubscribe()
69
+
70
+ // 监听键盘事件(UI View 模式下)
71
+ const unsubscribeKey = plugin.feature.onKeyEvent((event) => {
72
+ if (event.key === 'Enter') {
73
+ // 处理回车键
74
+ submitSelection()
75
+ } else if (event.key === 'ArrowDown') {
76
+ // 向下导航
77
+ selectNext()
78
+ } else if (event.key === 'ArrowUp') {
79
+ // 向上导航
80
+ selectPrev()
81
+ } else if (event.metaKey && event.key === 'k') {
82
+ // 处理 Cmd+K
83
+ openSearch()
84
+ }
85
+ })
86
+ ```
87
+
88
+ ### 3. 键盘事件自动处理
89
+
90
+ 当插件的 UI View 附加到 CoreBox 时,系统会自动处理以下行为:
91
+
92
+ #### ESC 键自动退出
93
+ - 在 UI View 中按下 ESC 键会**自动退出 UI 模式**(deactivate providers)
94
+ - 插件无需手动处理 ESC 键的退出逻辑
95
+ - 这与 CoreBox 主界面的 ESC 行为保持一致
96
+
97
+ #### 键盘事件转发
98
+ 以下按键会从 CoreBox 主输入框转发到插件 UI View:
99
+ - **Enter** - 确认/提交
100
+ - **ArrowUp / ArrowDown** - 上下导航
101
+ - **Meta/Ctrl + 任意键** - 快捷键组合(Cmd+V 除外,用于粘贴)
102
+
103
+ > **注意**:`ArrowLeft` 和 `ArrowRight` 不会被转发,因为它们用于输入框中的文本编辑。如果需要左右导航,请使用 `Meta/Ctrl + ArrowLeft/ArrowRight`。
104
+
105
+ ```typescript
106
+ // 键盘事件数据结构
107
+ interface ForwardedKeyEvent {
108
+ key: string // 按键名称,如 'Enter', 'ArrowDown'
109
+ code: string // 按键代码,如 'Enter', 'ArrowDown'
110
+ metaKey: boolean // Cmd/Win 键是否按下
111
+ ctrlKey: boolean // Ctrl 键是否按下
112
+ altKey: boolean // Alt 键是否按下
113
+ shiftKey: boolean // Shift 键是否按下
114
+ repeat: boolean // 是否为重复按键
115
+ }
69
116
  ```
70
117
 
71
118
  ## 废弃的 API
@@ -151,7 +198,7 @@ onInit(context) {
151
198
  export default {
152
199
  onInit(context) {
153
200
  const { feature, box } = context.utils
154
-
201
+
155
202
  // 监听输入变化
156
203
  feature.onInputChange((input) => {
157
204
  if (input.length > 2) {
@@ -163,10 +210,10 @@ export default {
163
210
  }
164
211
  })
165
212
  },
166
-
213
+
167
214
  onFeatureTriggered(featureId, query, feature) {
168
215
  const { feature: featureSDK, box } = this.context.utils
169
-
216
+
170
217
  // 推送结果
171
218
  featureSDK.pushItems([
172
219
  {
@@ -176,10 +223,10 @@ export default {
176
223
  source: { id: this.pluginName, name: this.pluginName }
177
224
  }
178
225
  ])
179
-
226
+
180
227
  // 扩展窗口显示结果
181
228
  box.expand({ length: 5 })
182
-
229
+
183
230
  // 3秒后隐藏
184
231
  setTimeout(() => {
185
232
  box.hide()
@@ -207,7 +254,8 @@ export default {
207
254
  - `core-box:get-input` - 获取当前输入值
208
255
  - `core-box:set-input` - 设置输入框内容
209
256
  - `core-box:clear-input` - 清空输入框
210
- - `core-box:input-changed` - 输入变化广播(主进程 → 插件)
257
+ - `core-box:input-change` - 输入变化广播(主进程 → 插件)
258
+ - `core-box:key-event` - 键盘事件转发(主进程 → 插件 UI View)
211
259
  - `core-box:set-input-visibility` - 设置输入框可见性(主进程 → 渲染进程)
212
260
  - `core-box:request-input-value` - 请求输入值(主进程 → 渲染进程)
213
261
 
@@ -23,7 +23,7 @@ function normalizeItem(item: PluginClipboardItem | null): PluginClipboardItem |
23
23
  return item
24
24
  }
25
25
 
26
- export type ClipboardHistoryOptions = Omit<PluginClipboardSearchOptions, 'keyword' | 'startTime' | 'endTime' | 'type' | 'isFavorite' | 'sourceApp' | 'sortOrder'> & { page?: number }
26
+ export type ClipboardHistoryOptions = PluginClipboardSearchOptions
27
27
 
28
28
  export interface ClipboardFavoriteOptions {
29
29
  id: number
@@ -44,69 +44,141 @@ export interface ClipboardApplyOptions {
44
44
  type?: PluginClipboardItem['type']
45
45
  }
46
46
 
47
+ export interface ClipboardWriteOptions {
48
+ text?: string
49
+ html?: string
50
+ image?: string
51
+ files?: string[]
52
+ }
53
+
54
+ export interface ClipboardReadResult {
55
+ text: string
56
+ html: string
57
+ hasImage: boolean
58
+ hasFiles: boolean
59
+ formats: string[]
60
+ }
61
+
62
+ export interface ClipboardImageResult {
63
+ dataUrl: string
64
+ width: number
65
+ height: number
66
+ }
67
+
68
+ export interface ClipboardCopyAndPasteOptions {
69
+ text?: string
70
+ html?: string
71
+ image?: string
72
+ files?: string[]
73
+ delayMs?: number
74
+ hideCoreBox?: boolean
75
+ }
76
+
47
77
  export type ClipboardSearchOptions = PluginClipboardSearchOptions
48
78
  export type ClipboardSearchResponse = PluginClipboardSearchResponse
49
79
 
50
-
80
+ /**
81
+ * @deprecated Use `useClipboard()` instead. This function will be removed in a future version.
82
+ */
51
83
  export function useClipboardHistory() {
84
+ return useClipboard().history
85
+ }
86
+
87
+ /**
88
+ * Unified Clipboard SDK for plugin renderer context.
89
+ *
90
+ * Provides:
91
+ * - Basic clipboard operations (read/write) via IPC to main process
92
+ * - Clipboard history management
93
+ * - Copy and paste to active application
94
+ *
95
+ * All operations go through IPC to avoid WebContents focus issues.
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const clipboard = useClipboard()
100
+ *
101
+ * // === Basic Operations ===
102
+ * await clipboard.writeText('Hello World')
103
+ * const content = await clipboard.read()
104
+ *
105
+ * // === Copy and Paste ===
106
+ * await clipboard.copyAndPaste({ text: 'Pasted content' })
107
+ *
108
+ * // === History Operations ===
109
+ * const latest = await clipboard.history.getLatest()
110
+ * const { history } = await clipboard.history.getHistory({ page: 1 })
111
+ *
112
+ * // === Listen to Changes ===
113
+ * // Note: Plugin must call box.allowClipboard(types) first
114
+ * const unsubscribe = clipboard.history.onDidChange((item) => {
115
+ * console.log('Clipboard changed:', item)
116
+ * })
117
+ * ```
118
+ */
119
+ export function useClipboard() {
52
120
  const channel = ensurePluginChannel()
53
121
 
54
- return {
122
+ const history = {
123
+ /**
124
+ * Gets the most recent clipboard item
125
+ */
55
126
  async getLatest(): Promise<PluginClipboardItem | null> {
56
127
  const result = await channel.send('clipboard:get-latest')
57
128
  return normalizeItem(result)
58
129
  },
59
130
 
131
+ /**
132
+ * Gets clipboard history with pagination
133
+ */
60
134
  async getHistory(options: ClipboardHistoryOptions = {}): Promise<PluginClipboardHistoryResponse> {
61
- const { page = 1 } = options
62
- const response = await channel.send('clipboard:get-history', { page })
63
- const history = Array.isArray(response?.history)
135
+ const response = await channel.send('clipboard:get-history', options)
136
+ const items = Array.isArray(response?.history)
64
137
  ? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
65
138
  : []
66
139
  return {
67
140
  ...response,
68
- history,
141
+ history: items,
69
142
  }
70
143
  },
71
144
 
145
+ /**
146
+ * Sets favorite status for a clipboard item
147
+ */
72
148
  async setFavorite(options: ClipboardFavoriteOptions): Promise<void> {
73
149
  await channel.send('clipboard:set-favorite', options)
74
150
  },
75
151
 
152
+ /**
153
+ * Deletes a clipboard item from history
154
+ */
76
155
  async deleteItem(options: ClipboardDeleteOptions): Promise<void> {
77
156
  await channel.send('clipboard:delete-item', options)
78
157
  },
79
158
 
159
+ /**
160
+ * Clears all clipboard history
161
+ */
80
162
  async clearHistory(): Promise<void> {
81
163
  await channel.send('clipboard:clear-history')
82
164
  },
83
165
 
84
166
  /**
85
- * Search clipboard history with advanced filtering options.
86
- * Supports keyword search, time-based filtering, and combined filters.
87
- *
88
- * @param options - Search options
89
- * @returns Search results with pagination metadata
167
+ * Search clipboard history with advanced filtering
90
168
  *
91
169
  * @example
92
170
  * ```typescript
93
171
  * // Search by keyword
94
- * const result = await searchHistory({ keyword: 'hello' })
172
+ * const result = await clipboard.history.searchHistory({ keyword: 'hello' })
95
173
  *
96
- * // Search by time range (last 24 hours)
97
- * const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
98
- * const recent = await searchHistory({ startTime: oneDayAgo })
99
- *
100
- * // Combined search: text type, favorite items from a specific app
101
- * const filtered = await searchHistory({
174
+ * // Filter by type and time
175
+ * const recent = await clipboard.history.searchHistory({
102
176
  * type: 'text',
103
- * isFavorite: true,
104
- * sourceApp: 'com.apple.Safari'
177
+ * startTime: Date.now() - 24 * 60 * 60 * 1000
105
178
  * })
106
179
  * ```
107
180
  */
108
181
  async searchHistory(options: ClipboardSearchOptions = {}): Promise<ClipboardSearchResponse> {
109
- // Use the extended clipboard:get-history interface with search parameters
110
182
  const response = await channel.send('clipboard:get-history', options)
111
183
  const items = Array.isArray(response?.history)
112
184
  ? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
@@ -119,21 +191,120 @@ export function useClipboardHistory() {
119
191
  }
120
192
  },
121
193
 
194
+ /**
195
+ * Listen to clipboard changes.
196
+ *
197
+ * **Important**: Plugin must call `box.allowClipboard(types)` first to enable monitoring.
198
+ * Only changes matching the allowed types will be received.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * import { ClipboardType } from '@talex-touch/utils/plugin/sdk'
203
+ *
204
+ * // Enable monitoring for text and images
205
+ * await box.allowClipboard(ClipboardType.TEXT | ClipboardType.IMAGE)
206
+ *
207
+ * // Listen to changes
208
+ * const unsubscribe = clipboard.history.onDidChange((item) => {
209
+ * console.log('New clipboard item:', item.type, item.content)
210
+ * })
211
+ *
212
+ * // Later: stop listening
213
+ * unsubscribe()
214
+ * ```
215
+ */
122
216
  onDidChange(callback: (item: PluginClipboardItem) => void): () => void {
123
- return channel.regChannel('core-box:clipboard-change', ({ data }) => {
124
- const item = (data && 'item' in data ? data.item : data) as PluginClipboardItem
217
+ return channel.regChannel('core-box:clipboard-change', ({ data }: { data: unknown }) => {
218
+ const item = (data && typeof data === 'object' && 'item' in data ? (data as { item: PluginClipboardItem }).item : data) as PluginClipboardItem
125
219
  callback(normalizeItem(item) ?? item)
126
220
  })
127
221
  },
128
222
 
129
223
  /**
130
- * Writes the provided clipboard payload to the system clipboard and issues a paste command
131
- * to the foreground application.
224
+ * Apply a clipboard item to the active application (write + paste)
225
+ * @deprecated Use `clipboard.copyAndPaste()` instead
132
226
  */
133
227
  async applyToActiveApp(options: ClipboardApplyOptions = {}): Promise<boolean> {
134
228
  const response = await channel.send('clipboard:apply-to-active-app', options)
135
229
  if (typeof response === 'object' && response) {
136
- return Boolean((response as any).success)
230
+ return Boolean((response as { success?: boolean }).success)
231
+ }
232
+ return true
233
+ },
234
+ }
235
+
236
+ return {
237
+ /**
238
+ * Clipboard history operations
239
+ */
240
+ history,
241
+
242
+ /**
243
+ * Writes text to the system clipboard
244
+ */
245
+ async writeText(text: string): Promise<void> {
246
+ await channel.send('clipboard:write-text', { text })
247
+ },
248
+
249
+ /**
250
+ * Writes content to the system clipboard.
251
+ * Supports text, HTML, image (data URL), and files.
252
+ */
253
+ async write(options: ClipboardWriteOptions): Promise<void> {
254
+ await channel.send('clipboard:write', options)
255
+ },
256
+
257
+ /**
258
+ * Reads current clipboard content
259
+ */
260
+ async read(): Promise<ClipboardReadResult> {
261
+ return await channel.send('clipboard:read')
262
+ },
263
+
264
+ /**
265
+ * Reads image from clipboard as data URL
266
+ */
267
+ async readImage(): Promise<ClipboardImageResult | null> {
268
+ return await channel.send('clipboard:read-image')
269
+ },
270
+
271
+ /**
272
+ * Reads file paths from clipboard
273
+ */
274
+ async readFiles(): Promise<string[]> {
275
+ return await channel.send('clipboard:read-files')
276
+ },
277
+
278
+ /**
279
+ * Clears the system clipboard
280
+ */
281
+ async clear(): Promise<void> {
282
+ await channel.send('clipboard:clear')
283
+ },
284
+
285
+ /**
286
+ * Writes content to clipboard and simulates paste command to the active application.
287
+ * This is the recommended way to "paste" content from a plugin.
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * // Paste text
292
+ * await clipboard.copyAndPaste({ text: 'Hello' })
293
+ *
294
+ * // Paste with HTML formatting
295
+ * await clipboard.copyAndPaste({ text: 'Hello', html: '<b>Hello</b>' })
296
+ *
297
+ * // Paste image
298
+ * await clipboard.copyAndPaste({ image: 'data:image/png;base64,...' })
299
+ *
300
+ * // Paste files
301
+ * await clipboard.copyAndPaste({ files: ['/path/to/file.txt'] })
302
+ * ```
303
+ */
304
+ async copyAndPaste(options: ClipboardCopyAndPasteOptions): Promise<boolean> {
305
+ const response = await channel.send('clipboard:copy-and-paste', options)
306
+ if (typeof response === 'object' && response) {
307
+ return Boolean((response as { success?: boolean }).success)
137
308
  }
138
309
  return true
139
310
  },
@@ -1,4 +1,5 @@
1
1
  export enum BridgeEventForCoreBox {
2
2
  CORE_BOX_INPUT_CHANGE = 'core-box:input-change',
3
3
  CORE_BOX_CLIPBOARD_CHANGE = 'core-box:clipboard-change',
4
+ CORE_BOX_KEY_EVENT = 'core-box:key-event',
4
5
  }