dsh-locator 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README-zh.md +7 -3
- package/README.md +11 -6
- package/bin/darwin-arm64/indexd +0 -0
- package/bin/darwin-x64/indexd +0 -0
- package/bin/linux-arm64/indexd +0 -0
- package/bin/linux-x64/indexd +0 -0
- package/bin/win32-arm64/indexd.exe +0 -0
- package/bin/win32-x64/indexd.exe +0 -0
- package/client.js +192 -55
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +27 -1
- package/lib/http.js.map +1 -1
- package/lib/reveal.d.ts +5 -3
- package/lib/reveal.d.ts.map +1 -1
- package/lib/reveal.js +19 -17
- package/lib/reveal.js.map +1 -1
- package/package.json +1 -1
- package/src/http.ts +25 -1
- package/src/reveal.ts +17 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-11-09
|
|
4
|
+
|
|
5
|
+
* **USN restart catch-up**: index records carry the NTFS FRN (in the old snapshot padding, so v1 snapshots still load), `scan --usn` persists `usn-state.json` (journal id, next USN, roots), and `serve --usn` replays the journal before serving. A file created while the daemon is stopped is found after restart without a full scan; a wrapped/mismatched journal falls back to the portable watcher. Validated on a Windows CI runner.
|
|
6
|
+
* **Panel i18n** (zh/en/es/pt/hi): follow `navigator.language`, including status, empty/error states, relative times and footer hints.
|
|
7
|
+
* **Panel actions**: `Ctrl+O` opens the selected file with its default app; `Ctrl+Enter` inserts a quoted `@"path"` into the composer (falls back to copying the mention); double-click still reveals it.
|
|
8
|
+
* `/dsh-locator/open` host route with the same root/path validation as reveal.
|
|
9
|
+
|
|
10
|
+
## 0.4.1 — 2026-11-09
|
|
11
|
+
|
|
12
|
+
* Engine version is injected at build time (`-X main.version`) instead of a hard-coded `0.1.0`; `locator_status` now reports the package version.
|
|
13
|
+
* `locator_export` verified against the live 1.24 M-entry index: 1,238,602 rows, 477 MB, 6.4 s.
|
|
14
|
+
* Memory tuning documented: Go honours `GOMEMLIMIT` / `GOGC`, so the engine can be capped without a rebuild (e.g. `GOMEMLIMIT=600MiB`).
|
|
15
|
+
|
|
3
16
|
## 0.4.0 — 2026-11-09
|
|
4
17
|
|
|
5
18
|
* macOS now ships **FSEvents** in the cgo darwin binaries (arm64 and x64): a pure-C stream callback serializes events into a pipe, so the recursive watcher has no per-directory descriptor limit. The previous crash was `FSEventStreamCreate` receiving the callback context instead of a real `FSEventStreamContext`.
|
package/README-zh.md
CHANGED
|
@@ -55,7 +55,8 @@ npm 包内置 `linux-x64`、`linux-arm64`、`win32-x64`、`win32-arm64`、`darwi
|
|
|
55
55
|
* 空查询按修改时间倒序列出最近条目;输入后 110 ms 防抖,走和工具完全相同的 Everything 风格语法。
|
|
56
56
|
* 点列头按名称、大小、修改时间、路径排序,再点切换升降序。
|
|
57
57
|
* `只看文件` / `只看目录` 过滤类型;底栏显示命中数、分页上限和查询耗时。
|
|
58
|
-
* `↑`/`↓` 选择,`Enter`
|
|
58
|
+
* `↑`/`↓` 选择,`Enter` 复制选中路径,`Ctrl+O` 打开,`Ctrl+Enter` 把带引号的 `@"路径"` 插入输入框,**双击在文件管理器中定位**(Explorer / Finder / `xdg-open`),`Esc` 关闭。
|
|
59
|
+
* 面板跟随 `navigator.language`(中/英/西/葡/印地语)。
|
|
59
60
|
* 面板是可选的:没有 Web server 的 profile 不会挂载它,三个工具照常工作。
|
|
60
61
|
|
|
61
62
|
## 工具
|
|
@@ -135,7 +136,7 @@ npm 包内置 `linux-x64`、`linux-arm64`、`win32-x64`、`win32-arm64`、`darwi
|
|
|
135
136
|
|
|
136
137
|
它会打印日志 id/USN、枚举 MFT(每秒记录数、根目录下文件/目录数、样例路径),并解码最近几条变更日志。非管理员运行会明确报 `Access is denied`;解析与路径重建在 Linux 上有单元测试覆盖。
|
|
137
138
|
|
|
138
|
-
打开 `windowsUsn: true`(或 `indexd scan --usn`)后,引擎用 MFT 枚举代替目录遍历,再按 FRN
|
|
139
|
+
打开 `windowsUsn: true`(或 `indexd scan --usn`)后,引擎用 MFT 枚举代替目录遍历,再按 FRN 父表重建路径。成功的 USN 扫描会写 `usn-state.json`(日志 id、next USN、根目录),并把 FRN 存进索引记录;下次 `serve --usn` 会先从该游标重放日志再提供服务,停机期间新建的文件无需全量重扫即可出现。日志回绕/被删/不匹配时回退到可移植监听。引擎保留卷句柄,每 2 秒轮询变更日志,按同一张 FRN 表就地应用新建、删除、重命名(`usn.ApplyChanges`)。USN 记录**没有大小和修改时间**:名字/路径是准的,`locator_status` 会把这种索引标成 `USN`,守护进程会对返回页现场 stat,所以面板/模型看到的行仍有真实大小和日期;但全库的 `size:`/`dm:` 过滤在元数据补齐前是近似的。任何失败(没提权、卷不支持、记录过多)都会带警告回退到目录遍历,绝不会让搜索不可用。运行中日志轮询失败会报警并停止该监视器,可移植的 `ReadDirectoryChangesW` 监听和 `locator_rebuild` 仍然可用。变更应用逻辑有合成记录测试,ioctl 路径由 Windows CI job 验证。
|
|
139
140
|
|
|
140
141
|
Everything 之所以首次索引近乎瞬时,是因为它通过 USN 日志直接读 NTFS 主文件表。`dsh-locator` v0.1 刻意先做可移植的目录遍历 + 变更通知:不需要服务、不需要 UAC、任何文件系统都能用。USN/MFT 枚举会作为可选快路径加入路线图(需要进程对卷持有 `GENERIC_READ`,通常意味着管理员权限),可移植路径始终保留为回退。
|
|
141
142
|
|
|
@@ -203,7 +204,6 @@ pnpm build:engine:all # 或 build:engine 只构建本机
|
|
|
203
204
|
* WSL 写 `/mnt/c` 可能不产生 Windows 侧通知(Windows 监听看不到 9p 写入方的变更流),需显式重建。
|
|
204
205
|
* 网络共享只有在能当普通目录访问时才会被扫描;没有 SMB 专用变更源。
|
|
205
206
|
* macOS 随包二进制使用 FSEvents(递归);`CGO_ENABLED=0` 构建回退到 kqueue(每目录一个描述符,上限 4,096)。两种模式的内容变更都靠周期重建兜底。
|
|
206
|
-
* 还没有 USN/MFT 快速枚举(见上)。
|
|
207
207
|
|
|
208
208
|
## 路线图
|
|
209
209
|
|
|
@@ -213,6 +213,10 @@ pnpm build:engine:all # 或 build:engine 只构建本机
|
|
|
213
213
|
4. 可选的小写名字 arena 与 trigram 预筛。
|
|
214
214
|
5. 增量快照压缩与按根目录的日志。
|
|
215
215
|
|
|
216
|
+
## 内存
|
|
217
|
+
|
|
218
|
+
引擎是单个 Go 进程,遵循标准 `GOMEMLIMIT` / `GOGC` 环境变量。124 万条索引在默认 GC 下峰值约 700–800MB RSS;内存紧张时设 `GOMEMLIMIT=600MiB`(软上限,运行时用 CPU 换内存)或 `GOGC=50`。都不改变快照格式。
|
|
219
|
+
|
|
216
220
|
## 安全与隐私
|
|
217
221
|
|
|
218
222
|
索引完全本地:不联网、不埋点、不上传。快照里是文件名和路径,请把状态目录当私有数据;介意的话别把它纳入备份。插件绝不修改、移动或删除被索引的文件。
|
package/README.md
CHANGED
|
@@ -55,7 +55,8 @@ Press `Ctrl+Shift+F` anywhere in the Web GUI, or click the magnifier at the side
|
|
|
55
55
|
* Empty query lists the most recently modified entries; typing runs the same Everything-style syntax as the tool after a 110 ms debounce.
|
|
56
56
|
* Click a column header to sort by name, size, modification time or path; toggle ascending/descending.
|
|
57
57
|
* `只看文件` / `只看目录` filter the kind; the footer reports matches, the page cap and query latency.
|
|
58
|
-
* `↑`/`↓` select, `Enter` copies the selected path, **double-click reveals it** in Explorer/Finder/`xdg-open`, `Esc` closes.
|
|
58
|
+
* `↑`/`↓` select, `Enter` copies the selected path, `Ctrl+O` opens it, `Ctrl+Enter` inserts a quoted `@"path"` into the composer, **double-click reveals it** in Explorer/Finder/`xdg-open`, `Esc` closes.
|
|
59
|
+
* The panel follows `navigator.language` (zh/en/es/pt/hi).
|
|
59
60
|
* The panel is optional: profiles without a Web server never mount it, and the three tools keep working.
|
|
60
61
|
|
|
61
62
|
## Tools
|
|
@@ -135,9 +136,9 @@ You can still run the same probe yourself:
|
|
|
135
136
|
|
|
136
137
|
It prints the journal id/USNs, enumerates the MFT (records per second, files/dirs below the root, sample paths) and decodes the last few change-journal records. A non-elevated run fails with `Access is denied` — the parser and path reconstruction are covered by unit tests on every platform.
|
|
137
138
|
|
|
138
|
-
With `windowsUsn: true` (or `indexd scan --usn`) the engine enumerates the MFT instead of walking directories, then builds paths from the FRN parent map. It keeps the volume handle and polls the change journal every two seconds, applying creates, deletes and renames through `usn.ApplyChanges` (the same FRN map, updated in place). USN records carry **no size or modification time**: results are correct for names and paths, `locator_status` marks such an index with `USN`, and the daemon stats the returned page so the rows the panel/model sees still show real sizes and dates. `size:`/`dm:` filters over the whole index stay approximate until a metadata pass runs. Any failure (no elevation, unsupported volume, too many records) falls back to the portable walk with a warning; it never disables search. If journal polling fails mid-session, the watcher reports it and stops; the portable `ReadDirectoryChangesW` watcher and `locator_rebuild` are still there. The journal applier is covered by synthetic-record tests and the ioctl path by the Windows CI job.
|
|
139
|
+
With `windowsUsn: true` (or `indexd scan --usn`) the engine enumerates the MFT instead of walking directories, then builds paths from the FRN parent map. A successful USN scan also writes `usn-state.json` (journal id, next USN, roots) and keeps the FRN in each record; on the next `serve --usn` the engine replays the journal from that cursor before answering, so files created while it was stopped appear without a full scan. A wrapped, deleted or mismatched journal falls back to the portable watcher. It keeps the volume handle and polls the change journal every two seconds, applying creates, deletes and renames through `usn.ApplyChanges` (the same FRN map, updated in place). USN records carry **no size or modification time**: results are correct for names and paths, `locator_status` marks such an index with `USN`, and the daemon stats the returned page so the rows the panel/model sees still show real sizes and dates. `size:`/`dm:` filters over the whole index stay approximate until a metadata pass runs. Any failure (no elevation, unsupported volume, too many records) falls back to the portable walk with a warning; it never disables search. If journal polling fails mid-session, the watcher reports it and stops; the portable `ReadDirectoryChangesW` watcher and `locator_rebuild` are still there. The journal applier is covered by synthetic-record tests and the ioctl path by the Windows CI job.
|
|
139
140
|
|
|
140
|
-
Everything reads the NTFS Master File Table through the USN journal, which is why its first index is nearly instant. `dsh-locator`
|
|
141
|
+
Everything reads the NTFS Master File Table through the USN journal, which is why its first index is nearly instant. `dsh-locator` keeps a portable directory walk as the default (no service, no UAC, any filesystem) and uses USN/MFT as an opt-in fast path where the process holds `GENERIC_READ` (admin), with journal replay across restarts. The portable path stays the fallback.
|
|
141
142
|
|
|
142
143
|
## Performance
|
|
143
144
|
|
|
@@ -207,12 +208,16 @@ The engine is a separate Go module under `engine/`; run its tests with `cd engin
|
|
|
207
208
|
|
|
208
209
|
## Roadmap
|
|
209
210
|
|
|
210
|
-
1.
|
|
211
|
-
2.
|
|
212
|
-
3.
|
|
211
|
+
1. Electron desktop runtime check on a real Windows install.
|
|
212
|
+
2. Richer previews and result grouping in the panel.
|
|
213
|
+
3. Windows on ARM / Linux ARM runtime coverage.
|
|
213
214
|
4. Optional lowercase-name arena and trigram prefilter for large indexes.
|
|
214
215
|
5. Incremental snapshot compaction and per-root journals.
|
|
215
216
|
|
|
217
|
+
## Memory
|
|
218
|
+
|
|
219
|
+
The engine is a single Go process and honours the standard `GOMEMLIMIT` and `GOGC` environment variables. A 1.24 M-entry index peaks around 700–800 MB RSS with the default GC; set `GOMEMLIMIT=600MiB` (soft limit, the runtime trades CPU for memory) or `GOGC=50` when the host is tight on RAM. Neither changes the snapshot format.
|
|
220
|
+
|
|
216
221
|
## Security and privacy
|
|
217
222
|
|
|
218
223
|
The index is local: no network calls, no telemetry, no uploads. The snapshot contains file names and paths, so treat the state directory as private data and exclude it from backups if that matters to you. The plugin never modifies, moves or deletes indexed files.
|
package/bin/darwin-arm64/indexd
CHANGED
|
Binary file
|
package/bin/darwin-x64/indexd
CHANGED
|
Binary file
|
package/bin/linux-arm64/indexd
CHANGED
|
Binary file
|
package/bin/linux-x64/indexd
CHANGED
|
Binary file
|
|
Binary file
|
package/bin/win32-x64/indexd.exe
CHANGED
|
Binary file
|
package/client.js
CHANGED
|
@@ -100,10 +100,135 @@ window.__ModuleLoader__.load({
|
|
|
100
100
|
})
|
|
101
101
|
const data = await response.json().catch(() => null)
|
|
102
102
|
if (!response.ok || data === null) throw new Error('HTTP ' + response.status)
|
|
103
|
-
if (data.ok === false) throw new Error(typeof data.error === 'string' ? data.error : '
|
|
103
|
+
if (data.ok === false) throw new Error(typeof data.error === 'string' ? data.error : 'request failed')
|
|
104
104
|
return data
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
|
|
108
|
+
// ── i18n ────────────────────────────────────────────────────────────────
|
|
109
|
+
const STRINGS = {
|
|
110
|
+
en: {
|
|
111
|
+
placeholder: 'Search file names or paths… e.g. ext:pdf size:>10mb dm:thisweek path:src !tmp',
|
|
112
|
+
searchLabel: 'Search files', dialog: 'Local file search', closeTitle: 'Close (Esc)', closeLabel: 'Close',
|
|
113
|
+
filesOnly: 'Files only', dirsOnly: 'Directories only', statusLoading: 'Reading index status…',
|
|
114
|
+
status: 'Index {entries} entries · {mode} · {roots} roots', live: 'live', snapshot: 'snapshot', usn: 'USN',
|
|
115
|
+
sortTitle: 'Click to change sorting', name: 'Name', size: 'Size', mtime: 'Modified', path: 'Path',
|
|
116
|
+
dirBadge: 'DIR', fileBadge: 'FILE', loading: 'Searching…', errorPrefix: 'Search failed: ', empty: 'No matching files',
|
|
117
|
+
results: '{total} results', showing: 'showing first {n}', indexing: 'indexing, results may be incomplete',
|
|
118
|
+
copied: 'Path copied', revealed: 'Revealed in file manager', opened: 'Opened', inserted: 'Inserted @file',
|
|
119
|
+
insertFailed: 'Insert failed; @file copied', footer: '↑↓ select · Enter copy · Ctrl+O open · Ctrl+Enter insert @file · double-click reveal · Esc close',
|
|
120
|
+
launcher: 'Local file search (Ctrl+Shift+F)',
|
|
121
|
+
justNow: 'just now', minutesAgo: '{n} min ago', hoursAgo: '{n} h ago',
|
|
122
|
+
},
|
|
123
|
+
zh: {
|
|
124
|
+
placeholder: '搜索文件名或路径… 支持 ext:pdf size:>10mb dm:thisweek path:src !tmp',
|
|
125
|
+
searchLabel: '文件搜索', dialog: '本地文件搜索', closeTitle: '关闭 (Esc)', closeLabel: '关闭',
|
|
126
|
+
filesOnly: '只看文件', dirsOnly: '只看目录', statusLoading: '索引状态读取中…',
|
|
127
|
+
status: '索引 {entries} 条 · {mode} · {roots} 个根目录', live: '实时监听', snapshot: '快照', usn: 'USN',
|
|
128
|
+
sortTitle: '点击切换排序', name: '名称', size: '大小', mtime: '修改时间', path: '路径',
|
|
129
|
+
dirBadge: '目录', fileBadge: '文件', loading: '搜索中…', errorPrefix: '搜索失败:', empty: '没有匹配的文件',
|
|
130
|
+
results: '{total} 条结果', showing: '显示前 {n} 条', indexing: '索引构建中,结果可能不完整',
|
|
131
|
+
copied: '已复制路径', revealed: '已在文件管理器中定位', opened: '已打开', inserted: '已插入 @file',
|
|
132
|
+
insertFailed: '插入失败,已复制 @file', footer: '↑↓ 选择 · Enter 复制 · Ctrl+O 打开 · Ctrl+Enter 插入 @file · 双击定位 · Esc 关闭',
|
|
133
|
+
launcher: '本地文件搜索 (Ctrl+Shift+F)',
|
|
134
|
+
justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前',
|
|
135
|
+
},
|
|
136
|
+
es: {
|
|
137
|
+
placeholder: 'Busca nombres o rutas… p. ej. ext:pdf size:>10mb dm:thisweek path:src !tmp',
|
|
138
|
+
searchLabel: 'Buscar archivos', dialog: 'Búsqueda local de archivos', closeTitle: 'Cerrar (Esc)', closeLabel: 'Cerrar',
|
|
139
|
+
filesOnly: 'Solo archivos', dirsOnly: 'Solo carpetas', statusLoading: 'Leyendo el estado del índice…',
|
|
140
|
+
status: 'Índice: {entries} entradas · {mode} · {roots} raíces', live: 'en vivo', snapshot: 'instantánea', usn: 'USN',
|
|
141
|
+
sortTitle: 'Clic para cambiar el orden', name: 'Nombre', size: 'Tamaño', mtime: 'Modificado', path: 'Ruta',
|
|
142
|
+
dirBadge: 'DIR', fileBadge: 'ARCH', loading: 'Buscando…', errorPrefix: 'Error de búsqueda: ', empty: 'Sin coincidencias',
|
|
143
|
+
results: '{total} resultados', showing: 'mostrando {n}', indexing: 'indexando, resultados incompletos',
|
|
144
|
+
copied: 'Ruta copiada', revealed: 'Mostrado en el gestor de archivos', opened: 'Abierto', inserted: '@file insertado',
|
|
145
|
+
insertFailed: 'No se pudo insertar; @file copiado', footer: '↑↓ elegir · Enter copiar · Ctrl+O abrir · Ctrl+Enter insertar @file · doble clic revelar · Esc cerrar',
|
|
146
|
+
launcher: 'Búsqueda local (Ctrl+Shift+F)',
|
|
147
|
+
justNow: 'ahora', minutesAgo: 'hace {n} min', hoursAgo: 'hace {n} h',
|
|
148
|
+
},
|
|
149
|
+
pt: {
|
|
150
|
+
placeholder: 'Busque nomes ou caminhos… ex. ext:pdf size:>10mb dm:thisweek path:src !tmp',
|
|
151
|
+
searchLabel: 'Buscar arquivos', dialog: 'Busca local de arquivos', closeTitle: 'Fechar (Esc)', closeLabel: 'Fechar',
|
|
152
|
+
filesOnly: 'Somente arquivos', dirsOnly: 'Somente pastas', statusLoading: 'Lendo o estado do índice…',
|
|
153
|
+
status: 'Índice: {entries} entradas · {mode} · {roots} raízes', live: 'ao vivo', snapshot: 'instantâneo', usn: 'USN',
|
|
154
|
+
sortTitle: 'Clique para mudar a ordem', name: 'Nome', size: 'Tamanho', mtime: 'Modificado', path: 'Caminho',
|
|
155
|
+
dirBadge: 'DIR', fileBadge: 'ARQ', loading: 'Buscando…', errorPrefix: 'Falha na busca: ', empty: 'Nenhum resultado',
|
|
156
|
+
results: '{total} resultados', showing: 'mostrando {n}', indexing: 'indexando, resultados incompletos',
|
|
157
|
+
copied: 'Caminho copiado', revealed: 'Mostrado no gerenciador de arquivos', opened: 'Aberto', inserted: '@file inserido',
|
|
158
|
+
insertFailed: 'Falha ao inserir; @file copiado', footer: '↑↓ escolher · Enter copiar · Ctrl+O abrir · Ctrl+Enter inserir @file · clique duplo revelar · Esc fechar',
|
|
159
|
+
launcher: 'Busca local (Ctrl+Shift+F)',
|
|
160
|
+
justNow: 'agora', minutesAgo: 'há {n} min', hoursAgo: 'há {n} h',
|
|
161
|
+
},
|
|
162
|
+
hi: {
|
|
163
|
+
placeholder: 'फ़ाइल नाम या पथ खोजें… जैसे ext:pdf size:>10mb dm:thisweek path:src !tmp',
|
|
164
|
+
searchLabel: 'फ़ाइल खोज', dialog: 'स्थानीय फ़ाइल खोज', closeTitle: 'बंद करें (Esc)', closeLabel: 'बंद करें',
|
|
165
|
+
filesOnly: 'केवल फ़ाइलें', dirsOnly: 'केवल फ़ोल्डर', statusLoading: 'इंडेक्स स्थिति पढ़ी जा रही है…',
|
|
166
|
+
status: 'इंडेक्स {entries} प्रविष्टियाँ · {mode} · {roots} रूट', live: 'लाइव', snapshot: 'स्नैपशॉट', usn: 'USN',
|
|
167
|
+
sortTitle: 'क्रम बदलने के लिए क्लिक करें', name: 'नाम', size: 'आकार', mtime: 'संशोधित', path: 'पथ',
|
|
168
|
+
dirBadge: 'DIR', fileBadge: 'FILE', loading: 'खोज रहे हैं…', errorPrefix: 'खोज विफल: ', empty: 'कोई मेल नहीं',
|
|
169
|
+
results: '{total} परिणाम', showing: 'पहले {n} दिखाए', indexing: 'इंडेक्स बन रहा है, परिणाम अधूरे हो सकते हैं',
|
|
170
|
+
copied: 'पथ कॉपी हुआ', revealed: 'फ़ाइल मैनेजर में दिखाया', opened: 'खोला गया', inserted: '@file डाला गया',
|
|
171
|
+
insertFailed: 'डालना विफल; @file कॉपी हुआ', footer: '↑↓ चुनें · Enter कॉपी · Ctrl+O खोलें · Ctrl+Enter @file डालें · डबल-क्लिक दिखाएँ · Esc बंद',
|
|
172
|
+
launcher: 'स्थानीय फ़ाइल खोज (Ctrl+Shift+F)',
|
|
173
|
+
justNow: 'अभी', minutesAgo: '{n} मिनट पहले', hoursAgo: '{n} घंटे पहले',
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
const LANG = (() => {
|
|
177
|
+
const tag = String((typeof navigator !== 'undefined' && navigator.language) || 'en').toLowerCase()
|
|
178
|
+
if (tag.startsWith('zh')) return 'zh'
|
|
179
|
+
if (tag.startsWith('es')) return 'es'
|
|
180
|
+
if (tag.startsWith('pt')) return 'pt'
|
|
181
|
+
if (tag.startsWith('hi')) return 'hi'
|
|
182
|
+
return 'en'
|
|
183
|
+
})()
|
|
184
|
+
const TEXT = STRINGS[LANG]
|
|
185
|
+
function t(key, vars) {
|
|
186
|
+
let value = TEXT[key] || STRINGS.en[key] || key
|
|
187
|
+
if (vars) value = value.replace(/\{(\w+)\}/g, (_, name) => String(vars[name] ?? ''))
|
|
188
|
+
return value
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatRelative(ms) {
|
|
192
|
+
const diff = Date.now() - ms
|
|
193
|
+
if (diff >= 0 && diff < 60_000) return t('justNow')
|
|
194
|
+
if (diff >= 0 && diff < 3_600_000) return t('minutesAgo', { n: Math.floor(diff / 60_000) })
|
|
195
|
+
if (diff >= 0 && diff < 86_400_000) return t('hoursAgo', { n: Math.floor(diff / 3_600_000) })
|
|
196
|
+
return ''
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function copyText(text) {
|
|
200
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
201
|
+
return navigator.clipboard.writeText(text)
|
|
202
|
+
}
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const area = document.createElement('textarea')
|
|
205
|
+
area.value = text
|
|
206
|
+
document.body.appendChild(area)
|
|
207
|
+
area.select()
|
|
208
|
+
try {
|
|
209
|
+
if (document.execCommand('copy')) resolve()
|
|
210
|
+
else reject(new Error('copy failed'))
|
|
211
|
+
} finally {
|
|
212
|
+
area.remove()
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function insertIntoComposer(text) {
|
|
218
|
+
const element = document.querySelector('textarea, input[type="text"], [contenteditable="true"], [role="textbox"]')
|
|
219
|
+
if (!element) return false
|
|
220
|
+
element.focus()
|
|
221
|
+
if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
|
|
222
|
+
const start = typeof element.selectionStart === 'number' ? element.selectionStart : element.value.length
|
|
223
|
+
const end = typeof element.selectionEnd === 'number' ? element.selectionEnd : start
|
|
224
|
+
if (typeof element.setRangeText === 'function') element.setRangeText(text, start, end, 'end')
|
|
225
|
+
else element.value = element.value.slice(0, start) + text + element.value.slice(end)
|
|
226
|
+
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
227
|
+
return true
|
|
228
|
+
}
|
|
229
|
+
return document.execCommand('insertText', false, text)
|
|
230
|
+
}
|
|
231
|
+
|
|
107
232
|
// ── helpers ─────────────────────────────────────────────────────────────
|
|
108
233
|
function formatSize(bytes) {
|
|
109
234
|
if (!bytes) return ''
|
|
@@ -122,11 +247,8 @@ window.__ModuleLoader__.load({
|
|
|
122
247
|
if (!ms) return ''
|
|
123
248
|
const date = new Date(ms)
|
|
124
249
|
if (Number.isNaN(date.getTime())) return ''
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
if (diff >= 0 && diff < 60_000) return '刚刚'
|
|
128
|
-
if (diff >= 0 && diff < 3_600_000) return Math.floor(diff / 60_000) + ' 分钟前'
|
|
129
|
-
if (diff >= 0 && diff < 86_400_000) return Math.floor(diff / 3_600_000) + ' 小时前'
|
|
250
|
+
const relative = formatRelative(ms)
|
|
251
|
+
if (relative !== '') return relative
|
|
130
252
|
const pad = value => String(value).padStart(2, '0')
|
|
131
253
|
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
|
|
132
254
|
}
|
|
@@ -239,38 +361,38 @@ window.__ModuleLoader__.load({
|
|
|
239
361
|
}
|
|
240
362
|
}, [sort])
|
|
241
363
|
|
|
364
|
+
const notify = useCallback(message => {
|
|
365
|
+
setToast(message)
|
|
366
|
+
setTimeout(() => setToast(''), 1600)
|
|
367
|
+
}, [])
|
|
368
|
+
|
|
242
369
|
const copy = useCallback(hit => {
|
|
243
370
|
if (!hit) return
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
setToast('已复制路径')
|
|
247
|
-
setTimeout(() => setToast(''), 1500)
|
|
248
|
-
}
|
|
249
|
-
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
250
|
-
navigator.clipboard.writeText(text).then(done).catch(() => {})
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
const area = document.createElement('textarea')
|
|
254
|
-
area.value = text
|
|
255
|
-
document.body.appendChild(area)
|
|
256
|
-
area.select()
|
|
257
|
-
try {
|
|
258
|
-
document.execCommand('copy')
|
|
259
|
-
done()
|
|
260
|
-
} finally {
|
|
261
|
-
area.remove()
|
|
262
|
-
}
|
|
263
|
-
}, [])
|
|
371
|
+
copyText(hit.path).then(() => notify(t('copied'))).catch(() => {})
|
|
372
|
+
}, [notify])
|
|
264
373
|
|
|
265
374
|
const reveal = useCallback(hit => {
|
|
266
375
|
if (!hit) return
|
|
267
|
-
post('/dsh-locator/reveal', { path: hit.path }).then(() =>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
})
|
|
273
|
-
}, [copy])
|
|
376
|
+
post('/dsh-locator/reveal', { path: hit.path }).then(() => notify(t('revealed'))).catch(() => copy(hit))
|
|
377
|
+
}, [copy, notify])
|
|
378
|
+
|
|
379
|
+
const openFile = useCallback(hit => {
|
|
380
|
+
if (!hit || hit.dir) return
|
|
381
|
+
post('/dsh-locator/open', { path: hit.path }).then(() => notify(t('opened'))).catch(() => copy(hit))
|
|
382
|
+
}, [copy, notify])
|
|
383
|
+
|
|
384
|
+
const insert = useCallback(hit => {
|
|
385
|
+
if (!hit) return
|
|
386
|
+
const mention = '@"' + hit.path.replace(/"/g, '') + '" '
|
|
387
|
+
close()
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
if (insertIntoComposer(mention)) {
|
|
390
|
+
notify(t('inserted'))
|
|
391
|
+
} else {
|
|
392
|
+
copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
393
|
+
}
|
|
394
|
+
}, 80)
|
|
395
|
+
}, [close, notify])
|
|
274
396
|
|
|
275
397
|
const onKeyDown = useCallback(event => {
|
|
276
398
|
if (event.key === 'Escape') {
|
|
@@ -288,11 +410,22 @@ window.__ModuleLoader__.load({
|
|
|
288
410
|
setSelected(value => Math.max(0, value - 1))
|
|
289
411
|
return
|
|
290
412
|
}
|
|
413
|
+
const modifier = event.ctrlKey || event.metaKey
|
|
414
|
+
if (modifier && String(event.key).toLowerCase() === 'o') {
|
|
415
|
+
event.preventDefault()
|
|
416
|
+
openFile(hits[selected])
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
if (modifier && event.key === 'Enter' && !event.isComposing) {
|
|
420
|
+
event.preventDefault()
|
|
421
|
+
insert(hits[selected])
|
|
422
|
+
return
|
|
423
|
+
}
|
|
291
424
|
if (event.key === 'Enter' && !event.isComposing) {
|
|
292
425
|
event.preventDefault()
|
|
293
426
|
copy(hits[selected])
|
|
294
427
|
}
|
|
295
|
-
}, [close, copy, hits, selected])
|
|
428
|
+
}, [close, copy, hits, insert, openFile, selected])
|
|
296
429
|
|
|
297
430
|
useEffect(() => {
|
|
298
431
|
const list = listRef.current
|
|
@@ -308,7 +441,7 @@ window.__ModuleLoader__.load({
|
|
|
308
441
|
className: sort === key ? 'sorted' : '',
|
|
309
442
|
style: { width },
|
|
310
443
|
onClick: () => toggleSort(key),
|
|
311
|
-
title: '
|
|
444
|
+
title: t('sortTitle'),
|
|
312
445
|
}, label + (sort === key ? (desc ? ' ↓' : ' ↑') : ''))
|
|
313
446
|
|
|
314
447
|
return h('div', {
|
|
@@ -318,7 +451,7 @@ window.__ModuleLoader__.load({
|
|
|
318
451
|
className: 'dsh-locator-panel',
|
|
319
452
|
role: 'dialog',
|
|
320
453
|
'aria-modal': 'true',
|
|
321
|
-
'aria-label': '
|
|
454
|
+
'aria-label': t('dialog'),
|
|
322
455
|
onKeyDown,
|
|
323
456
|
},
|
|
324
457
|
h('div', { className: 'dsh-locator-head' },
|
|
@@ -329,24 +462,28 @@ window.__ModuleLoader__.load({
|
|
|
329
462
|
value: query,
|
|
330
463
|
spellCheck: false,
|
|
331
464
|
autoComplete: 'off',
|
|
332
|
-
placeholder: '
|
|
465
|
+
placeholder: t('placeholder'),
|
|
333
466
|
onChange: event => setQuery(event.target.value),
|
|
334
|
-
'aria-label': '
|
|
467
|
+
'aria-label': t('searchLabel'),
|
|
335
468
|
}),
|
|
336
469
|
h('span', { className: 'dsh-locator-kbd' }, 'Ctrl+Shift+F'),
|
|
337
|
-
h('button', { className: 'dsh-locator-close', onClick: close, title: '
|
|
470
|
+
h('button', { className: 'dsh-locator-close', onClick: close, title: t('closeTitle'), 'aria-label': t('closeLabel') }, '✕'),
|
|
338
471
|
),
|
|
339
472
|
h('div', { className: 'dsh-locator-bar' },
|
|
340
473
|
h('label', { className: 'dsh-locator-check' },
|
|
341
474
|
h('input', { type: 'checkbox', checked: filesOnly, onChange: event => { setFilesOnly(event.target.checked); if (event.target.checked) setDirsOnly(false) } }),
|
|
342
|
-
'
|
|
475
|
+
t('filesOnly')),
|
|
343
476
|
h('label', { className: 'dsh-locator-check' },
|
|
344
477
|
h('input', { type: 'checkbox', checked: dirsOnly, onChange: event => { setDirsOnly(event.target.checked); if (event.target.checked) setFilesOnly(false) } }),
|
|
345
|
-
'
|
|
478
|
+
t('dirsOnly')),
|
|
346
479
|
h('span', { className: 'dsh-locator-status' },
|
|
347
480
|
status === null
|
|
348
|
-
? '
|
|
349
|
-
:
|
|
481
|
+
? t('statusLoading')
|
|
482
|
+
: t('status', {
|
|
483
|
+
entries: (status.entries || 0).toLocaleString(),
|
|
484
|
+
mode: status.watching ? t('live') : t('snapshot'),
|
|
485
|
+
roots: (status.roots || []).length,
|
|
486
|
+
}) + ((status.usnActive || status.usn) ? ' · ' + t('usn') : '')),
|
|
350
487
|
),
|
|
351
488
|
h('div', { className: 'dsh-locator-scroll', ref: listRef },
|
|
352
489
|
h('table', { className: 'dsh-locator-table' },
|
|
@@ -357,15 +494,15 @@ window.__ModuleLoader__.load({
|
|
|
357
494
|
h('col', { style: { width: '41%' } }),
|
|
358
495
|
),
|
|
359
496
|
h('thead', null, h('tr', null,
|
|
360
|
-
header('name', '
|
|
361
|
-
header('size', '
|
|
362
|
-
header('mtime', '
|
|
363
|
-
header('path', '
|
|
497
|
+
header('name', t('name')),
|
|
498
|
+
header('size', t('size')),
|
|
499
|
+
header('mtime', t('mtime')),
|
|
500
|
+
header('path', t('path')),
|
|
364
501
|
)),
|
|
365
502
|
h('tbody', null,
|
|
366
503
|
hits.length === 0
|
|
367
504
|
? h('tr', null, h('td', { colSpan: 4, className: 'dsh-locator-empty' },
|
|
368
|
-
loading ? '
|
|
505
|
+
loading ? t('loading') : error !== '' ? t('errorPrefix') + error : t('empty')))
|
|
369
506
|
: hits.map((hit, index) => h('tr', {
|
|
370
507
|
key: hit.path,
|
|
371
508
|
'data-index': index,
|
|
@@ -375,7 +512,7 @@ window.__ModuleLoader__.load({
|
|
|
375
512
|
onDoubleClick: () => reveal(hit),
|
|
376
513
|
},
|
|
377
514
|
h('td', null, h('div', { className: 'dsh-locator-name' },
|
|
378
|
-
h('span', { className: 'dsh-locator-badge' }, hit.dir ? '
|
|
515
|
+
h('span', { className: 'dsh-locator-badge' }, hit.dir ? t('dirBadge') : (hit.ext || t('fileBadge')).toUpperCase()),
|
|
379
516
|
h('span', null, highlight(hit.name, query)),
|
|
380
517
|
)),
|
|
381
518
|
h('td', { className: 'dsh-locator-size' }, hit.dir ? '' : formatSize(hit.size)),
|
|
@@ -388,12 +525,12 @@ window.__ModuleLoader__.load({
|
|
|
388
525
|
h('div', { className: 'dsh-locator-foot' },
|
|
389
526
|
h('span', null,
|
|
390
527
|
loading
|
|
391
|
-
? '
|
|
392
|
-
: total.toLocaleString()
|
|
393
|
-
+ (status && status.scanning ? ' ·
|
|
528
|
+
? t('loading')
|
|
529
|
+
: t('results', { total: total.toLocaleString() }) + (truncated ? ' (' + t('showing', { n: hits.length }) + ')' : '') + ' · ' + took.toFixed(1) + ' ms'
|
|
530
|
+
+ (status && status.scanning ? ' · ' + t('indexing') : '')),
|
|
394
531
|
h('span', { className: 'grow' }),
|
|
395
532
|
toast === '' ? null : h('span', { className: 'dsh-locator-toast' }, toast),
|
|
396
|
-
h('span', null, '
|
|
533
|
+
h('span', null, t('footer')),
|
|
397
534
|
),
|
|
398
535
|
))
|
|
399
536
|
}
|
|
@@ -401,8 +538,8 @@ window.__ModuleLoader__.load({
|
|
|
401
538
|
function Launcher() {
|
|
402
539
|
return h('button', {
|
|
403
540
|
className: 'dsh-locator-launch',
|
|
404
|
-
title: '
|
|
405
|
-
'aria-label': '
|
|
541
|
+
title: t('launcher'),
|
|
542
|
+
'aria-label': t('searchLabel'),
|
|
406
543
|
onClick: () => store.toggle(),
|
|
407
544
|
}, '🔎')
|
|
408
545
|
}
|
package/lib/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,cAAc,CAAA;AA+B9D,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,cAAc,CAAA;AA+B9D,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CA6BnE"}
|
package/lib/http.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* service, and the plugin still works there through its tools.
|
|
12
12
|
* @module dsh-locator/http
|
|
13
13
|
*/
|
|
14
|
-
import { revealInFileManager, withinRoots } from "./reveal.js";
|
|
14
|
+
import { openWithDefaultApp, revealInFileManager, withinRoots } from "./reveal.js";
|
|
15
15
|
import { statSync } from 'node:fs';
|
|
16
16
|
const BODY_LIMIT = 64 * 1024;
|
|
17
17
|
/** Mount the panel's routes when this profile runs a Web server. */
|
|
@@ -38,6 +38,11 @@ export function registerHttp(ctx, rt) {
|
|
|
38
38
|
path: '/dsh-locator/reveal',
|
|
39
39
|
handler: handler(scoped, async (body) => await reveal(rt, body)),
|
|
40
40
|
}));
|
|
41
|
+
scoped.effect(() => server.register({
|
|
42
|
+
kind: 'exact',
|
|
43
|
+
path: '/dsh-locator/open',
|
|
44
|
+
handler: handler(scoped, async (body) => await openPath(rt, body)),
|
|
45
|
+
}));
|
|
41
46
|
});
|
|
42
47
|
}
|
|
43
48
|
function handler(scoped, run) {
|
|
@@ -111,6 +116,27 @@ async function reveal(rt, raw) {
|
|
|
111
116
|
return { ok: false, error: messageOf(error) };
|
|
112
117
|
}
|
|
113
118
|
}
|
|
119
|
+
async function openPath(rt, raw) {
|
|
120
|
+
const path = typeof raw.path === 'string' ? raw.path : '';
|
|
121
|
+
if (path === '' || !withinRoots(path, rt.roots)) {
|
|
122
|
+
return { ok: false, error: 'path is outside the indexed roots' };
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const info = statSync(path);
|
|
126
|
+
if (!info.isFile())
|
|
127
|
+
return { ok: false, error: 'not a file' };
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { ok: false, error: 'path does not exist' };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await openWithDefaultApp(path);
|
|
134
|
+
return { ok: true };
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return { ok: false, error: messageOf(error) };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
114
140
|
async function status(rt) {
|
|
115
141
|
rt.start();
|
|
116
142
|
const engine = await rt.engineStatus();
|
package/lib/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/lib/reveal.d.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
/** Whether target is one of the roots or lives below one of them. */
|
|
6
6
|
export declare function withinRoots(target: string, roots: readonly string[]): boolean;
|
|
7
7
|
/**
|
|
8
|
-
* Ask the platform
|
|
9
|
-
* a shell, so a path can never be interpreted as a command. The
|
|
10
|
-
* detached: the opener outlives the plugin call.
|
|
8
|
+
* Ask the platform to reveal a path in its file manager. Uses an argument
|
|
9
|
+
* vector, not a shell, so a path can never be interpreted as a command. The
|
|
10
|
+
* child is detached: the opener outlives the plugin call.
|
|
11
11
|
*/
|
|
12
12
|
export declare function revealInFileManager(target: string): Promise<void>;
|
|
13
|
+
/** Open a path with its default application. */
|
|
14
|
+
export declare function openWithDefaultApp(target: string): Promise<void>;
|
|
13
15
|
//# sourceMappingURL=reveal.d.ts.map
|
package/lib/reveal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reveal.d.ts","sourceRoot":"","sources":["../src/reveal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAS7E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"reveal.d.ts","sourceRoot":"","sources":["../src/reveal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAS7E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvE;AAED,gDAAgD;AAChD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKtE"}
|
package/lib/reveal.js
CHANGED
|
@@ -18,26 +18,28 @@ export function withinRoots(target, roots) {
|
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Ask the platform
|
|
22
|
-
* a shell, so a path can never be interpreted as a command. The
|
|
23
|
-
* detached: the opener outlives the plugin call.
|
|
21
|
+
* Ask the platform to reveal a path in its file manager. Uses an argument
|
|
22
|
+
* vector, not a shell, so a path can never be interpreted as a command. The
|
|
23
|
+
* child is detached: the opener outlives the plugin call.
|
|
24
24
|
*/
|
|
25
25
|
export async function revealInFileManager(target) {
|
|
26
26
|
const absolute = resolve(target);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (process.platform === '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
if (process.platform === 'win32')
|
|
28
|
+
return spawnDetached('explorer.exe', [`/select,${absolute}`]);
|
|
29
|
+
if (process.platform === 'darwin')
|
|
30
|
+
return spawnDetached('open', ['-R', absolute]);
|
|
31
|
+
return spawnDetached('xdg-open', [dirname(absolute)]);
|
|
32
|
+
}
|
|
33
|
+
/** Open a path with its default application. */
|
|
34
|
+
export async function openWithDefaultApp(target) {
|
|
35
|
+
const absolute = resolve(target);
|
|
36
|
+
if (process.platform === 'win32')
|
|
37
|
+
return spawnDetached('explorer.exe', [absolute]);
|
|
38
|
+
if (process.platform === 'darwin')
|
|
39
|
+
return spawnDetached('open', [absolute]);
|
|
40
|
+
return spawnDetached('xdg-open', [absolute]);
|
|
41
|
+
}
|
|
42
|
+
async function spawnDetached(command, args) {
|
|
41
43
|
await new Promise((resolvePromise, reject) => {
|
|
42
44
|
const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
43
45
|
child.once('error', reject);
|
package/lib/reveal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reveal.js","sourceRoot":"","sources":["../src/reveal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAElE,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,KAAwB;IAClE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC1B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QACpC,IAAI,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;IAC5E,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAc;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,IAAI,
|
|
1
|
+
{"version":3,"file":"reveal.js","sourceRoot":"","sources":["../src/reveal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAElE,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,KAAwB;IAClE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC1B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QACpC,IAAI,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;IAC5E,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAc;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,aAAa,CAAC,cAAc,EAAE,CAAC,WAAW,QAAQ,EAAE,CAAC,CAAC,CAAA;IAC/F,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAA;IACjF,OAAO,aAAa,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;AACvD,CAAC;AAED,gDAAgD;AAChD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,aAAa,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAClF,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC3E,OAAO,aAAa,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;AAC9C,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,OAAe,EAAE,IAAc;IAC1D,MAAM,IAAI,OAAO,CAAO,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1F,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC3B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;YACvB,KAAK,CAAC,KAAK,EAAE,CAAA;YACb,cAAc,EAAE,CAAA;QAClB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-locator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Whole-disk, millisecond file search for DeepSeek Harness without Everything: a self-built cross-platform index, an Everything-style query language, agent tools and a Web search panel (Ctrl+Shift+F). Windows, macOS and Linux.",
|
|
5
5
|
"author": "PerryLink contributors",
|
|
6
6
|
"repository": {
|
package/src/http.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { Context } from '@deepseek-ai/cordis'
|
|
16
16
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
17
17
|
import type { LocatorRuntime, SearchArgs } from './runtime.ts'
|
|
18
|
-
import { revealInFileManager, withinRoots } from './reveal.ts'
|
|
18
|
+
import { openWithDefaultApp, revealInFileManager, withinRoots } from './reveal.ts'
|
|
19
19
|
import { statSync } from 'node:fs'
|
|
20
20
|
|
|
21
21
|
interface WebServerLike {
|
|
@@ -69,6 +69,11 @@ export function registerHttp(ctx: Context, rt: LocatorRuntime): void {
|
|
|
69
69
|
path: '/dsh-locator/reveal',
|
|
70
70
|
handler: handler(scoped, async body => await reveal(rt, body)),
|
|
71
71
|
}))
|
|
72
|
+
scoped.effect(() => server.register({
|
|
73
|
+
kind: 'exact',
|
|
74
|
+
path: '/dsh-locator/open',
|
|
75
|
+
handler: handler(scoped, async body => await openPath(rt, body)),
|
|
76
|
+
}))
|
|
72
77
|
})
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -145,6 +150,25 @@ async function reveal(rt: LocatorRuntime, raw: Record<string, unknown>): Promise
|
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
async function openPath(rt: LocatorRuntime, raw: Record<string, unknown>): Promise<unknown> {
|
|
154
|
+
const path = typeof raw.path === 'string' ? raw.path : ''
|
|
155
|
+
if (path === '' || !withinRoots(path, rt.roots)) {
|
|
156
|
+
return { ok: false, error: 'path is outside the indexed roots' }
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const info = statSync(path)
|
|
160
|
+
if (!info.isFile()) return { ok: false, error: 'not a file' }
|
|
161
|
+
} catch {
|
|
162
|
+
return { ok: false, error: 'path does not exist' }
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await openWithDefaultApp(path)
|
|
166
|
+
return { ok: true }
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { ok: false, error: messageOf(error) }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
async function status(rt: LocatorRuntime): Promise<unknown> {
|
|
149
173
|
rt.start()
|
|
150
174
|
const engine = await rt.engineStatus()
|
package/src/reveal.ts
CHANGED
|
@@ -19,24 +19,26 @@ export function withinRoots(target: string, roots: readonly string[]): boolean {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Ask the platform
|
|
23
|
-
* a shell, so a path can never be interpreted as a command. The
|
|
24
|
-
* detached: the opener outlives the plugin call.
|
|
22
|
+
* Ask the platform to reveal a path in its file manager. Uses an argument
|
|
23
|
+
* vector, not a shell, so a path can never be interpreted as a command. The
|
|
24
|
+
* child is detached: the opener outlives the plugin call.
|
|
25
25
|
*/
|
|
26
26
|
export async function revealInFileManager(target: string): Promise<void> {
|
|
27
27
|
const absolute = resolve(target)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
if (process.platform === 'win32') return spawnDetached('explorer.exe', [`/select,${absolute}`])
|
|
29
|
+
if (process.platform === 'darwin') return spawnDetached('open', ['-R', absolute])
|
|
30
|
+
return spawnDetached('xdg-open', [dirname(absolute)])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Open a path with its default application. */
|
|
34
|
+
export async function openWithDefaultApp(target: string): Promise<void> {
|
|
35
|
+
const absolute = resolve(target)
|
|
36
|
+
if (process.platform === 'win32') return spawnDetached('explorer.exe', [absolute])
|
|
37
|
+
if (process.platform === 'darwin') return spawnDetached('open', [absolute])
|
|
38
|
+
return spawnDetached('xdg-open', [absolute])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function spawnDetached(command: string, args: string[]): Promise<void> {
|
|
40
42
|
await new Promise<void>((resolvePromise, reject) => {
|
|
41
43
|
const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true })
|
|
42
44
|
child.once('error', reject)
|