windowpp 0.1.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.
- package/bin/windowpp.js +86 -0
- package/cmake/embed_assets.py +144 -0
- package/framework/CMakeLists.txt +176 -0
- package/framework/include/windowpp/windowpp.h +704 -0
- package/framework/src/AppData/API/AppData.ts +137 -0
- package/framework/src/AppData/appdata_bridge.h +138 -0
- package/framework/src/AppData/appdata_manager.cpp +126 -0
- package/framework/src/AppData/appdata_manager.h +3 -0
- package/framework/src/FileSystem/API/FileSystem.ts +389 -0
- package/framework/src/FileSystem/Linux/filesearch.cpp +148 -0
- package/framework/src/FileSystem/Linux/readfile.cpp +79 -0
- package/framework/src/FileSystem/Linux/savefile.cpp +333 -0
- package/framework/src/FileSystem/MacOS/filesearch.cpp +149 -0
- package/framework/src/FileSystem/MacOS/readfile.cpp +80 -0
- package/framework/src/FileSystem/MacOS/savefile.cpp +264 -0
- package/framework/src/FileSystem/Windows/filesearch.cpp +195 -0
- package/framework/src/FileSystem/Windows/readfile.cpp +122 -0
- package/framework/src/FileSystem/Windows/savefile.cpp +290 -0
- package/framework/src/FileSystem/file_index_service.cpp +262 -0
- package/framework/src/FileSystem/file_index_service.h +55 -0
- package/framework/src/FileSystem/filesystem_bridge.h +243 -0
- package/framework/src/FileSystem/filesystem_handler.h +93 -0
- package/framework/src/FileSystem/filesystem_json.h +241 -0
- package/framework/src/FileSystem/filesystem_search_service.cpp +414 -0
- package/framework/src/FileSystem/filesystem_search_service.h +94 -0
- package/framework/src/Input/API/Input.ts +161 -0
- package/framework/src/Input/Linux/linux_key_utils.h +135 -0
- package/framework/src/Input/MacOS/macos_key_utils.h +137 -0
- package/framework/src/Input/Windows/win32_key_utils.h +199 -0
- package/framework/src/Input/input_bridge.h +192 -0
- package/framework/src/Input/input_service.cpp +584 -0
- package/framework/src/Input/input_service.h +21 -0
- package/framework/src/application.cpp +29 -0
- package/framework/src/common/hit_test.cpp +40 -0
- package/framework/src/common/image_loader.cpp +24 -0
- package/framework/src/common/paths.cpp +75 -0
- package/framework/src/filedrop/filedrop.cpp +316 -0
- package/framework/src/filedrop/filedrop.css +421 -0
- package/framework/src/filedrop/filedrop.hpp +92 -0
- package/framework/src/filedrop/filedrop.ts +183 -0
- package/framework/src/platform/API/App.ts +156 -0
- package/framework/src/platform/API/Window.ts +249 -0
- package/framework/src/platform/linux/app_linux.cpp +256 -0
- package/framework/src/platform/linux/app_linux.h +64 -0
- package/framework/src/platform/linux/linux_helpers.cpp +26 -0
- package/framework/src/platform/linux/linux_helpers.h +19 -0
- package/framework/src/platform/linux/tray_linux.cpp +21 -0
- package/framework/src/platform/linux/tray_linux.h +26 -0
- package/framework/src/platform/linux/window_linux.cpp +256 -0
- package/framework/src/platform/linux/window_linux.h +70 -0
- package/framework/src/platform/macos/app_macos.h +59 -0
- package/framework/src/platform/macos/app_macos.mm +223 -0
- package/framework/src/platform/macos/macos_helpers.h +21 -0
- package/framework/src/platform/macos/tray_macos.h +22 -0
- package/framework/src/platform/macos/tray_macos.mm +53 -0
- package/framework/src/platform/macos/window_macos.h +74 -0
- package/framework/src/platform/macos/window_macos.mm +318 -0
- package/framework/src/platform/platform_bridge.h +514 -0
- package/framework/src/platform/platform_factory.cpp +33 -0
- package/framework/src/platform/platform_factory.h +19 -0
- package/framework/src/platform/win32/app_win32.cpp +572 -0
- package/framework/src/platform/win32/app_win32.h +83 -0
- package/framework/src/platform/win32/tray_win32.cpp +57 -0
- package/framework/src/platform/win32/tray_win32.h +30 -0
- package/framework/src/platform/win32/win32_helpers.h +61 -0
- package/framework/src/platform/win32/window_win32.cpp +267 -0
- package/framework/src/platform/win32/window_win32.h +79 -0
- package/framework/src/renderer/webgpu.h +128 -0
- package/framework/src/renderer/webview/include/WebView2.h +48014 -0
- package/framework/src/renderer/webview/include/WebView2EnvironmentOptions.h +342 -0
- package/framework/src/renderer/webview/webview.h +13 -0
- package/framework/src/renderer/webview/webview_linux.cpp +392 -0
- package/framework/src/renderer/webview/webview_macos.mm +388 -0
- package/framework/src/renderer/webview/webview_win32.cpp +688 -0
- package/framework/src/renderer/webview/x64/WebView2Loader.dll +0 -0
- package/framework/src/renderer/webview/x64/WebView2Loader.lib +0 -0
- package/framework/src/renderer/webview/x64/WebView2LoaderStatic.lib +0 -0
- package/lib/build.js +112 -0
- package/lib/create.js +283 -0
- package/lib/dev.js +155 -0
- package/package.json +24 -0
- package/scripts/publish.js +67 -0
- package/scripts/sync-framework.js +73 -0
- package/templates/solid/CMakeLists.txt +56 -0
- package/templates/solid/frontend/package.json +22 -0
- package/templates/solid/frontend/vite.config.ts +25 -0
- package/templates/solid/main.cpp +72 -0
- package/templates/solid/package.json +12 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// FileSystem/filesystem_search_service.cpp
|
|
3
|
+
// Multi-threaded streamed filesystem search with cancellation.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
#include "filesystem_search_service.h"
|
|
7
|
+
|
|
8
|
+
#include "file_index_service.h"
|
|
9
|
+
|
|
10
|
+
#include <filesystem>
|
|
11
|
+
#include <thread>
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
|
|
14
|
+
#ifdef _WIN32
|
|
15
|
+
#include <windows.h>
|
|
16
|
+
#include <shlwapi.h>
|
|
17
|
+
#pragma comment(lib, "shlwapi.lib")
|
|
18
|
+
#endif
|
|
19
|
+
|
|
20
|
+
namespace wpp::fs {
|
|
21
|
+
|
|
22
|
+
// ─── ThreadPool Implementation ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
ThreadPool::ThreadPool(size_t num_threads) {
|
|
25
|
+
// Default to hardware concurrency if not specified, min 2, max 8
|
|
26
|
+
size_t actual_threads = num_threads;
|
|
27
|
+
if (actual_threads == 0) {
|
|
28
|
+
actual_threads = std::thread::hardware_concurrency();
|
|
29
|
+
actual_threads = std::max(size_t(2), std::min(actual_threads, size_t(8)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
workers_.reserve(actual_threads);
|
|
33
|
+
for (size_t i = 0; i < actual_threads; ++i) {
|
|
34
|
+
workers_.emplace_back(&ThreadPool::worker_thread, this);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ThreadPool::~ThreadPool() {
|
|
39
|
+
shutdown();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void ThreadPool::worker_thread() {
|
|
43
|
+
while (true) {
|
|
44
|
+
std::function<void()> task;
|
|
45
|
+
{
|
|
46
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
|
47
|
+
cv_.wait(lock, [this] { return stop_.load() || !tasks_.empty(); });
|
|
48
|
+
|
|
49
|
+
if (stop_.load() && tasks_.empty()) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
task = std::move(tasks_.front());
|
|
54
|
+
tasks_.pop();
|
|
55
|
+
}
|
|
56
|
+
task();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void ThreadPool::shutdown() {
|
|
61
|
+
stop_.store(true);
|
|
62
|
+
cv_.notify_all();
|
|
63
|
+
for (auto& worker : workers_) {
|
|
64
|
+
if (worker.joinable()) {
|
|
65
|
+
worker.join();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
workers_.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Platform-specific glob matching ────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
#ifdef _WIN32
|
|
74
|
+
|
|
75
|
+
// Windows: Use PathMatchSpecW for fast native glob matching
|
|
76
|
+
static std::wstring toWide(const std::string& utf8) {
|
|
77
|
+
if (utf8.empty()) return {};
|
|
78
|
+
int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
|
|
79
|
+
std::wstring ws(len, L'\0');
|
|
80
|
+
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, ws.data(), len);
|
|
81
|
+
if (!ws.empty() && ws.back() == L'\0') ws.pop_back();
|
|
82
|
+
return ws;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static bool matches_glob(const std::string& filename, const std::string& pattern) {
|
|
86
|
+
std::wstring wfilename = toWide(filename);
|
|
87
|
+
std::wstring wpattern = toWide(pattern);
|
|
88
|
+
return PathMatchSpecW(wfilename.c_str(), wpattern.c_str()) != 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#else
|
|
92
|
+
|
|
93
|
+
// POSIX: Use fnmatch for fast native glob matching
|
|
94
|
+
#include <fnmatch.h>
|
|
95
|
+
|
|
96
|
+
static bool matches_glob(const std::string& filename, const std::string& pattern) {
|
|
97
|
+
#if defined(__APPLE__)
|
|
98
|
+
return fnmatch(pattern.c_str(), filename.c_str(), FNM_CASEFOLD) == 0;
|
|
99
|
+
#else
|
|
100
|
+
return fnmatch(pattern.c_str(), filename.c_str(), 0) == 0;
|
|
101
|
+
#endif
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#endif
|
|
105
|
+
|
|
106
|
+
static bool is_hidden_entry(const std::filesystem::directory_entry& entry) {
|
|
107
|
+
const auto filename = entry.path().filename().u8string();
|
|
108
|
+
if (!filename.empty() && filename[0] == '.') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#ifdef _WIN32
|
|
113
|
+
const DWORD attributes = GetFileAttributesW(entry.path().c_str());
|
|
114
|
+
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_HIDDEN) != 0;
|
|
115
|
+
#else
|
|
116
|
+
return false;
|
|
117
|
+
#endif
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── FileSearchService Implementation ───────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
FileSearchService::FileSearchService() : threadPool_(4) {}
|
|
123
|
+
|
|
124
|
+
FileSearchService::~FileSearchService() {
|
|
125
|
+
threadPool_.shutdown();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
FileSearchService& FileSearchService::instance() {
|
|
129
|
+
static FileSearchService service;
|
|
130
|
+
return service;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void FileSearchService::configure_cache_root(const std::string& cacheRoot) {
|
|
134
|
+
FileIndexService::instance().configure_cache_root(cacheRoot);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
std::string FileSearchService::start(const std::vector<std::string>& roots,
|
|
138
|
+
const SearchOptions& options,
|
|
139
|
+
StreamBatchCallback onBatch,
|
|
140
|
+
StreamDoneCallback onDone,
|
|
141
|
+
const std::string& preferredId) {
|
|
142
|
+
auto state = std::make_shared<SearchState>();
|
|
143
|
+
state->id = preferredId.empty()
|
|
144
|
+
? std::to_string(next_id_.fetch_add(1))
|
|
145
|
+
: preferredId;
|
|
146
|
+
state->roots = roots;
|
|
147
|
+
state->options = options;
|
|
148
|
+
state->onBatch = std::move(onBatch);
|
|
149
|
+
state->onDone = std::move(onDone);
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
153
|
+
searches_[state->id] = state;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
std::thread([this, state]() {
|
|
157
|
+
run_search(state);
|
|
158
|
+
}).detach();
|
|
159
|
+
|
|
160
|
+
return state->id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
void FileSearchService::cancel(const std::string& searchId) {
|
|
164
|
+
std::shared_ptr<SearchState> state;
|
|
165
|
+
{
|
|
166
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
167
|
+
auto it = searches_.find(searchId);
|
|
168
|
+
if (it == searches_.end()) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
state = it->second;
|
|
172
|
+
}
|
|
173
|
+
state->cancelled.store(true);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
void FileSearchService::search_root(const std::shared_ptr<SearchState>& state,
|
|
177
|
+
const std::string& root,
|
|
178
|
+
std::vector<std::string>& results,
|
|
179
|
+
std::mutex& resultsMutex) {
|
|
180
|
+
const auto pattern = state->options.pattern.empty() ? std::string("*") : state->options.pattern;
|
|
181
|
+
const auto max_results = state->options.maxResults;
|
|
182
|
+
const auto recursive = state->options.recursive;
|
|
183
|
+
const auto includeHidden = state->options.includeHidden;
|
|
184
|
+
const auto includeDirectories = state->options.includeDirectories;
|
|
185
|
+
|
|
186
|
+
if (recursive) {
|
|
187
|
+
uint64_t indexed_entries = 0;
|
|
188
|
+
const auto indexed = FileIndexService::instance().get_or_build_root_index(
|
|
189
|
+
root,
|
|
190
|
+
&state->cancelled,
|
|
191
|
+
indexed_entries);
|
|
192
|
+
state->scannedCount.fetch_add(indexed_entries);
|
|
193
|
+
|
|
194
|
+
for (const auto& entry : indexed) {
|
|
195
|
+
if (state->cancelled.load()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!includeHidden && entry.isHidden) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (max_results > 0 && state->matchedCount.load() >= max_results) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (entry.isDir && !includeDirectories) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (matches_glob(entry.name, pattern)) {
|
|
212
|
+
std::lock_guard<std::mutex> lock(resultsMutex);
|
|
213
|
+
results.push_back(entry.path);
|
|
214
|
+
state->matchedCount.fetch_add(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
auto is_file = [&](const std::filesystem::path& path) -> bool {
|
|
221
|
+
std::error_code ec;
|
|
222
|
+
return std::filesystem::is_regular_file(path, ec);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
auto is_dir = [&](const std::filesystem::path& path) -> bool {
|
|
226
|
+
std::error_code ec;
|
|
227
|
+
return std::filesystem::is_directory(path, ec);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Start processing from root
|
|
231
|
+
if (is_file(root)) {
|
|
232
|
+
state->scannedCount.fetch_add(1);
|
|
233
|
+
std::filesystem::path path(root);
|
|
234
|
+
const auto filename = path.filename().u8string();
|
|
235
|
+
if (matches_glob(filename, pattern)) {
|
|
236
|
+
std::lock_guard<std::mutex> lock(resultsMutex);
|
|
237
|
+
results.push_back(root);
|
|
238
|
+
state->matchedCount.fetch_add(1);
|
|
239
|
+
}
|
|
240
|
+
} else if (is_dir(root)) {
|
|
241
|
+
std::vector<std::filesystem::path> pending_dirs;
|
|
242
|
+
pending_dirs.push_back(root);
|
|
243
|
+
|
|
244
|
+
while (!pending_dirs.empty()) {
|
|
245
|
+
if (state->cancelled.load()) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const std::filesystem::path dir = std::move(pending_dirs.back());
|
|
250
|
+
pending_dirs.pop_back();
|
|
251
|
+
|
|
252
|
+
std::error_code ec;
|
|
253
|
+
auto it = std::filesystem::directory_iterator(
|
|
254
|
+
dir,
|
|
255
|
+
std::filesystem::directory_options::skip_permission_denied,
|
|
256
|
+
ec);
|
|
257
|
+
if (ec) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const auto& entry : it) {
|
|
262
|
+
if (state->cancelled.load()) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (max_results > 0 && state->matchedCount.load() >= max_results) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!includeHidden && is_hidden_entry(entry)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
state->scannedCount.fetch_add(1);
|
|
275
|
+
const bool is_directory = is_dir(entry.path());
|
|
276
|
+
if (is_directory) {
|
|
277
|
+
const auto dirname = entry.path().filename().u8string();
|
|
278
|
+
if (includeDirectories && matches_glob(dirname, pattern)) {
|
|
279
|
+
std::lock_guard<std::mutex> lock(resultsMutex);
|
|
280
|
+
results.push_back(entry.path().u8string());
|
|
281
|
+
state->matchedCount.fetch_add(1);
|
|
282
|
+
if (max_results > 0 && state->matchedCount.load() >= max_results) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (recursive) {
|
|
287
|
+
pending_dirs.push_back(entry.path());
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const auto filename = entry.path().filename().u8string();
|
|
293
|
+
if (matches_glob(filename, pattern)) {
|
|
294
|
+
std::lock_guard<std::mutex> lock(resultsMutex);
|
|
295
|
+
results.push_back(entry.path().u8string());
|
|
296
|
+
state->matchedCount.fetch_add(1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
void FileSearchService::run_search(const std::shared_ptr<SearchState>& state) {
|
|
304
|
+
StreamSearchSummary summary;
|
|
305
|
+
const auto max_results = state->options.maxResults;
|
|
306
|
+
|
|
307
|
+
std::vector<std::string> batch;
|
|
308
|
+
batch.reserve(24);
|
|
309
|
+
std::mutex batchMutex;
|
|
310
|
+
|
|
311
|
+
std::vector<std::string> allResults;
|
|
312
|
+
std::mutex resultsMutex;
|
|
313
|
+
|
|
314
|
+
// Auto-flush timer for instant UI updates without manual intervals
|
|
315
|
+
auto flush_batch = [&]() {
|
|
316
|
+
std::lock_guard<std::mutex> lock(batchMutex);
|
|
317
|
+
if (batch.empty()) return;
|
|
318
|
+
|
|
319
|
+
// Flush immediately, no waiting
|
|
320
|
+
state->onBatch(state->id, std::move(batch));
|
|
321
|
+
batch.clear();
|
|
322
|
+
batch.reserve(24);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// Enqueue all roots for parallel processing
|
|
327
|
+
for (const auto& root : state->roots) {
|
|
328
|
+
if (root.empty()) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
std::error_code ec;
|
|
333
|
+
if (!std::filesystem::exists(root, ec)) {
|
|
334
|
+
if (summary.error.empty()) {
|
|
335
|
+
summary.error = "Path does not exist: " + root;
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
state->activeWorkers.fetch_add(1);
|
|
341
|
+
threadPool_.enqueue([this, state, root, &allResults, &resultsMutex]() {
|
|
342
|
+
search_root(state, root, allResults, resultsMutex);
|
|
343
|
+
state->activeWorkers.fetch_sub(1);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Wait for all workers to complete or cancellation
|
|
348
|
+
while (!state->cancelled.load() && state->activeWorkers.load() > 0) {
|
|
349
|
+
// Flush frequently for immediate feedback - every 8 items or every 5ms
|
|
350
|
+
if (batch.size() >= 3) {
|
|
351
|
+
flush_batch();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Collect any new results into batch immediately
|
|
355
|
+
{
|
|
356
|
+
std::lock_guard<std::mutex> lock(resultsMutex);
|
|
357
|
+
if (!allResults.empty()) {
|
|
358
|
+
size_t to_take = std::min(allResults.size(), size_t(8) - batch.size());
|
|
359
|
+
if (to_take > 0) {
|
|
360
|
+
batch.insert(batch.end(),
|
|
361
|
+
std::make_move_iterator(allResults.begin()),
|
|
362
|
+
std::make_move_iterator(allResults.begin() + to_take));
|
|
363
|
+
allResults.erase(allResults.begin(), allResults.begin() + to_take);
|
|
364
|
+
if (batch.size() >= 2) {
|
|
365
|
+
flush_batch();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (max_results > 0 && state->matchedCount.load() >= max_results) {
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Very short sleep - just yield to reduce CPU while staying responsive
|
|
376
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Collect any remaining results
|
|
380
|
+
{
|
|
381
|
+
std::lock_guard<std::mutex> lock(resultsMutex);
|
|
382
|
+
if (!allResults.empty()) {
|
|
383
|
+
batch.insert(batch.end(),
|
|
384
|
+
std::make_move_iterator(allResults.begin()),
|
|
385
|
+
std::make_move_iterator(allResults.end()));
|
|
386
|
+
allResults.clear();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
} catch (const std::exception& e) {
|
|
391
|
+
summary.error = e.what();
|
|
392
|
+
} catch (...) {
|
|
393
|
+
summary.error = "Unknown filesystem search error";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
summary.scannedEntries = state->scannedCount.load();
|
|
397
|
+
summary.matchedEntries = state->matchedCount.load();
|
|
398
|
+
summary.cancelled = state->cancelled.load();
|
|
399
|
+
summary.truncated = (max_results > 0 && state->matchedCount.load() >= max_results);
|
|
400
|
+
|
|
401
|
+
flush_batch();
|
|
402
|
+
finish_search(state, summary);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
void FileSearchService::finish_search(const std::shared_ptr<SearchState>& state,
|
|
406
|
+
const StreamSearchSummary& summary) {
|
|
407
|
+
{
|
|
408
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
409
|
+
searches_.erase(state->id);
|
|
410
|
+
}
|
|
411
|
+
state->onDone(state->id, summary);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
} // namespace wpp::fs
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// FileSystem/filesystem_search_service.h
|
|
3
|
+
// Cross-platform streamed filesystem search with cancellation and multi-threading.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
#pragma once
|
|
7
|
+
|
|
8
|
+
#include <atomic>
|
|
9
|
+
#include <condition_variable>
|
|
10
|
+
#include <functional>
|
|
11
|
+
#include <memory>
|
|
12
|
+
#include <mutex>
|
|
13
|
+
#include <queue>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <thread>
|
|
16
|
+
#include <unordered_map>
|
|
17
|
+
#include <vector>
|
|
18
|
+
|
|
19
|
+
#include "filesystem_handler.h"
|
|
20
|
+
|
|
21
|
+
namespace wpp::fs {
|
|
22
|
+
|
|
23
|
+
// Thread pool for parallel file scanning
|
|
24
|
+
class ThreadPool {
|
|
25
|
+
public:
|
|
26
|
+
explicit ThreadPool(size_t num_threads = 0);
|
|
27
|
+
~ThreadPool();
|
|
28
|
+
|
|
29
|
+
template<typename F>
|
|
30
|
+
void enqueue(F&& task) {
|
|
31
|
+
{
|
|
32
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
|
33
|
+
tasks_.emplace(std::forward<F>(task));
|
|
34
|
+
}
|
|
35
|
+
cv_.notify_one();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
void shutdown();
|
|
39
|
+
|
|
40
|
+
private:
|
|
41
|
+
void worker_thread();
|
|
42
|
+
|
|
43
|
+
std::vector<std::thread> workers_;
|
|
44
|
+
std::queue<std::function<void()>> tasks_;
|
|
45
|
+
std::mutex mutex_;
|
|
46
|
+
std::condition_variable cv_;
|
|
47
|
+
std::atomic<bool> stop_{false};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
class FileSearchService {
|
|
51
|
+
public:
|
|
52
|
+
static FileSearchService& instance();
|
|
53
|
+
|
|
54
|
+
void configure_cache_root(const std::string& cacheRoot);
|
|
55
|
+
|
|
56
|
+
std::string start(const std::vector<std::string>& roots,
|
|
57
|
+
const SearchOptions& options,
|
|
58
|
+
StreamBatchCallback onBatch,
|
|
59
|
+
StreamDoneCallback onDone,
|
|
60
|
+
const std::string& preferredId = "");
|
|
61
|
+
|
|
62
|
+
void cancel(const std::string& searchId);
|
|
63
|
+
|
|
64
|
+
private:
|
|
65
|
+
struct SearchState {
|
|
66
|
+
std::string id;
|
|
67
|
+
std::vector<std::string> roots;
|
|
68
|
+
SearchOptions options;
|
|
69
|
+
StreamBatchCallback onBatch;
|
|
70
|
+
StreamDoneCallback onDone;
|
|
71
|
+
std::atomic<bool> cancelled{false};
|
|
72
|
+
std::atomic<uint64_t> matchedCount{0};
|
|
73
|
+
std::atomic<uint64_t> scannedCount{0};
|
|
74
|
+
std::atomic<size_t> activeWorkers{0};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
std::mutex mutex_;
|
|
78
|
+
std::unordered_map<std::string, std::shared_ptr<SearchState>> searches_;
|
|
79
|
+
std::atomic<uint64_t> next_id_{1};
|
|
80
|
+
|
|
81
|
+
// Thread pool for parallel directory scanning
|
|
82
|
+
ThreadPool threadPool_;
|
|
83
|
+
|
|
84
|
+
FileSearchService();
|
|
85
|
+
~FileSearchService();
|
|
86
|
+
|
|
87
|
+
void run_search(const std::shared_ptr<SearchState>& state);
|
|
88
|
+
void search_root(const std::shared_ptr<SearchState>& state, const std::string& root,
|
|
89
|
+
std::vector<std::string>& results, std::mutex& resultsMutex);
|
|
90
|
+
void finish_search(const std::shared_ptr<SearchState>& state,
|
|
91
|
+
const StreamSearchSummary& summary);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
} // namespace wpp::fs
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export interface InputKeyEvent {
|
|
2
|
+
type: 'keydown' | 'keyup';
|
|
3
|
+
key: string;
|
|
4
|
+
keyName: string;
|
|
5
|
+
keycode: number;
|
|
6
|
+
scancode: number;
|
|
7
|
+
ctrl: boolean;
|
|
8
|
+
shift: boolean;
|
|
9
|
+
alt: boolean;
|
|
10
|
+
meta: boolean;
|
|
11
|
+
repeat: boolean;
|
|
12
|
+
global: boolean;
|
|
13
|
+
windowId: string;
|
|
14
|
+
pressedKeys: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ShortcutEvent {
|
|
18
|
+
id: string;
|
|
19
|
+
event: InputKeyEvent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PendingCall {
|
|
23
|
+
resolve: (value: any) => void;
|
|
24
|
+
reject: (reason: Error) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let callId = 0;
|
|
28
|
+
const pending = new Map<string, PendingCall>();
|
|
29
|
+
const keyListeners = new Set<(event: InputKeyEvent) => void>();
|
|
30
|
+
const shortcutListeners = new Set<(event: ShortcutEvent) => void>();
|
|
31
|
+
let streamEnabled = false;
|
|
32
|
+
|
|
33
|
+
function getGlobal(): any {
|
|
34
|
+
if (typeof window !== 'undefined') return window;
|
|
35
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const globalObject = getGlobal();
|
|
40
|
+
const previousResponse = globalObject.__response__;
|
|
41
|
+
const previousInputEvent = globalObject.__wpp_input_event__;
|
|
42
|
+
|
|
43
|
+
globalObject.__response__ = function(id: string, result: any, error?: string) {
|
|
44
|
+
const request = pending.get(id);
|
|
45
|
+
if (request) {
|
|
46
|
+
pending.delete(id);
|
|
47
|
+
if (error) {
|
|
48
|
+
request.reject(new Error(error));
|
|
49
|
+
} else {
|
|
50
|
+
request.resolve(result);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (previousResponse) {
|
|
56
|
+
previousResponse(id, result, error);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
globalObject.__wpp_input_event__ = function(eventName: string, payload: any) {
|
|
61
|
+
if (eventName === 'key') {
|
|
62
|
+
keyListeners.forEach((listener) => listener(payload as InputKeyEvent));
|
|
63
|
+
} else if (eventName === 'shortcut') {
|
|
64
|
+
shortcutListeners.forEach((listener) => listener(payload as ShortcutEvent));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (previousInputEvent) {
|
|
68
|
+
previousInputEvent(eventName, payload);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function invoke<T>(method: string, params: unknown[] = []): Promise<T> {
|
|
73
|
+
return new Promise<T>((resolve, reject) => {
|
|
74
|
+
const id = `input:${++callId}`;
|
|
75
|
+
pending.set(id, { resolve, reject });
|
|
76
|
+
const payload = JSON.stringify({ id, method, params });
|
|
77
|
+
|
|
78
|
+
if (globalObject.__native_invoke__) {
|
|
79
|
+
globalObject.__native_invoke__(payload);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (globalObject.chrome?.webview?.postMessage) {
|
|
84
|
+
globalObject.chrome.webview.postMessage(payload);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pending.delete(id);
|
|
89
|
+
reject(new Error('Native bridge not available'));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function ensureStreamEnabled() {
|
|
94
|
+
if (streamEnabled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await invoke<void>('input:setEventStreamEnabled', [true]);
|
|
98
|
+
streamEnabled = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function onKeyEvent(listener: (event: InputKeyEvent) => void): Promise<() => void> {
|
|
102
|
+
await ensureStreamEnabled();
|
|
103
|
+
keyListeners.add(listener);
|
|
104
|
+
return () => {
|
|
105
|
+
keyListeners.delete(listener);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function onShortcut(listener: (event: ShortcutEvent) => void): () => void {
|
|
110
|
+
shortcutListeners.add(listener);
|
|
111
|
+
return () => {
|
|
112
|
+
shortcutListeners.delete(listener);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getPressedKeys(): Promise<string[]> {
|
|
117
|
+
return invoke<string[]>('input:getPressedKeys');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isPressed(key: string): Promise<boolean> {
|
|
121
|
+
return invoke<boolean>('input:isPressed', [key]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function registerLocalShortcut(id: string, shortcut: string): Promise<void> {
|
|
125
|
+
return invoke<void>('input:registerLocalShortcut', [id, shortcut]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function registerAppShortcut(id: string, shortcut: string): Promise<void> {
|
|
129
|
+
return registerLocalShortcut(id, shortcut);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function registerGlobalHotkey(id: string, shortcut: string): Promise<void> {
|
|
133
|
+
return invoke<void>('input:registerGlobalHotkey', [id, shortcut]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function registerGlobalShortcut(id: string, shortcut: string): Promise<void> {
|
|
137
|
+
return registerGlobalHotkey(id, shortcut);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function unregisterShortcut(id: string): Promise<void> {
|
|
141
|
+
return invoke<void>('input:unregisterShortcut', [id]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function clearShortcuts(): Promise<void> {
|
|
145
|
+
return invoke<void>('input:clearShortcuts');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const input = {
|
|
149
|
+
onKeyEvent,
|
|
150
|
+
onShortcut,
|
|
151
|
+
getPressedKeys,
|
|
152
|
+
isPressed,
|
|
153
|
+
registerAppShortcut,
|
|
154
|
+
registerLocalShortcut,
|
|
155
|
+
registerGlobalShortcut,
|
|
156
|
+
registerGlobalHotkey,
|
|
157
|
+
unregisterShortcut,
|
|
158
|
+
clearShortcuts,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default input;
|